Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f9309d86 authored by Evan Severson's avatar Evan Severson Committed by Android (Google) Code Review
Browse files

Merge "Make AppOpsUidStateTracker callbacks more testable"

parents d274a8df 482f199c
Loading
Loading
Loading
Loading
+21 −10
Original line number Diff line number Diff line
@@ -105,6 +105,7 @@ import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.PackageTagsList;
import android.os.Process;
@@ -374,10 +375,17 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch
    public AppOpsUidStateTracker getUidStateTracker() {
        if (mUidStateTracker == null) {
            mUidStateTracker = new AppOpsUidStateTrackerImpl(
                    LocalServices.getService(ActivityManagerInternal.class), mHandler,
                    LocalServices.getService(ActivityManagerInternal.class),
                    mHandler,
                    r -> {
                        synchronized (AppOpsService.this) {
                            r.run();
                        }
                    },
                    Clock.SYSTEM_CLOCK, mConstants);

            mUidStateTracker.addUidStateChangedCallback(mHandler, this::onUidStateChanged);
            mUidStateTracker.addUidStateChangedCallback(new HandlerExecutor(mHandler),
                    this::onUidStateChanged);
        }
        return mUidStateTracker;
    }
@@ -4809,6 +4817,8 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch
        pw.println("    Only output the watcher sections.");
        pw.println("  --history");
        pw.println("    Only output history.");
        pw.println("  --uid-state-change-logs");
        pw.println("    Include logs about uid state changes.");
    }

    private void dumpStatesLocked(@NonNull PrintWriter pw, @Nullable String filterAttributionTag,
@@ -4946,6 +4956,7 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch
        // TODO ntmyren: Remove the dumpHistory and dumpFilter
        boolean dumpHistory = false;
        boolean includeDiscreteOps = false;
        boolean dumpUidStateChangeLogs = false;
        int nDiscreteOps = 10;
        @HistoricalOpsRequestFilter int dumpFilter = 0;
        boolean dumpAll = false;
@@ -5028,6 +5039,8 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch
                } else if (arg.length() > 0 && arg.charAt(0) == '-') {
                    pw.println("Unknown option: " + arg);
                    return;
                } else if ("--uid-state-change-logs".equals(arg)) {
                    dumpUidStateChangeLogs = true;
                } else {
                    pw.println("Unknown command: " + arg);
                    return;
@@ -5363,6 +5376,12 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch
                    pw.println("  AppOps policy not set.");
                }
            }

            if (dumpAll || dumpUidStateChangeLogs) {
                pw.println();
                pw.println("Uid State Changes Event Log:");
                getUidStateTracker().dumpEvents(pw);
            }
        }

        // Must not hold the appops lock
@@ -5375,14 +5394,6 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch
            mHistoricalRegistry.dumpDiscreteData(pw, dumpUid, dumpPackage, dumpAttributionTag,
                    dumpFilter, dumpOp, sdf, date, "  ", nDiscreteOps);
        }

        if (dumpAll) {
            pw.println();
            pw.println("Uid State Changes Event Log:");
            if (mUidStateTracker != null) {
                mUidStateTracker.dumpEvents(pw);
            }
        }
    }

    @Override
+3 −2
Original line number Diff line number Diff line
@@ -30,10 +30,11 @@ import static android.app.AppOpsManager.UID_STATE_FOREGROUND_SERVICE;
import static android.app.AppOpsManager.UID_STATE_PERSISTENT;
import static android.app.AppOpsManager.UID_STATE_TOP;

import android.os.Handler;
import android.annotation.CallbackExecutor;
import android.util.SparseArray;

import java.io.PrintWriter;
import java.util.concurrent.Executor;

interface AppOpsUidStateTracker {

@@ -95,7 +96,7 @@ interface AppOpsUidStateTracker {
    /**
     * Listen to changes in {@link android.app.AppOpsManager.UidState}
     */
    void addUidStateChangedCallback(Handler handler,
    void addUidStateChangedCallback(@CallbackExecutor Executor executor,
            UidStateChangedCallback callback);

    /**
+57 −40
Original line number Diff line number Diff line
@@ -44,17 +44,18 @@ import android.util.SparseIntArray;
import android.util.SparseLongArray;
import android.util.TimeUtils;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.Clock;
import com.android.internal.util.function.pooled.PooledLambda;

import java.io.PrintWriter;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;

class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker {

    private static final String LOG_TAG = AppOpsUidStateTrackerImpl.class.getSimpleName();

    private final Handler mHandler;
    private final DelayableExecutor mExecutor;
    private final Clock mClock;
    private ActivityManagerInternal mActivityManagerInternal;
    private AppOpsService.Constants mConstants;
@@ -68,18 +69,46 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker {
    private SparseLongArray mPendingCommitTime = new SparseLongArray();
    private SparseBooleanArray mPendingGone = new SparseBooleanArray();

    private ArrayMap<UidStateChangedCallback, Handler> mUidStateChangedCallbacks = new ArrayMap<>();
    private ArrayMap<UidStateChangedCallback, Executor>
            mUidStateChangedCallbacks = new ArrayMap<>();

    private final EventLog mEventLog;

    @VisibleForTesting
    interface DelayableExecutor extends Executor {

        void execute(Runnable runnable);

        void executeDelayed(Runnable runnable, long delay);
    }

    AppOpsUidStateTrackerImpl(ActivityManagerInternal activityManagerInternal,
            Handler handler, Executor lockingExecutor, Clock clock,
            AppOpsService.Constants constants) {

        this(activityManagerInternal, new DelayableExecutor() {
            @Override
            public void execute(Runnable runnable) {
                handler.post(() -> lockingExecutor.execute(runnable));
            }

            @Override
            public void executeDelayed(Runnable runnable, long delay) {
                handler.postDelayed(() -> lockingExecutor.execute(runnable), delay);
            }
        }, clock, constants, handler.getLooper().getThread());
    }

    @VisibleForTesting
    AppOpsUidStateTrackerImpl(ActivityManagerInternal activityManagerInternal,
            Handler handler, Clock clock, AppOpsService.Constants constants) {
            DelayableExecutor executor, Clock clock, AppOpsService.Constants constants,
            Thread executorThread) {
        mActivityManagerInternal = activityManagerInternal;
        mHandler = handler;
        mExecutor = executor;
        mClock = clock;
        mConstants = constants;

        mEventLog = new EventLog(handler);
        mEventLog = new EventLog(executor, executorThread);
    }

    @Override
@@ -157,11 +186,12 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker {
    }

    @Override
    public void addUidStateChangedCallback(Handler handler, UidStateChangedCallback callback) {
    public void addUidStateChangedCallback(Executor executor, UidStateChangedCallback callback) {
        if (mUidStateChangedCallbacks.containsKey(callback)) {
            throw new IllegalStateException("Callback is already registered.");
        }
        mUidStateChangedCallbacks.put(callback, handler);

        mUidStateChangedCallbacks.put(callback, executor);
    }

    @Override
@@ -232,7 +262,7 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker {
                final long commitTime = mClock.elapsedRealtime() + settleTime;
                mPendingCommitTime.put(uid, commitTime);

                mHandler.sendMessageDelayed(PooledLambda.obtainMessage(
                mExecutor.executeDelayed(PooledLambda.obtainRunnable(
                                AppOpsUidStateTrackerImpl::updateUidPendingStateIfNeeded, this,
                                uid), settleTime + 1);
            }
@@ -323,10 +353,11 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker {

            for (int i = 0; i < mUidStateChangedCallbacks.size(); i++) {
                UidStateChangedCallback cb = mUidStateChangedCallbacks.keyAt(i);
                Handler h = mUidStateChangedCallbacks.valueAt(i);
                Executor executor = mUidStateChangedCallbacks.valueAt(i);

                h.sendMessage(PooledLambda.obtainMessage(UidStateChangedCallback::onUidStateChanged,
                        cb, uid, pendingUidState, foregroundChange));
                executor.execute(PooledLambda.obtainRunnable(
                        UidStateChangedCallback::onUidStateChanged, cb, uid, pendingUidState,
                        foregroundChange));
            }
        }

@@ -366,7 +397,8 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker {
        // Memory usage: 24 * size bytes
        private static final int EVAL_FOREGROUND_MODE_MAX_SIZE = 200;

        private final Handler mHandler;
        private final DelayableExecutor mExecutor;
        private final Thread mExecutorThread;

        private int[][] mUpdateUidProcStateLog = new int[UPDATE_UID_PROC_STATE_LOG_MAX_SIZE][3];
        private long[] mUpdateUidProcStateLogTimestamps =
@@ -384,15 +416,16 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker {
        private int mEvalForegroundModeLogSize = 0;
        private int mEvalForegroundModeLogHead = 0;

        EventLog(Handler handler) {
            mHandler = handler;
        EventLog(DelayableExecutor executor, Thread executorThread) {
            mExecutor = executor;
            mExecutorThread = executorThread;
        }

        void logUpdateUidProcState(int uid, int procState, int capability) {
            if (UPDATE_UID_PROC_STATE_LOG_MAX_SIZE == 0) {
                return;
            }
            mHandler.sendMessage(PooledLambda.obtainMessage(EventLog::logUpdateUidProcStateAsync,
            mExecutor.execute(PooledLambda.obtainRunnable(EventLog::logUpdateUidProcStateAsync,
                    this, System.currentTimeMillis(), uid, procState, capability));
        }

@@ -416,7 +449,7 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker {
            if (COMMIT_UID_STATE_LOG_MAX_SIZE == 0) {
                return;
            }
            mHandler.sendMessage(PooledLambda.obtainMessage(EventLog::logCommitUidStateAsync,
            mExecutor.execute(PooledLambda.obtainRunnable(EventLog::logCommitUidStateAsync,
                    this, System.currentTimeMillis(), uid, uidState, capability, visible));
        }

@@ -442,7 +475,7 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker {
            if (EVAL_FOREGROUND_MODE_MAX_SIZE == 0) {
                return;
            }
            mHandler.sendMessage(PooledLambda.obtainMessage(EventLog::logEvalForegroundModeAsync,
            mExecutor.execute(PooledLambda.obtainRunnable(EventLog::logEvalForegroundModeAsync,
                    this, System.currentTimeMillis(), uid, uidState, capability, code, result));
        }

@@ -466,22 +499,6 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker {
        }

        void dumpEvents(PrintWriter pw) {
            if (Thread.currentThread() != mHandler.getLooper().getThread()) {
                // All operations are done on the handler's thread
                CountDownLatch latch = new CountDownLatch(1);
                mHandler.post(() -> {
                    dumpEvents(pw);
                    latch.countDown();
                });

                try {
                    latch.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                return;
            }

            int updateIdx = 0;
            int commitIdx = 0;
            int evalIdx = 0;
@@ -536,13 +553,13 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker {
            pw.print(" UPDATE_UID_PROC_STATE");

            pw.print(" uid=");
            pw.print(uid);
            pw.print(String.format("%-8d", uid));

            pw.print(" procState=");
            pw.print(String.format("%-30s", ActivityManager.procStateToString(procState)));

            pw.print(" capability=");
            pw.print(ActivityManager.getCapabilitiesSummary(capability));
            pw.print(ActivityManager.getCapabilitiesSummary(capability) + " ");

            pw.println();
        }
@@ -559,13 +576,13 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker {
            pw.print(" COMMIT_UID_STATE     ");

            pw.print(" uid=");
            pw.print(uid);
            pw.print(String.format("%-8d", uid));

            pw.print(" uidState=");
            pw.print(String.format("%-30s", AppOpsManager.uidStateToString(uidState)));

            pw.print(" capability=");
            pw.print(ActivityManager.getCapabilitiesSummary(capability));
            pw.print(ActivityManager.getCapabilitiesSummary(capability) + " ");

            pw.print(" visibleAppWidget=");
            pw.print(visibleAppWidget);
@@ -586,13 +603,13 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker {
            pw.print(" EVAL_FOREGROUND_MODE ");

            pw.print(" uid=");
            pw.print(uid);
            pw.print(String.format("%-8d", uid));

            pw.print(" uidState=");
            pw.print(String.format("%-30s", AppOpsManager.uidStateToString(uidState)));

            pw.print(" capability=");
            pw.print(ActivityManager.getCapabilitiesSummary(capability));
            pw.print(ActivityManager.getCapabilitiesSummary(capability) + " ");

            pw.print(" code=");
            pw.print(String.format("%-20s", AppOpsManager.opToName(code)));
+90 −83
Original line number Diff line number Diff line
@@ -34,12 +34,9 @@ import static android.app.AppOpsManager.UID_STATE_TOP;
import static com.android.server.appop.AppOpsUidStateTracker.processStateToUidState;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
@@ -49,25 +46,22 @@ import static org.mockito.Mockito.verify;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
import android.os.Handler;
import android.os.Message;
import android.util.SparseArray;

import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import com.android.internal.os.Clock;
import com.android.server.appop.AppOpsUidStateTracker.UidStateChangedCallback;
import com.android.server.appop.AppOpsUidStateTrackerImpl.DelayableExecutor;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.quality.Strictness;

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.PriorityQueue;

public class AppOpsUidStateTrackerTest {

@@ -80,13 +74,12 @@ public class AppOpsUidStateTrackerTest {
    @Mock
    ActivityManagerInternal mAmi;

    @Mock
    Handler mHandler;

    @Mock
    AppOpsService.Constants mConstants;

    AppOpsUidStateTrackerTestClock mClock = new AppOpsUidStateTrackerTestClock();
    AppOpsUidStateTrackerTestExecutor mExecutor = new AppOpsUidStateTrackerTestExecutor();

    AppOpsUidStateTrackerTestClock mClock = new AppOpsUidStateTrackerTestClock(mExecutor);

    AppOpsUidStateTracker mIntf;

@@ -101,7 +94,8 @@ public class AppOpsUidStateTrackerTest {
        mConstants.TOP_STATE_SETTLE_TIME = 10 * 1000L;
        mConstants.FG_SERVICE_STATE_SETTLE_TIME = 5 * 1000L;
        mConstants.BG_STATE_SETTLE_TIME = 1 * 1000L;
        mIntf = new AppOpsUidStateTrackerImpl(mAmi, mHandler, mClock, mConstants);
        mIntf = new AppOpsUidStateTrackerImpl(mAmi, mExecutor, mClock, mConstants,
                Thread.currentThread());
    }

    @After
@@ -263,18 +257,10 @@ public class AppOpsUidStateTrackerTest {
        // Still in foreground due to settle time
        assertForeground(UID);

        AtomicReference<Message> messageAtomicReference = new AtomicReference<>();
        AtomicLong delayAtomicReference = new AtomicLong();

        getPostDelayedMessageArguments(messageAtomicReference, delayAtomicReference);
        Message message = messageAtomicReference.get();
        long delay = delayAtomicReference.get();

        assertNotNull(message);
        assertEquals(mConstants.TOP_STATE_SETTLE_TIME + 1, delay);
        mClock.advanceTime(mConstants.TOP_STATE_SETTLE_TIME - 1);
        assertForeground(UID);

        mClock.advanceTime(mConstants.TOP_STATE_SETTLE_TIME + 1);
        message.getCallback().run();
        mClock.advanceTime(1);
        assertBackground(UID);
    }

@@ -291,18 +277,10 @@ public class AppOpsUidStateTrackerTest {
        // Still in foreground due to settle time
        assertForeground(UID);

        AtomicReference<Message> messageAtomicReference = new AtomicReference<>();
        AtomicLong delayAtomicReference = new AtomicLong();

        getPostDelayedMessageArguments(messageAtomicReference, delayAtomicReference);
        Message message = messageAtomicReference.get();
        long delay = delayAtomicReference.get();

        assertNotNull(message);
        assertEquals(mConstants.FG_SERVICE_STATE_SETTLE_TIME + 1, delay);
        mClock.advanceTime(mConstants.FG_SERVICE_STATE_SETTLE_TIME - 1);
        assertForeground(UID);

        mClock.advanceTime(mConstants.FG_SERVICE_STATE_SETTLE_TIME + 1);
        message.getCallback().run();
        mClock.advanceTime(1);
        assertBackground(UID);
    }

@@ -319,14 +297,8 @@ public class AppOpsUidStateTrackerTest {
        // Still in foreground due to settle time
        assertForeground(UID);

        AtomicReference<Message> messageAtomicReference = new AtomicReference<>();

        getPostDelayedMessageArguments(messageAtomicReference, null);
        Message message = messageAtomicReference.get();

        // 1 ms short of settle time
        mClock.advanceTime(mConstants.FG_SERVICE_STATE_SETTLE_TIME - 1);
        message.getCallback().run();
        assertForeground(UID);
    }

@@ -471,8 +443,6 @@ public class AppOpsUidStateTrackerTest {
                .topState()
                .update();

        getLatestPostMessageArgument().getCallback().run();

        verify(cb).onUidStateChanged(eq(UID), eq(UID_STATE_TOP), eq(true));
    }

@@ -484,8 +454,6 @@ public class AppOpsUidStateTrackerTest {
                .foregroundServiceState()
                .update();

        getLatestPostMessageArgument().getCallback().run();

        verify(cb).onUidStateChanged(eq(UID), eq(UID_STATE_FOREGROUND_SERVICE), eq(true));
    }

@@ -497,8 +465,6 @@ public class AppOpsUidStateTrackerTest {
                .foregroundState()
                .update();

        getLatestPostMessageArgument().getCallback().run();

        verify(cb).onUidStateChanged(eq(UID), eq(UID_STATE_FOREGROUND), eq(true));
    }

@@ -510,8 +476,6 @@ public class AppOpsUidStateTrackerTest {
                .backgroundState()
                .update();

        getLatestPostMessageArgument().getCallback().run();

        verify(cb).onUidStateChanged(eq(UID), eq(UID_STATE_BACKGROUND), eq(false));
    }

@@ -679,7 +643,6 @@ public class AppOpsUidStateTrackerTest {
                .nonExistentState()
                .update();

        verify(mHandler, never()).post(any());
        verify(cb, never()).onUidStateChanged(anyInt(), anyInt(), anyBoolean());
    }

@@ -695,7 +658,6 @@ public class AppOpsUidStateTrackerTest {
                .nonExistentState()
                .update();

        getLatestPostMessageArgument().getCallback().run();
        verify(cb, atLeastOnce()).onUidStateChanged(eq(UID), eq(UID_STATE_CACHED), eq(false));
    }

@@ -711,7 +673,6 @@ public class AppOpsUidStateTrackerTest {
                .nonExistentState()
                .update();

        getLatestPostMessageArgument().getCallback().run();
        verify(cb, atLeastOnce()).onUidStateChanged(eq(UID), eq(UID_STATE_CACHED), eq(true));
    }

@@ -727,7 +688,6 @@ public class AppOpsUidStateTrackerTest {
                .nonExistentState()
                .update();

        getLatestPostMessageArgument().getCallback().run();
        verify(cb, atLeastOnce()).onUidStateChanged(eq(UID), eq(UID_STATE_CACHED), eq(true));
    }

@@ -743,10 +703,32 @@ public class AppOpsUidStateTrackerTest {
                .nonExistentState()
                .update();

        getLatestPostMessageArgument().getCallback().run();
        verify(cb, atLeastOnce()).onUidStateChanged(eq(UID), eq(UID_STATE_CACHED), eq(true));
    }

    @Test
    public void testUidStateChangedBackgroundThenForegroundImmediately() {
        procStateBuilder(UID)
            .topState()
            .update();

        UidStateChangedCallback cb = addUidStateChangeCallback();

        procStateBuilder(UID)
            .backgroundState()
            .update();

        mClock.advanceTime(mConstants.TOP_STATE_SETTLE_TIME - 1);

        procStateBuilder(UID)
            .topState()
            .update();

        mClock.advanceTime(1);

        verify(cb, never()).onUidStateChanged(anyInt(), anyInt(), anyBoolean());
    }

    public void testUidStateChangedCallback(int initialState, int finalState) {
        int initialUidState = processStateToUidState(initialState);
        int finalUidState = processStateToUidState(finalState);
@@ -767,13 +749,9 @@ public class AppOpsUidStateTrackerTest {
                .update();

        if (finalUidStateIsBackgroundAndLessImportant) {
            AtomicReference<Message> delayedMessage = new AtomicReference<>();
            getPostDelayedMessageArguments(delayedMessage, new AtomicLong());
            mClock.advanceTime(mConstants.TOP_STATE_SETTLE_TIME + 1);
            delayedMessage.get().getCallback().run();
        }

        getLatestPostMessageArgument().getCallback().run();
        verify(cb, atLeastOnce())
                .onUidStateChanged(eq(UID), eq(finalUidState), eq(foregroundChange));
    }
@@ -781,7 +759,7 @@ public class AppOpsUidStateTrackerTest {
    private UidStateChangedCallback addUidStateChangeCallback() {
        UidStateChangedCallback cb =
                Mockito.mock(UidStateChangedCallback.class);
        mIntf.addUidStateChangedCallback(mHandler, cb);
        mIntf.addUidStateChangedCallback(r -> r.run(), cb);
        return cb;
    }

@@ -795,30 +773,6 @@ public class AppOpsUidStateTrackerTest {
        assertEquals(MODE_IGNORED, mIntf.evalMode(uid, OP_NO_CAPABILITIES, MODE_FOREGROUND));
    }

    private void getPostDelayedMessageArguments(AtomicReference<Message> message,
            AtomicLong delay) {

        ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
        ArgumentCaptor<Long> delayCaptor = ArgumentCaptor.forClass(Long.class);

        verify(mHandler).sendMessageDelayed(messageCaptor.capture(), delayCaptor.capture());

        if (message != null) {
            message.set(messageCaptor.getValue());
        }
        if (delay != null) {
            delay.set(delayCaptor.getValue());
        }
    }

    private Message getLatestPostMessageArgument() {
        ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);

        verify(mHandler, atLeast(1)).sendMessage(messageCaptor.capture());

        return messageCaptor.getValue();
    }

    private UidProcStateUpdateBuilder procStateBuilder(int uid) {
        return new UidProcStateUpdateBuilder(mIntf, uid);
    }
@@ -896,8 +850,14 @@ public class AppOpsUidStateTrackerTest {

    private static class AppOpsUidStateTrackerTestClock extends Clock {

        private AppOpsUidStateTrackerTestExecutor mExecutor;
        long mElapsedRealTime = 0x5f3759df;

        AppOpsUidStateTrackerTestClock(AppOpsUidStateTrackerTestExecutor executor) {
            mExecutor = executor;
            executor.setUptime(mElapsedRealTime);
        }

        @Override
        public long elapsedRealtime() {
            return mElapsedRealTime;
@@ -905,6 +865,53 @@ public class AppOpsUidStateTrackerTest {

        void advanceTime(long time) {
            mElapsedRealTime += time;
            mExecutor.setUptime(mElapsedRealTime); // assume uptime == elapsedtime
        }
    }

    private static class AppOpsUidStateTrackerTestExecutor implements DelayableExecutor {

        private static class QueueElement implements Comparable<QueueElement> {

            private long mExecutionTime;
            private Runnable mRunnable;

            private QueueElement(long executionTime, Runnable runnable) {
                mExecutionTime = executionTime;
                mRunnable = runnable;
            }

            @Override
            public int compareTo(QueueElement queueElement) {
                return Long.compare(mExecutionTime, queueElement.mExecutionTime);
            }
        }

        private long mUptime = 0;

        private PriorityQueue<QueueElement> mDelayedMessages = new PriorityQueue();

        @Override
        public void execute(Runnable runnable) {
            runnable.run();
        }

        @Override
        public void executeDelayed(Runnable runnable, long delay) {
            if (delay <= 0) {
                execute(runnable);
            }

            mDelayedMessages.add(new QueueElement(mUptime + delay, runnable));
        }

        private void setUptime(long uptime) {
            while (!mDelayedMessages.isEmpty()
                    && mDelayedMessages.peek().mExecutionTime <= uptime) {
                mDelayedMessages.poll().mRunnable.run();
            }

            mUptime = uptime;
        }
    }
}