/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.am;

import static com.android.internal.util.Preconditions.checkState;
import static com.android.server.am.BroadcastRecord.deliveryStateToString;
import static com.android.server.am.BroadcastRecord.isReceiverEquals;

import android.annotation.CheckResult;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UptimeMillisLong;
import android.app.ActivityManager;
import android.app.BroadcastOptions;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
import android.text.format.DateUtils;
import android.util.IndentingPrintWriter;
import android.util.TimeUtils;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;

import dalvik.annotation.optimization.NeverCompile;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;

/**
 * Queue of pending {@link BroadcastRecord} entries intended for delivery to a
 * specific process.
 * <p>
 * Each queue has a concept of being "runnable at" a particular time in the
 * future, which supports arbitrarily pausing or delaying delivery on a
 * per-process basis.
 * <p>
 * Internally each queue consists of a pending broadcasts which are waiting to
 * be dispatched, and a single active broadcast which is currently being
 * dispatched.
 * <p>
 * This entire class is marked as {@code NotThreadSafe} since it's the
 * responsibility of the caller to always interact with a relevant lock held.
 */
// @NotThreadSafe
class BroadcastProcessQueue {
    static final boolean VERBOSE = false;
    final @NonNull BroadcastConstants constants;
    final @NonNull String processName;
    final int uid;

    /**
     * Linked list connection to another process under this {@link #uid} which
     * has a different {@link #processName}.
     */
    @Nullable BroadcastProcessQueue processNameNext;

    /**
     * Linked list connections to runnable process with lower and higher
     * {@link #getRunnableAt()} times.
     */
    @Nullable BroadcastProcessQueue runnableAtNext;
    @Nullable BroadcastProcessQueue runnableAtPrev;

    /**
     * Currently known details about the target process; typically undefined
     * when the process isn't actively running.
     */
    @Nullable ProcessRecord app;

    /**
     * Track name to use for {@link Trace} events, defined as part of upgrading
     * into a running slot.
     */
    @Nullable String runningTraceTrackName;

    /**
     * Flag indicating if this process should be OOM adjusted, defined as part
     * of upgrading into a running slot.
     */
    boolean runningOomAdjusted;

    /**
     * True if a timer has been started against this queue.
     */
    private boolean mTimeoutScheduled;

     /**
     * Snapshotted value of {@link ProcessRecord#getCurProcState()} before
     * dispatching the current broadcast to the receiver in this process.
     */
    int lastProcessState;

    /**
     * Ordered collection of broadcasts that are waiting to be dispatched to
     * this process, as a pair of {@link BroadcastRecord} and the index into
     * {@link BroadcastRecord#receivers} that represents the receiver.
     */
    private final ArrayDeque<SomeArgs> mPending = new ArrayDeque<>();

    /**
     * Ordered collection of "urgent" broadcasts that are waiting to be
     * dispatched to this process, in the same representation as
     * {@link #mPending}.
     */
    private final ArrayDeque<SomeArgs> mPendingUrgent = new ArrayDeque<>(4);

    /**
     * Ordered collection of "offload" broadcasts that are waiting to be
     * dispatched to this process, in the same representation as
     * {@link #mPending}.
     */
    private final ArrayDeque<SomeArgs> mPendingOffload = new ArrayDeque<>(4);

    /**
     * Broadcast actively being dispatched to this process.
     */
    private @Nullable BroadcastRecord mActive;

    /**
     * Receiver actively being dispatched to in this process. This is an index
     * into the {@link BroadcastRecord#receivers} list of {@link #mActive}.
     */
    private int mActiveIndex;

    /**
     * True if the broadcast actively being dispatched to this process was re-enqueued previously.
     */
    private boolean mActiveReEnqueued;

    /**
     * Count of {@link #mActive} broadcasts that have been dispatched since this
     * queue was last idle.
     */
    private int mActiveCountSinceIdle;

    /**
     * Count of {@link #mActive} broadcasts with assumed delivery that have been dispatched
     * since this queue was last idle.
     */
    private int mActiveAssumedDeliveryCountSinceIdle;

    /**
     * Flag indicating that the currently active broadcast is being dispatched
     * was scheduled via a cold start.
     */
    private boolean mActiveViaColdStart;

    /**
     * Flag indicating that the currently active broadcast is being dispatched
     * to a package that was in the stopped state.
     */
    private boolean mActiveWasStopped;

    /**
     * Flag indicating that the currently active broadcast is being dispatched
     * to a package that was never launched before.
     */
    private boolean mActiveFirstLaunch;

    /**
     * Number of consecutive urgent broadcasts that have been dispatched
     * since the last non-urgent dispatch.
     */
    private int mActiveCountConsecutiveUrgent;

    /**
     * Number of consecutive normal broadcasts that have been dispatched
     * since the last offload dispatch.
     */
    private int mActiveCountConsecutiveNormal;

    /**
     * Count of pending broadcasts of these various flavors.
     */
    private int mCountEnqueued;
    private int mCountDeferred;
    private int mCountForeground;
    private int mCountForegroundDeferred;
    private int mCountOrdered;
    private int mCountAlarm;
    private int mCountPrioritized;
    private int mCountPrioritizedDeferred;
    private int mCountInteractive;
    private int mCountResultTo;
    private int mCountInstrumented;
    private int mCountManifest;

    private int mCountPrioritizeEarliestRequests;

    private @UptimeMillisLong long mRunnableAt = Long.MAX_VALUE;
    private @Reason int mRunnableAtReason = REASON_EMPTY;
    private boolean mRunnableAtInvalidated;

    /**
     * Last state applied by {@link #updateDeferredStates}, used to quickly
     * determine if a state transition is occurring.
     */
    private boolean mLastDeferredStates;

    private boolean mUidForeground;
    private boolean mProcessFreezable;
    private boolean mProcessInstrumented;
    private boolean mProcessPersistent;

    private String mCachedToString;
    private String mCachedToShortString;

    /**
     * The duration by which any broadcasts to this process need to be delayed
     */
    private long mForcedDelayedDurationMs;

    /**
     * List of outgoing broadcasts from a freezable process.
     */
    private final ArrayList<BroadcastRecord> mOutgoingBroadcasts = new ArrayList<>();

    /**
     * The timestamp, in {@link SystemClock#uptimeMillis()}, at which a cold start was initiated
     * for the process associated with this queue.
     *
     * Note: We could use the already existing {@link ProcessRecord#getStartUptime()} instead
     * of this, but the need for this timestamp is to identify an issue (b/393898613) where the
     * suspicion is that process is not attached or getting changed. So, we don't want to rely on
     * ProcessRecord directly for this purpose.
     */
    private long mProcessStartInitiatedTimestampMillis;

    public BroadcastProcessQueue(@NonNull BroadcastConstants constants,
            @NonNull String processName, int uid) {
        this.constants = Objects.requireNonNull(constants);
        this.processName = Objects.requireNonNull(processName);
        this.uid = uid;
    }

    private @NonNull ArrayDeque<SomeArgs> getQueueForBroadcast(@NonNull BroadcastRecord record) {
        if (record.isUrgent()) {
            return mPendingUrgent;
        } else if (record.isOffload()) {
            return mPendingOffload;
        } else {
            return mPending;
        }
    }

    public void enqueueOutgoingBroadcast(@NonNull BroadcastRecord record) {
        mOutgoingBroadcasts.add(record);
    }

    public int getOutgoingBroadcastCount() {
        return mOutgoingBroadcasts.size();
    }

    public void enqueueOutgoingBroadcasts(@NonNull BroadcastRecordConsumer consumer) {
        for (int i = 0; i < mOutgoingBroadcasts.size(); ++i) {
            consumer.accept(mOutgoingBroadcasts.get(i));
        }
        mOutgoingBroadcasts.clear();
    }

    public void clearOutgoingBroadcasts() {
        mOutgoingBroadcasts.clear();
    }

    /**
     * Enqueue the given broadcast to be dispatched to this process at some
     * future point in time. The target receiver is indicated by the given index
     * into {@link BroadcastRecord#receivers}.
     * <p>
     * If the broadcast is marked as {@link BroadcastRecord#isReplacePending()},
     * then this call will replace any pending dispatch; otherwise it will
     * enqueue as a normal broadcast.
     * <p>
     * When defined, this receiver is considered "blocked" until at least the
     * given count of other receivers have reached a terminal state; typically
     * used for ordered broadcasts and priority traunches.
     *
     * @return the existing broadcast record in the queue that was replaced with a newer broadcast
     *         sent with {@link Intent#FLAG_RECEIVER_REPLACE_PENDING} or {@code null} if there
     *         wasn't any broadcast that was replaced.
     */
    @Nullable
    public BroadcastRecord enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record,
            int recordIndex, @NonNull BroadcastConsumer deferredStatesApplyConsumer) {
        // Ignore FLAG_RECEIVER_REPLACE_PENDING if the sender specified the policy using the
        // BroadcastOptions delivery group APIs.
        if (record.isReplacePending()
                && record.getDeliveryGroupPolicy() == BroadcastOptions.DELIVERY_GROUP_POLICY_ALL) {
            final BroadcastRecord replacedBroadcastRecord = replaceBroadcast(record, recordIndex);
            if (replacedBroadcastRecord != null) {
                if (mLastDeferredStates && shouldBeDeferred()
                        && (record.getDeliveryState(recordIndex)
                                == BroadcastRecord.DELIVERY_PENDING)) {
                    deferredStatesApplyConsumer.accept(record, recordIndex);
                }
                return replacedBroadcastRecord;
            }
        }

        // Caller isn't interested in replacing, or we didn't find any pending
        // item to replace above, so enqueue as a new broadcast
        SomeArgs newBroadcastArgs = SomeArgs.obtain();
        newBroadcastArgs.arg1 = record;
        newBroadcastArgs.argi1 = recordIndex;

        // Cross-broadcast prioritization policy:  some broadcasts might warrant being
        // issued ahead of others that are already pending, for example if this new
        // broadcast is in a different delivery class or is tied to a direct user interaction
        // with implicit responsiveness expectations.
        getQueueForBroadcast(record).addLast(newBroadcastArgs);
        onBroadcastEnqueued(record, recordIndex);

        // When updateDeferredStates() has already applied a deferred state to
        // all pending items, apply to this new broadcast too
        if (mLastDeferredStates && shouldBeDeferred()
                && (record.getDeliveryState(recordIndex) == BroadcastRecord.DELIVERY_PENDING)) {
            deferredStatesApplyConsumer.accept(record, recordIndex);
        }
        return null;
    }

    /**
     * Re-enqueue the active broadcast so that it can be made active and delivered again. In order
     * to keep its previous position same to avoid issues with reordering, insert it at the head
     * of the queue.
     *
     * Callers are responsible for clearing the active broadcast by calling
     * {@link #makeActiveIdle()} after re-enqueuing it.
     */
    public void reEnqueueActiveBroadcast() {
        final BroadcastRecord record = getActive();
        final int recordIndex = getActiveIndex();

        final SomeArgs broadcastArgs = SomeArgs.obtain();
        broadcastArgs.arg1 = record;
        broadcastArgs.argi1 = recordIndex;
        broadcastArgs.argi2 = 1;
        getQueueForBroadcast(record).addFirst(broadcastArgs);
        onBroadcastEnqueued(record, recordIndex);
    }

    /**
     * Searches from newest to oldest in the pending broadcast queues, and at the first matching
     * pending broadcast it finds, replaces it in-place and returns -- does not attempt to handle
     * "duplicate" broadcasts in the queue.
     *
     * @return the existing broadcast record in the queue that was replaced with a newer broadcast
     *         sent with {@link Intent#FLAG_RECEIVER_REPLACE_PENDING} or {@code null} if there
     *         wasn't any broadcast that was replaced.
     */
    @Nullable
    private BroadcastRecord replaceBroadcast(@NonNull BroadcastRecord record, int recordIndex) {
        final ArrayDeque<SomeArgs> queue = getQueueForBroadcast(record);
        return replaceBroadcastInQueue(queue, record, recordIndex);
    }

    /**
     * Searches from newest to oldest, and at the first matching pending broadcast
     * it finds, replaces it in-place and returns -- does not attempt to handle
     * "duplicate" broadcasts in the queue.
     *
     * @return the existing broadcast record in the queue that was replaced with a newer broadcast
     *         sent with {@link Intent#FLAG_RECEIVER_REPLACE_PENDING} or {@code null} if there
     *         wasn't any broadcast that was replaced.
     */
    @Nullable
    private BroadcastRecord replaceBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue,
            @NonNull BroadcastRecord record, int recordIndex) {
        final Iterator<SomeArgs> it = queue.descendingIterator();
        final Object receiver = record.receivers.get(recordIndex);
        while (it.hasNext()) {
            final SomeArgs args = it.next();
            final BroadcastRecord testRecord = (BroadcastRecord) args.arg1;
            final int testRecordIndex = args.argi1;
            final Object testReceiver = testRecord.receivers.get(testRecordIndex);
            // If we come across the record that's being enqueued in the queue, then that means
            // we already enqueued it for a receiver in this process and trying to insert a new
            // one past this could create priority inversion in the queue, so bail out.
            if (record == testRecord) {
                break;
            }
            if ((record.callingUid == testRecord.callingUid)
                    && (record.userId == testRecord.userId)
                    && record.intent.filterEquals(testRecord.intent)
                    && isReceiverEquals(receiver, testReceiver)
                    && testRecord.allReceiversPending()
                    && record.isMatchingRecord(testRecord)) {
                // Exact match found; perform in-place swap
                args.arg1 = record;
                args.argi1 = recordIndex;
                record.copyEnqueueTimeFrom(testRecord);
                onBroadcastDequeued(testRecord, testRecordIndex);
                onBroadcastEnqueued(record, recordIndex);
                return testRecord;
            }
        }
        return null;
    }

    /**
     * Functional interface that tests a {@link BroadcastRecord} and an index in the
     * {@link BroadcastRecord} that has been previously enqueued in {@link BroadcastProcessQueue}.
     */
    @FunctionalInterface
    public interface BroadcastPredicate {
        boolean test(@NonNull BroadcastRecord r, int index);
    }

    /**
     * Functional interface that consumes a {@link BroadcastRecord} and an index in the
     * {@link BroadcastRecord} that has been previously enqueued in {@link BroadcastProcessQueue}.
     */
    @FunctionalInterface
    public interface BroadcastConsumer {
        void accept(@NonNull BroadcastRecord r, int index);
    }

    /**
     * Functional interface that consumes a {@link BroadcastRecord} that has
     * been previously enqueued in {@link BroadcastProcessQueue}.
     */
    @FunctionalInterface
    public interface BroadcastRecordConsumer {
        void accept(@NonNull BroadcastRecord r);
    }

    /**
     * Invoke given consumer for any broadcasts matching given predicate. If
     * requested, matching broadcasts will also be removed from this queue.
     * <p>
     * Predicates that choose to remove a broadcast <em>must</em> finish
     * delivery of the matched broadcast, to ensure that situations like ordered
     * broadcasts are handled consistently.
     *
     * @return if this operation may have changed internal state, indicating
     *         that the caller is responsible for invoking
     *         {@link BroadcastQueueImpl#updateRunnableList}
     */
    @CheckResult
    public boolean forEachMatchingBroadcast(@NonNull BroadcastPredicate predicate,
            @NonNull BroadcastConsumer consumer, boolean andRemove) {
        boolean didSomething = false;
        didSomething |= forEachMatchingBroadcastInQueue(mPending,
                predicate, consumer, andRemove);
        didSomething |= forEachMatchingBroadcastInQueue(mPendingUrgent,
                predicate, consumer, andRemove);
        didSomething |= forEachMatchingBroadcastInQueue(mPendingOffload,
                predicate, consumer, andRemove);
        return didSomething;
    }

    @CheckResult
    private boolean forEachMatchingBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue,
            @NonNull BroadcastPredicate predicate, @NonNull BroadcastConsumer consumer,
            boolean andRemove) {
        boolean didSomething = false;
        final Iterator<SomeArgs> it = queue.iterator();
        while (it.hasNext()) {
            final SomeArgs args = it.next();
            final BroadcastRecord record = (BroadcastRecord) args.arg1;
            final int recordIndex = args.argi1;
            if (predicate.test(record, recordIndex)) {
                consumer.accept(record, recordIndex);
                if (andRemove) {
                    args.recycle();
                    it.remove();
                    onBroadcastDequeued(record, recordIndex);
                } else {
                    // Even if we're leaving broadcast in queue, it may have
                    // been mutated in such a way to change our runnable time
                    invalidateRunnableAt();
                }
                didSomething = true;
            }
        }
        // TODO: also check any active broadcast once we have a better "nonce"
        // representing each scheduled broadcast to avoid races
        return didSomething;
    }

    /**
     * Update the actively running "warm" process for this process.
     *
     * @return if this operation may have changed internal state, indicating
     *         that the caller is responsible for invoking
     *         {@link BroadcastQueueImpl#updateRunnableList}
     */
    @CheckResult
    public boolean setProcessAndUidState(@Nullable ProcessRecord app, boolean uidForeground,
            boolean processFreezable) {
        this.app = app;

        // Since we may have just changed our PID, invalidate cached strings
        mCachedToString = null;
        mCachedToShortString = null;

        boolean didSomething = false;
        if (app != null) {
            didSomething |= setUidForeground(uidForeground);
            didSomething |= setProcessFreezable(processFreezable);
            didSomething |= setProcessInstrumented(app.getActiveInstrumentation() != null);
            didSomething |= setProcessPersistent(app.isPersistent());
        } else {
            didSomething |= setUidForeground(false);
            didSomething |= setProcessFreezable(false);
            didSomething |= setProcessInstrumented(false);
            didSomething |= setProcessPersistent(false);
        }
        return didSomething;
    }

    /**
     * Update if the UID this process is belongs to is in "foreground" state, which signals
     * broadcast dispatch should prioritize delivering broadcasts to this process to minimize any
     * delays in UI updates.
     */
    @CheckResult
    private boolean setUidForeground(boolean uidForeground) {
        if (mUidForeground != uidForeground) {
            mUidForeground = uidForeground;
            invalidateRunnableAt();
            return true;
        } else {
            return false;
        }
    }

    /**
     * Update if this process is in the "freezable" state, typically signaling that
     * broadcast dispatch should be paused or delayed.
     */
    @CheckResult
    private boolean setProcessFreezable(boolean freezable) {
        if (mProcessFreezable != freezable) {
            mProcessFreezable = freezable;
            invalidateRunnableAt();
            return true;
        } else {
            return false;
        }
    }

    /**
     * Update if this process is in the "instrumented" state, typically
     * signaling that broadcast dispatch should bypass all pauses or delays, to
     * avoid holding up test suites.
     */
    @CheckResult
    private boolean setProcessInstrumented(boolean instrumented) {
        if (mProcessInstrumented != instrumented) {
            mProcessInstrumented = instrumented;
            invalidateRunnableAt();
            return true;
        } else {
            return false;
        }
    }

    /**
     * Update if this process is in the "persistent" state, which signals broadcast dispatch should
     * bypass all pauses or delays to prevent the system from becoming out of sync with itself.
     */
    @CheckResult
    private boolean setProcessPersistent(boolean persistent) {
        if (mProcessPersistent != persistent) {
            mProcessPersistent = persistent;
            invalidateRunnableAt();
            return true;
        } else {
            return false;
        }
    }

    /**
     * Return if we know of an actively running "warm" process for this queue.
     */
    public boolean isProcessWarm() {
        return (app != null) && (app.getOnewayThread() != null) && !app.isKilled();
    }

    public int getPreferredSchedulingGroupLocked() {
        if (!isActive()) {
            return ProcessList.SCHED_GROUP_UNDEFINED;
        } else if (mCountForeground > mCountForegroundDeferred) {
            // We have a foreground broadcast somewhere down the queue, so
            // boost priority until we drain them all
            return ProcessList.SCHED_GROUP_DEFAULT;
        } else if ((mActive != null) && mActive.isForeground()) {
            // We have a foreground broadcast right now, so boost priority
            return ProcessList.SCHED_GROUP_DEFAULT;
        } else {
            return ProcessList.SCHED_GROUP_BACKGROUND;
        }
    }

    /**
     * Count of {@link #mActive} broadcasts that have been dispatched since this
     * queue was last idle.
     */
    public int getActiveCountSinceIdle() {
        return mActiveCountSinceIdle;
    }

    /**
     * Count of {@link #mActive} broadcasts with assumed delivery that have been dispatched
     * since this queue was last idle.
     */
    public int getActiveAssumedDeliveryCountSinceIdle() {
        return mActiveAssumedDeliveryCountSinceIdle;
    }

    public void setActiveViaColdStart(boolean activeViaColdStart) {
        mActiveViaColdStart = activeViaColdStart;
    }

    public void setActiveWasStopped(boolean activeWasStopped) {
        mActiveWasStopped = activeWasStopped;
    }

    public void setActiveFirstLaunch(boolean activeFirstLaunch) {
        mActiveFirstLaunch = activeFirstLaunch;
    }

    public boolean getActiveViaColdStart() {
        return mActiveViaColdStart;
    }

    public boolean getActiveWasStopped() {
        return mActiveWasStopped;
    }

    public boolean getActiveFirstLaunch() {
        return mActiveFirstLaunch;
    }

    public void setProcessStartInitiatedTimestampMillis(@UptimeMillisLong long timestampMillis) {
        mProcessStartInitiatedTimestampMillis = timestampMillis;
    }

    @UptimeMillisLong
    public long getProcessStartInitiatedTimestampMillis() {
        return mProcessStartInitiatedTimestampMillis;
    }

    public boolean hasProcessStartInitiationTimedout() {
        if (mProcessStartInitiatedTimestampMillis <= 0) {
            return false;
        }
        return (SystemClock.uptimeMillis() - mProcessStartInitiatedTimestampMillis)
                > constants.PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS;
    }

    /**
     * Returns if the process start initiation is expected to be timed out at this point. This
     * allows us to dump necessary state for debugging before the process start is timed out
     * and discarded.
     */
    public boolean isProcessStartInitiationTimeoutExpected() {
        if (mProcessStartInitiatedTimestampMillis <= 0) {
            return false;
        }
        return (SystemClock.uptimeMillis() - mProcessStartInitiatedTimestampMillis)
                > constants.PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS / 2;
    }

    public void clearProcessStartInitiatedTimestampMillis() {
        mProcessStartInitiatedTimestampMillis = 0;
    }

    /**
     * Get package name of the first application loaded into this process.
     */
    @Nullable
    public String getPackageName() {
        return app == null ? null : app.getApplicationInfo().packageName;
    }

    /**
     * Set the currently active broadcast to the next pending broadcast.
     */
    public void makeActiveNextPending() {
        // TODO: what if the next broadcast isn't runnable yet?
        final SomeArgs next = removeNextBroadcast();
        mActive = (BroadcastRecord) next.arg1;
        mActiveIndex = next.argi1;
        mActiveReEnqueued = (next.argi2 == 1);
        mActiveCountSinceIdle++;
        mActiveAssumedDeliveryCountSinceIdle +=
                (mActive.isAssumedDelivered(mActiveIndex) ? 1 : 0);
        mActiveViaColdStart = false;
        mActiveWasStopped = false;
        next.recycle();
        onBroadcastDequeued(mActive, mActiveIndex);
    }

    /**
     * Set the currently running broadcast to be idle.
     */
    public void makeActiveIdle() {
        mActive = null;
        mActiveIndex = 0;
        mActiveReEnqueued = false;
        mActiveCountSinceIdle = 0;
        mActiveAssumedDeliveryCountSinceIdle = 0;
        mActiveViaColdStart = false;
        invalidateRunnableAt();
    }

    public boolean wasActiveBroadcastReEnqueued() {
        return mActiveReEnqueued;
    }

    /**
     * Update summary statistics when the given record has been enqueued.
     */
    private void onBroadcastEnqueued(@NonNull BroadcastRecord record, int recordIndex) {
        mCountEnqueued++;
        if (record.deferUntilActive) {
            mCountDeferred++;
        }
        if (record.isForeground()) {
            if (record.deferUntilActive) {
                mCountForegroundDeferred++;
            }
            mCountForeground++;
        }
        if (record.ordered) {
            mCountOrdered++;
        }
        if (record.alarm) {
            mCountAlarm++;
        }
        if (record.prioritized) {
            if (record.deferUntilActive) {
                mCountPrioritizedDeferred++;
            }
            mCountPrioritized++;
        }
        if (record.interactive) {
            mCountInteractive++;
        }
        if (record.resultTo != null) {
            mCountResultTo++;
        }
        if (record.callerInstrumented) {
            mCountInstrumented++;
        }
        if (record.receivers.get(recordIndex) instanceof ResolveInfo) {
            mCountManifest++;
        }
        invalidateRunnableAt();
    }

    /**
     * Update summary statistics when the given record has been dequeued.
     */
    private void onBroadcastDequeued(@NonNull BroadcastRecord record, int recordIndex) {
        mCountEnqueued--;
        if (record.deferUntilActive) {
            mCountDeferred--;
        }
        if (record.isForeground()) {
            if (record.deferUntilActive) {
                mCountForegroundDeferred--;
            }
            mCountForeground--;
        }
        if (record.ordered) {
            mCountOrdered--;
        }
        if (record.alarm) {
            mCountAlarm--;
        }
        if (record.prioritized) {
            if (record.deferUntilActive) {
                mCountPrioritizedDeferred--;
            }
            mCountPrioritized--;
        }
        if (record.interactive) {
            mCountInteractive--;
        }
        if (record.resultTo != null) {
            mCountResultTo--;
        }
        if (record.callerInstrumented) {
            mCountInstrumented--;
        }
        if (record.receivers.get(recordIndex) instanceof ResolveInfo) {
            mCountManifest--;
        }
        invalidateRunnableAt();
    }

    public void traceProcessStartingBegin() {
        Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                runningTraceTrackName, toShortString() + " starting", hashCode());
    }

    public void traceProcessRunningBegin() {
        Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                runningTraceTrackName, toShortString() + " running", hashCode());
    }

    public void traceProcessEnd() {
        Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                runningTraceTrackName, hashCode());
    }

    public void traceActiveBegin() {
        Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                runningTraceTrackName, mActive.toShortString() + " scheduled", hashCode());
    }

    public void traceActiveEnd() {
        Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                runningTraceTrackName, hashCode());
    }

    /**
     * Return the broadcast being actively dispatched in this process.
     */
    public @NonNull BroadcastRecord getActive() {
        return Objects.requireNonNull(mActive, toString());
    }

    /**
     * Return the index into {@link BroadcastRecord#receivers} of the receiver
     * being actively dispatched in this process.
     */
    public int getActiveIndex() {
        Objects.requireNonNull(mActive, toString());
        return mActiveIndex;
    }

    public boolean isOutgoingEmpty() {
        return mOutgoingBroadcasts.isEmpty();
    }

    public boolean isEmpty() {
        return mPending.isEmpty() && mPendingUrgent.isEmpty() && mPendingOffload.isEmpty();
    }

    public boolean isActive() {
        return mActive != null;
    }

    /**
     * @return if this operation may have changed internal state, indicating
     *         that the caller is responsible for invoking
     *         {@link BroadcastQueueImpl#updateRunnableList}
     */
    @CheckResult
    boolean forceDelayBroadcastDelivery(long delayedDurationMs) {
        if (mForcedDelayedDurationMs != delayedDurationMs) {
            mForcedDelayedDurationMs = delayedDurationMs;
            invalidateRunnableAt();
            return true;
        } else {
            return false;
        }
    }

    /**
     * Will thrown an exception if there are no pending broadcasts; relies on
     * {@link #isEmpty()} being false.
     */
    private @Nullable SomeArgs removeNextBroadcast() {
        final ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
        if (queue == mPendingUrgent) {
            mActiveCountConsecutiveUrgent++;
        } else if (queue == mPending) {
            mActiveCountConsecutiveUrgent = 0;
            mActiveCountConsecutiveNormal++;
        } else if (queue == mPendingOffload) {
            mActiveCountConsecutiveUrgent = 0;
            mActiveCountConsecutiveNormal = 0;
        }
        return !isQueueEmpty(queue) ? queue.removeFirst() : null;
    }

    @Nullable ArrayDeque<SomeArgs> queueForNextBroadcast() {
        final ArrayDeque<SomeArgs> nextNormal = queueForNextBroadcast(
                mPending, mPendingOffload,
                mActiveCountConsecutiveNormal, constants.MAX_CONSECUTIVE_NORMAL_DISPATCHES);
        final ArrayDeque<SomeArgs> nextBroadcastQueue = queueForNextBroadcast(
                mPendingUrgent, nextNormal,
                mActiveCountConsecutiveUrgent, constants.MAX_CONSECUTIVE_URGENT_DISPATCHES);
        return nextBroadcastQueue;
    }

    private @Nullable ArrayDeque<SomeArgs> queueForNextBroadcast(
            @Nullable ArrayDeque<SomeArgs> highPriorityQueue,
            @Nullable ArrayDeque<SomeArgs> lowPriorityQueue,
            int consecutiveHighPriorityCount,
            int maxHighPriorityDispatchLimit) {
        // nothing high priority pending, no further decisionmaking
        if (isQueueEmpty(highPriorityQueue)) {
            return lowPriorityQueue;
        }
        // nothing but high priority pending, also no further decisionmaking
        if (isQueueEmpty(lowPriorityQueue)) {
            return highPriorityQueue;
        }

        // Starvation mitigation: although we prioritize high priority queues by default,
        // we allow low priority queues to make steady progress even if broadcasts in
        // high priority queue are arriving faster than they can be dispatched.
        //
        // We do not try to defer to the next broadcast in low priority queues if that broadcast
        // is ordered and still blocked on delivery to other recipients.
        final SomeArgs nextLPArgs = lowPriorityQueue.peekFirst();
        final BroadcastRecord nextLPRecord = (BroadcastRecord) nextLPArgs.arg1;
        final int nextLPRecordIndex = nextLPArgs.argi1;
        final BroadcastRecord nextHPRecord = (BroadcastRecord) highPriorityQueue.peekFirst().arg1;
        final boolean shouldConsiderLPQueue = (mCountPrioritizeEarliestRequests > 0
                || consecutiveHighPriorityCount >= maxHighPriorityDispatchLimit);
        final boolean isLPQueueEligible = shouldConsiderLPQueue
                && nextLPRecord.enqueueTime <= nextHPRecord.enqueueTime
                && !nextLPRecord.isBlocked(nextLPRecordIndex);
        return isLPQueueEligible ? lowPriorityQueue : highPriorityQueue;
    }

    private static boolean isQueueEmpty(@Nullable ArrayDeque<SomeArgs> queue) {
        return (queue == null || queue.isEmpty());
    }

    /**
     * Add a request to prioritize dispatching of broadcasts that have been enqueued the earliest,
     * even if there are urgent broadcasts waiting to be dispatched. This is typically used in
     * case there are callers waiting for "barrier" to be reached.
     *
     * @return if this operation may have changed internal state, indicating
     *         that the caller is responsible for invoking
     *         {@link BroadcastQueueImpl#updateRunnableList}
     */
    @CheckResult
    @VisibleForTesting
    boolean addPrioritizeEarliestRequest() {
        if (mCountPrioritizeEarliestRequests == 0) {
            mCountPrioritizeEarliestRequests++;
            invalidateRunnableAt();
            return true;
        } else {
            mCountPrioritizeEarliestRequests++;
            return false;
        }
    }

    /**
     * Remove a request to prioritize dispatching of broadcasts that have been enqueued the
     * earliest, even if there are urgent broadcasts waiting to be dispatched. This is typically
     * used in case there are callers waiting for "barrier" to be reached.
     *
     * <p> Once there are no more remaining requests, the dispatching order reverts back to normal.
     *
     * @return if this operation may have changed internal state, indicating
     *         that the caller is responsible for invoking
     *         {@link BroadcastQueueImpl#updateRunnableList}
     */
    @CheckResult
    boolean removePrioritizeEarliestRequest() {
        mCountPrioritizeEarliestRequests--;
        if (mCountPrioritizeEarliestRequests == 0) {
            invalidateRunnableAt();
            return true;
        } else if (mCountPrioritizeEarliestRequests < 0) {
            mCountPrioritizeEarliestRequests = 0;
            return false;
        } else {
            return false;
        }
    }

    /**
     * Returns null if there are no pending broadcasts
     */
    @Nullable SomeArgs peekNextBroadcast() {
        ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
        return !isQueueEmpty(queue) ? queue.peekFirst() : null;
    }

    @VisibleForTesting
    @Nullable BroadcastRecord peekNextBroadcastRecord() {
        ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
        return !isQueueEmpty(queue) ? (BroadcastRecord) queue.peekFirst().arg1 : null;
    }

    /**
     * Quickly determine if this queue has broadcasts waiting to be delivered to
     * manifest receivers, which indicates we should request an OOM adjust.
     */
    public boolean isPendingManifest() {
        return mCountManifest > 0;
    }

    /**
     * Quickly determine if this queue has ordered broadcasts waiting to be delivered,
     * which indicates we should request an OOM adjust.
     */
    public boolean isPendingOrdered() {
        return mCountOrdered > 0;
    }

    /**
     * Quickly determine if this queue has broadcasts waiting to be delivered for which result is
     * expected from the senders, which indicates we should request an OOM adjust.
     */
    public boolean isPendingResultTo() {
        return mCountResultTo > 0;
    }

    /**
     * Report whether this queue is currently handling an urgent broadcast.
     */
    public boolean isPendingUrgent() {
        BroadcastRecord next = peekNextBroadcastRecord();
        return (next != null) ? next.isUrgent() : false;
    }

    /**
     * Quickly determine if this queue has broadcasts that are still waiting to
     * be delivered at some point in the future.
     */
    public boolean isIdle() {
        return (!isActive() && isEmpty()) || isDeferredUntilActive();
    }

    /**
     * Quickly determine if this queue has non-deferred broadcasts enqueued before the given
     * barrier timestamp that are still waiting to be delivered.
     */
    public boolean isBeyondBarrierLocked(@UptimeMillisLong long barrierTime) {
        final SomeArgs next = mPending.peekFirst();
        final SomeArgs nextUrgent = mPendingUrgent.peekFirst();
        final SomeArgs nextOffload = mPendingOffload.peekFirst();

        // Empty records are always past any barrier
        final boolean activeBeyond = (mActive == null)
                || mActive.enqueueTime > barrierTime;
        final boolean nextBeyond = (next == null)
                || ((BroadcastRecord) next.arg1).enqueueTime > barrierTime;
        final boolean nextUrgentBeyond = (nextUrgent == null)
                || ((BroadcastRecord) nextUrgent.arg1).enqueueTime > barrierTime;
        final boolean nextOffloadBeyond = (nextOffload == null)
                || ((BroadcastRecord) nextOffload.arg1).enqueueTime > barrierTime;

        return (activeBeyond && nextBeyond && nextUrgentBeyond && nextOffloadBeyond)
                || isDeferredUntilActive();
    }

    /**
     * Quickly determine if this queue has non-deferred broadcasts waiting to be dispatched,
     * that match {@code intent}, as defined by {@link Intent#filterEquals(Intent)}.
     */
    public boolean isDispatched(@NonNull Intent intent) {
        final boolean activeDispatched = (mActive == null)
                || (!intent.filterEquals(mActive.intent));
        final boolean dispatched = isDispatchedInQueue(mPending, intent);
        final boolean urgentDispatched = isDispatchedInQueue(mPendingUrgent, intent);
        final boolean offloadDispatched = isDispatchedInQueue(mPendingOffload, intent);

        return (activeDispatched && dispatched && urgentDispatched && offloadDispatched)
                || isDeferredUntilActive();
    }

    /**
     * Quickly determine if the {@code queue} has non-deferred broadcasts waiting to be dispatched,
     * that match {@code intent}, as defined by {@link Intent#filterEquals(Intent)}.
     */
    private boolean isDispatchedInQueue(@NonNull ArrayDeque<SomeArgs> queue,
            @NonNull Intent intent) {
        final Iterator<SomeArgs> it = queue.iterator();
        while (it.hasNext()) {
            final SomeArgs args = it.next();
            if (args == null) {
                return true;
            }
            final BroadcastRecord record = (BroadcastRecord) args.arg1;
            if (intent.filterEquals(record.intent)) {
                return false;
            }
        }
        return true;
    }

    public boolean isRunnable() {
        if (mRunnableAtInvalidated) updateRunnableAt();
        return mRunnableAt != Long.MAX_VALUE;
    }

    public boolean isDeferredUntilActive() {
        if (mRunnableAtInvalidated) updateRunnableAt();
        return mRunnableAtReason == BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER;
    }

    public boolean hasDeferredBroadcasts() {
        return (mCountDeferred > 0);
    }

    /**
     * Return time at which this process is considered runnable. This is
     * typically the time at which the next pending broadcast was first
     * enqueued, but it also reflects any pauses or delays that should be
     * applied to the process.
     * <p>
     * Returns {@link Long#MAX_VALUE} when this queue isn't currently runnable,
     * typically when the queue is empty or when paused.
     */
    public @UptimeMillisLong long getRunnableAt() {
        if (mRunnableAtInvalidated) updateRunnableAt();
        return mRunnableAt;
    }

    /**
     * Return the "reason" behind the current {@link #getRunnableAt()} value,
     * such as indicating why the queue is being delayed or paused.
     */
    public @Reason int getRunnableAtReason() {
        if (mRunnableAtInvalidated) updateRunnableAt();
        return mRunnableAtReason;
    }

    public void invalidateRunnableAt() {
        mRunnableAtInvalidated = true;
    }

    static final int REASON_EMPTY = 0;
    static final int REASON_CACHED = 1;
    static final int REASON_NORMAL = 2;
    static final int REASON_MAX_PENDING = 3;
    static final int REASON_BLOCKED = 4;
    static final int REASON_INSTRUMENTED = 5;
    static final int REASON_PERSISTENT = 6;
    static final int REASON_FORCE_DELAYED = 7;
    static final int REASON_CACHED_INFINITE_DEFER = 8;
    static final int REASON_CONTAINS_FOREGROUND = 10;
    static final int REASON_CONTAINS_ORDERED = 11;
    static final int REASON_CONTAINS_ALARM = 12;
    static final int REASON_CONTAINS_PRIORITIZED = 13;
    static final int REASON_CONTAINS_INTERACTIVE = 14;
    static final int REASON_CONTAINS_RESULT_TO = 15;
    static final int REASON_CONTAINS_INSTRUMENTED = 16;
    static final int REASON_CONTAINS_MANIFEST = 17;
    static final int REASON_FOREGROUND = 18;
    static final int REASON_CORE_UID = 19;
    static final int REASON_TOP_PROCESS = 20;

    @IntDef(flag = false, prefix = { "REASON_" }, value = {
            REASON_EMPTY,
            REASON_CACHED,
            REASON_NORMAL,
            REASON_MAX_PENDING,
            REASON_BLOCKED,
            REASON_INSTRUMENTED,
            REASON_PERSISTENT,
            REASON_FORCE_DELAYED,
            REASON_CACHED_INFINITE_DEFER,
            REASON_CONTAINS_FOREGROUND,
            REASON_CONTAINS_ORDERED,
            REASON_CONTAINS_ALARM,
            REASON_CONTAINS_PRIORITIZED,
            REASON_CONTAINS_INTERACTIVE,
            REASON_CONTAINS_RESULT_TO,
            REASON_CONTAINS_INSTRUMENTED,
            REASON_CONTAINS_MANIFEST,
            REASON_FOREGROUND,
            REASON_CORE_UID,
            REASON_TOP_PROCESS,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Reason {}

    static @NonNull String reasonToString(@Reason int reason) {
        switch (reason) {
            case REASON_EMPTY: return "EMPTY";
            case REASON_CACHED: return "CACHED";
            case REASON_NORMAL: return "NORMAL";
            case REASON_MAX_PENDING: return "MAX_PENDING";
            case REASON_BLOCKED: return "BLOCKED";
            case REASON_INSTRUMENTED: return "INSTRUMENTED";
            case REASON_PERSISTENT: return "PERSISTENT";
            case REASON_FORCE_DELAYED: return "FORCE_DELAYED";
            case REASON_CACHED_INFINITE_DEFER: return "INFINITE_DEFER";
            case REASON_CONTAINS_FOREGROUND: return "CONTAINS_FOREGROUND";
            case REASON_CONTAINS_ORDERED: return "CONTAINS_ORDERED";
            case REASON_CONTAINS_ALARM: return "CONTAINS_ALARM";
            case REASON_CONTAINS_PRIORITIZED: return "CONTAINS_PRIORITIZED";
            case REASON_CONTAINS_INTERACTIVE: return "CONTAINS_INTERACTIVE";
            case REASON_CONTAINS_RESULT_TO: return "CONTAINS_RESULT_TO";
            case REASON_CONTAINS_INSTRUMENTED: return "CONTAINS_INSTRUMENTED";
            case REASON_CONTAINS_MANIFEST: return "CONTAINS_MANIFEST";
            case REASON_FOREGROUND: return "FOREGROUND";
            case REASON_CORE_UID: return "CORE_UID";
            case REASON_TOP_PROCESS: return "TOP_PROCESS";
            default: return Integer.toString(reason);
        }
    }

    /**
     * Update {@link #getRunnableAt()}, when needed.
     */
    void updateRunnableAt() {
        if (!mRunnableAtInvalidated) return;
        mRunnableAtInvalidated = false;

        final SomeArgs next = peekNextBroadcast();
        if (next != null) {
            final BroadcastRecord r = (BroadcastRecord) next.arg1;
            final int index = next.argi1;
            final long runnableAt = r.enqueueTime;

            if (r.isBlocked(index)) {
                mRunnableAt = Long.MAX_VALUE;
                mRunnableAtReason = REASON_BLOCKED;
                return;
            }

            if (mForcedDelayedDurationMs > 0) {
                mRunnableAt = runnableAt + mForcedDelayedDurationMs;
                mRunnableAtReason = REASON_FORCE_DELAYED;
            } else if (mCountForeground > mCountForegroundDeferred) {
                mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS;
                mRunnableAtReason = REASON_CONTAINS_FOREGROUND;
            } else if (mCountInteractive > 0) {
                mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS;
                mRunnableAtReason = REASON_CONTAINS_INTERACTIVE;
            } else if (mCountInstrumented > 0) {
                mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS;
                mRunnableAtReason = REASON_CONTAINS_INSTRUMENTED;
            } else if (mProcessInstrumented) {
                mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS;
                mRunnableAtReason = REASON_INSTRUMENTED;
            } else if (mUidForeground) {
                mRunnableAt = runnableAt + constants.DELAY_FOREGROUND_PROC_MILLIS;
                mRunnableAtReason = REASON_FOREGROUND;
            } else if (app != null && app.getSetProcState() == ActivityManager.PROCESS_STATE_TOP) {
                // TODO (b/287676625): Use a callback to check when a process goes in and out of
                // the TOP state.
                mRunnableAt = runnableAt + constants.DELAY_FOREGROUND_PROC_MILLIS;
                mRunnableAtReason = REASON_TOP_PROCESS;
            } else if (mProcessPersistent) {
                mRunnableAt = runnableAt + constants.DELAY_PERSISTENT_PROC_MILLIS;
                mRunnableAtReason = REASON_PERSISTENT;
            } else if (mCountOrdered > 0) {
                mRunnableAt = runnableAt;
                mRunnableAtReason = REASON_CONTAINS_ORDERED;
            } else if (mCountAlarm > 0) {
                mRunnableAt = runnableAt;
                mRunnableAtReason = REASON_CONTAINS_ALARM;
            } else if (mCountPrioritized > mCountPrioritizedDeferred) {
                mRunnableAt = runnableAt;
                mRunnableAtReason = REASON_CONTAINS_PRIORITIZED;
            } else if (mCountManifest > 0) {
                mRunnableAt = runnableAt;
                mRunnableAtReason = REASON_CONTAINS_MANIFEST;
            } else if (mProcessFreezable) {
                if (r.deferUntilActive) {
                    // All enqueued broadcasts are deferrable, defer
                    if (mCountDeferred == mCountEnqueued) {
                        mRunnableAt = Long.MAX_VALUE;
                        mRunnableAtReason = REASON_CACHED_INFINITE_DEFER;
                    } else {
                        // At least one enqueued broadcast isn't deferrable, repick time and reason
                        // for this record. If a later record is not deferrable and is one of these
                        // special cases, one of the cases above would have already caught that.
                        if (r.isForeground()) {
                            mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS;
                            mRunnableAtReason = REASON_CONTAINS_FOREGROUND;
                        } else if (r.prioritized) {
                            mRunnableAt = runnableAt;
                            mRunnableAtReason = REASON_CONTAINS_PRIORITIZED;
                        } else if (r.resultTo != null) {
                            mRunnableAt = runnableAt;
                            mRunnableAtReason = REASON_CONTAINS_RESULT_TO;
                        } else {
                            mRunnableAt = runnableAt + constants.DELAY_CACHED_MILLIS;
                            mRunnableAtReason = REASON_CACHED;
                        }
                    }
                } else {
                    // This record isn't deferrable
                    mRunnableAt = runnableAt + constants.DELAY_CACHED_MILLIS;
                    mRunnableAtReason = REASON_CACHED;
                }
            } else if (mCountResultTo > 0) {
                // All resultTo broadcasts are infinitely deferrable, so if the app
                // is already cached, they'll be deferred on the line above
                mRunnableAt = runnableAt;
                mRunnableAtReason = REASON_CONTAINS_RESULT_TO;
            } else if (UserHandle.isCore(uid)) {
                mRunnableAt = runnableAt;
                mRunnableAtReason = REASON_CORE_UID;
            } else {
                mRunnableAt = runnableAt + constants.DELAY_NORMAL_MILLIS;
                mRunnableAtReason = REASON_NORMAL;
            }

            // If we have too many broadcasts pending, bypass any delays that
            // might have been applied above to aid draining
            if (mPending.size() + mPendingUrgent.size()
                    + mPendingOffload.size() >= constants.MAX_PENDING_BROADCASTS) {
                mRunnableAt = Math.min(mRunnableAt, runnableAt);
                mRunnableAtReason = REASON_MAX_PENDING;
            }

            if (VERBOSE) {
                Trace.instantForTrack(Trace.TRACE_TAG_ACTIVITY_MANAGER, "BroadcastQueue",
                        ((app != null) ? app.processName : "(null)")
                        + ":" + r.intent.toString() + ":"
                        + r.deferUntilActive
                        + ":" + mRunnableAt + " " + reasonToString(mRunnableAtReason)
                        + ":" + ((app != null) ? app.isCached() : "false"));
            }
        } else {
            mRunnableAt = Long.MAX_VALUE;
            mRunnableAtReason = REASON_EMPTY;
        }
    }

    /**
     * Update {@link BroadcastRecord#DELIVERY_DEFERRED} states of all our
     * pending broadcasts, when needed.
     */
    void updateDeferredStates(@NonNull BroadcastConsumer applyConsumer,
            @NonNull BroadcastConsumer clearConsumer) {
        // When all we have pending is deferred broadcasts, and we're cached,
        // then we want everything to be marked deferred
        final boolean wantDeferredStates = shouldBeDeferred();

        if (mLastDeferredStates != wantDeferredStates) {
            mLastDeferredStates = wantDeferredStates;
            if (wantDeferredStates) {
                forEachMatchingBroadcast((r, i) -> {
                    return (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_PENDING);
                }, applyConsumer, false);
            } else {
                forEachMatchingBroadcast((r, i) -> {
                    return (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_DEFERRED);
                }, clearConsumer, false);
            }
        }
    }

    void clearDeferredStates(@NonNull BroadcastConsumer clearConsumer) {
        if (mLastDeferredStates) {
            mLastDeferredStates = false;
            forEachMatchingBroadcast((r, i) -> {
                return (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_DEFERRED);
            }, clearConsumer, false);
        }
    }

    @VisibleForTesting
    boolean shouldBeDeferred() {
        if (mRunnableAtInvalidated) updateRunnableAt();
        return mRunnableAtReason == REASON_CACHED
                || mRunnableAtReason == REASON_CACHED_INFINITE_DEFER;
    }

    /**
     * Check overall health, confirming things are in a reasonable state and
     * that we're not wedged.
     */
    public void assertHealthLocked() {
        // If we're not actively running, we should be sorted into the runnable
        // list, and if we're invalidated then someone likely forgot to invoke
        // updateRunnableList() to re-sort us into place
        if (!isActive()) {
            checkState(!mRunnableAtInvalidated, "mRunnableAtInvalidated");
        }

        assertHealthLocked(mPending);
        assertHealthLocked(mPendingUrgent);
        assertHealthLocked(mPendingOffload);
    }

    private void assertHealthLocked(@NonNull ArrayDeque<SomeArgs> queue) {
        if (queue.isEmpty()) return;

        final Iterator<SomeArgs> it = queue.descendingIterator();
        while (it.hasNext()) {
            final SomeArgs args = it.next();
            final BroadcastRecord record = (BroadcastRecord) args.arg1;
            final int recordIndex = args.argi1;

            if (BroadcastRecord.isDeliveryStateTerminal(record.getDeliveryState(recordIndex))
                    || record.isDeferUntilActive()) {
                continue;
            } else {
                // If waiting more than 10 minutes, we're likely wedged
                final long waitingTime = SystemClock.uptimeMillis() - record.enqueueTime;
                checkState(waitingTime < (10 * DateUtils.MINUTE_IN_MILLIS), "waitingTime");
            }
        }
    }

    /**
     * Insert the given queue into a sorted linked list of "runnable" queues.
     *
     * @param head the current linked list head
     * @param item the queue to insert
     * @return a potentially updated linked list head
     */
    @VisibleForTesting
    static @Nullable BroadcastProcessQueue insertIntoRunnableList(
            @Nullable BroadcastProcessQueue head, @NonNull BroadcastProcessQueue item) {
        if (head == null) {
            return item;
        }
        final long itemRunnableAt = item.getRunnableAt();
        BroadcastProcessQueue test = head;
        BroadcastProcessQueue tail = null;
        while (test != null) {
            if (test.getRunnableAt() > itemRunnableAt) {
                item.runnableAtNext = test;
                item.runnableAtPrev = test.runnableAtPrev;
                if (item.runnableAtNext != null) {
                    item.runnableAtNext.runnableAtPrev = item;
                }
                if (item.runnableAtPrev != null) {
                    item.runnableAtPrev.runnableAtNext = item;
                }
                return (test == head) ? item : head;
            }
            tail = test;
            test = test.runnableAtNext;
        }
        item.runnableAtPrev = tail;
        item.runnableAtPrev.runnableAtNext = item;
        return head;
    }

    /**
     * Remove the given queue from a sorted linked list of "runnable" queues.
     *
     * @param head the current linked list head
     * @param item the queue to remove
     * @return a potentially updated linked list head
     */
    @VisibleForTesting
    static @Nullable BroadcastProcessQueue removeFromRunnableList(
            @Nullable BroadcastProcessQueue head, @NonNull BroadcastProcessQueue item) {
        if (head == item) {
            head = item.runnableAtNext;
        }
        if (item.runnableAtNext != null) {
            item.runnableAtNext.runnableAtPrev = item.runnableAtPrev;
        }
        if (item.runnableAtPrev != null) {
            item.runnableAtPrev.runnableAtNext = item.runnableAtNext;
        }
        item.runnableAtNext = null;
        item.runnableAtPrev = null;
        return head;
    }

    /**
     * Set the timeout flag to indicate that an ANR timer has been started.  A value of true means a
     * timer is running; a value of false means there is no timer running.
     */
    void setTimeoutScheduled(boolean timeoutScheduled) {
        mTimeoutScheduled = timeoutScheduled;
    }

    /**
     * Get the timeout flag
     */
    boolean timeoutScheduled() {
        return mTimeoutScheduled;
    }

    @Override
    public String toString() {
        if (mCachedToString == null) {
            mCachedToString = "BroadcastProcessQueue{" + toShortString() + "}";
        }
        return mCachedToString;
    }

    public String toShortString() {
        if (mCachedToShortString == null) {
            mCachedToShortString = Integer.toHexString(System.identityHashCode(this))
                    + " " + ((app != null) ? app.getPid() : "?") + ":" + processName + "/"
                    + UserHandle.formatUid(uid);
        }
        return mCachedToShortString;
    }

    public String describeStateLocked() {
        return describeStateLocked(SystemClock.uptimeMillis());
    }

    public String describeStateLocked(@UptimeMillisLong long now) {
        final StringBuilder sb = new StringBuilder();
        if (isRunnable()) {
            sb.append("runnable at ");
            TimeUtils.formatDuration(getRunnableAt(), now, sb);
        } else {
            sb.append("not runnable");
        }
        sb.append(" because ");
        sb.append(reasonToString(mRunnableAtReason));
        return sb.toString();
    }

    @NeverCompile
    public void dumpLocked(@UptimeMillisLong long now, @NonNull IndentingPrintWriter pw) {
        if ((mActive == null) && isEmpty() && isOutgoingEmpty()) return;

        pw.print(toShortString());
        pw.print(" ");
        pw.print(describeStateLocked(now));
        pw.println();

        pw.increaseIndent();
        dumpProcessState(pw);
        dumpBroadcastCounts(pw);

        if (!mOutgoingBroadcasts.isEmpty()) {
            for (int i = 0; i < mOutgoingBroadcasts.size(); ++i) {
                dumpOutgoingRecord(now, pw, mOutgoingBroadcasts.get(i));
            }
        }

        if (mActive != null) {
            dumpRecord("ACTIVE", now, pw, mActive, mActiveIndex);
        }
        for (SomeArgs args : mPendingUrgent) {
            final BroadcastRecord r = (BroadcastRecord) args.arg1;
            dumpRecord("URGENT", now, pw, r, args.argi1);
        }
        for (SomeArgs args : mPending) {
            final BroadcastRecord r = (BroadcastRecord) args.arg1;
            dumpRecord(null, now, pw, r, args.argi1);
        }
        for (SomeArgs args : mPendingOffload) {
            final BroadcastRecord r = (BroadcastRecord) args.arg1;
            dumpRecord("OFFLOAD", now, pw, r, args.argi1);
        }
        pw.decreaseIndent();
        pw.println();
    }

    @NeverCompile
    private void dumpProcessState(@NonNull IndentingPrintWriter pw) {
        final StringBuilder sb = new StringBuilder();
        if (mUidForeground) {
            sb.append("FG");
        }
        if (mProcessFreezable) {
            if (sb.length() > 0) sb.append("|");
            sb.append("FRZ");
        }
        if (mProcessInstrumented) {
            if (sb.length() > 0) sb.append("|");
            sb.append("INSTR");
        }
        if (mProcessPersistent) {
            if (sb.length() > 0) sb.append("|");
            sb.append("PER");
        }
        if (sb.length() > 0) {
            pw.print("state:"); pw.println(sb);
        }
        if (runningOomAdjusted) {
            pw.print("runningOomAdjusted:"); pw.println(runningOomAdjusted);
        }
        if (mActiveReEnqueued) {
            pw.print("activeReEnqueued:"); pw.println(mActiveReEnqueued);
        }
        if (mProcessStartInitiatedTimestampMillis > 0) {
            pw.print("processStartInitiatedTimestamp:"); pw.println(
                    TimeUtils.formatUptime(mProcessStartInitiatedTimestampMillis));
        }
    }

    @NeverCompile
    private void dumpBroadcastCounts(@NonNull IndentingPrintWriter pw) {
        pw.print("e:"); pw.print(mCountEnqueued);
        pw.print(" d:"); pw.print(mCountDeferred);
        pw.print(" f:"); pw.print(mCountForeground);
        pw.print(" fd:"); pw.print(mCountForegroundDeferred);
        pw.print(" o:"); pw.print(mCountOrdered);
        pw.print(" a:"); pw.print(mCountAlarm);
        pw.print(" p:"); pw.print(mCountPrioritized);
        pw.print(" pd:"); pw.print(mCountPrioritizedDeferred);
        pw.print(" int:"); pw.print(mCountInteractive);
        pw.print(" rt:"); pw.print(mCountResultTo);
        pw.print(" ins:"); pw.print(mCountInstrumented);
        pw.print(" m:"); pw.print(mCountManifest);

        pw.print(" csi:"); pw.print(mActiveCountSinceIdle);
        pw.print(" adcsi:"); pw.print(mActiveAssumedDeliveryCountSinceIdle);
        pw.print(" ccu:"); pw.print(mActiveCountConsecutiveUrgent);
        pw.print(" ccn:"); pw.print(mActiveCountConsecutiveNormal);
        pw.println();
    }

    @NeverCompile
    private void dumpOutgoingRecord(@UptimeMillisLong long now,
            @NonNull IndentingPrintWriter pw, @NonNull BroadcastRecord record) {
        pw.print("OUTGOING ");
        TimeUtils.formatDuration(record.enqueueTime, now, pw);
        pw.print(' ');
        pw.println(record.toShortString());
    }

    @NeverCompile
    private void dumpRecord(@Nullable String flavor, @UptimeMillisLong long now,
            @NonNull IndentingPrintWriter pw, @NonNull BroadcastRecord record, int recordIndex) {
        TimeUtils.formatDuration(record.enqueueTime, now, pw);
        pw.print(' ');
        pw.println(record.toShortString());
        pw.print("    ");
        final int deliveryState = record.delivery[recordIndex];
        pw.print(deliveryStateToString(deliveryState));
        if (deliveryState == BroadcastRecord.DELIVERY_SCHEDULED) {
            pw.print(" at ");
            TimeUtils.formatDuration(record.scheduledTime[recordIndex], now, pw);
        }
        if (flavor != null) {
            pw.print(' ');
            pw.print(flavor);
        }
        final Object receiver = record.receivers.get(recordIndex);
        if (receiver instanceof BroadcastFilter) {
            final BroadcastFilter filter = (BroadcastFilter) receiver;
            pw.print(" for registered ");
            pw.print(Integer.toHexString(System.identityHashCode(filter)));
        } else /* if (receiver instanceof ResolveInfo) */ {
            final ResolveInfo info = (ResolveInfo) receiver;
            pw.print(" for manifest ");
            pw.print(info.activityInfo.name);
        }
        pw.println();
        final int blockedUntilBeyondCount = record.blockedUntilBeyondCount[recordIndex];
        if (blockedUntilBeyondCount != -1) {
            pw.print("    blocked until ");
            pw.print(blockedUntilBeyondCount);
            pw.print(", currently at ");
            pw.print(record.beyondCount);
            pw.print(" of ");
            pw.println(record.receivers.size());
        }
    }
}
