Fix nondeterminism in NotificationLockscreenUserManagerTest due to use of a real looper

* Adds a Rule that exposes handling of Log.wtf in a way that is safe for tests
* Adds a mockExecutorHandler method which allows using FakeExecutor when testing classes that take Handler

Fixes: 313999630
Test: atest NotificationLockscreenUserManagerTest
Flag: NA
Change-Id: I3eafe50af9fe26286c955c9aa5aa408142cf1dba
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index ae32142..8bc5e70 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -26,14 +26,10 @@
 import static android.os.UserHandle.USER_ALL;
 import static android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS;
 import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS;
-
+import static com.android.systemui.util.concurrency.MockExecutorHandlerKt.mockExecutorHandler;
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertTrue;
-
-import static org.junit.Assume.assumeFalse;
-import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -57,8 +53,6 @@
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
@@ -75,6 +69,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlagsClassic;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.log.LogWtfHandlerRule;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.settings.UserTracker;
@@ -85,11 +80,14 @@
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.settings.FakeSettings;
-
+import com.android.systemui.util.time.FakeSystemClock;
 import com.google.android.collect.Lists;
 
+import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -144,7 +142,11 @@
     private NotificationEntry mSecondaryUserNotif;
     private NotificationEntry mWorkProfileNotif;
     private final FakeFeatureFlagsClassic mFakeFeatureFlags = new FakeFeatureFlagsClassic();
-    private Executor mBackgroundExecutor = Runnable::run; // Direct executor
+    private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
+    private final FakeExecutor mBackgroundExecutor = new FakeExecutor(mFakeSystemClock);
+    private final Executor mMainExecutor = Runnable::run; // Direct executor
+
+    @Rule public final LogWtfHandlerRule wtfHandlerRule = new LogWtfHandlerRule();
 
     @Before
     public void setUp() {
@@ -175,7 +177,7 @@
         when(mUserManager.getProfilesIncludingCommunal(mSecondaryUser.id)).thenReturn(
                 Lists.newArrayList(mSecondaryUser, mCommunalUser));
         mDependency.injectTestDependency(Dependency.MAIN_HANDLER,
-                Handler.createAsync(Looper.myLooper()));
+                mockExecutorHandler(mMainExecutor));
 
         Notification notifWithPrivateVisibility = new Notification();
         notifWithPrivateVisibility.visibility = VISIBILITY_PRIVATE;
@@ -209,6 +211,14 @@
 
         mLockscreenUserManager = new TestNotificationLockscreenUserManager(mContext);
         mLockscreenUserManager.setUpWithPresenter(mPresenter);
+
+        mBackgroundExecutor.runAllReady();
+    }
+
+    @After
+    public void tearDown() {
+        // Validate that all tests processed all background posted code
+        assertEquals(0, mBackgroundExecutor.numPending());
     }
 
     private void changeSetting(String setting) {
@@ -443,28 +453,28 @@
 
         // first call explicitly sets user 0 to not public; notifies
         mLockscreenUserManager.updatePublicMode();
-        TestableLooper.get(this).processAllMessages();
+        mBackgroundExecutor.runAllReady();
         assertFalse(mLockscreenUserManager.isLockscreenPublicMode(0));
         verify(listener).onNotificationStateChanged();
         clearInvocations(listener);
 
         // calling again has no changes; does not notify
         mLockscreenUserManager.updatePublicMode();
-        TestableLooper.get(this).processAllMessages();
+        mBackgroundExecutor.runAllReady();
         assertFalse(mLockscreenUserManager.isLockscreenPublicMode(0));
         verify(listener, never()).onNotificationStateChanged();
 
         // Calling again with keyguard now showing makes user 0 public; notifies
         when(mKeyguardStateController.isShowing()).thenReturn(true);
         mLockscreenUserManager.updatePublicMode();
-        TestableLooper.get(this).processAllMessages();
+        mBackgroundExecutor.runAllReady();
         assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0));
         verify(listener).onNotificationStateChanged();
         clearInvocations(listener);
 
         // calling again has no changes; does not notify
         mLockscreenUserManager.updatePublicMode();
-        TestableLooper.get(this).processAllMessages();
+        mBackgroundExecutor.runAllReady();
         assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0));
         verify(listener, never()).onNotificationStateChanged();
     }
@@ -742,6 +752,9 @@
         intent.putExtra(Intent.EXTRA_USER_HANDLE, newUserId);
         broadcastReceiver.onReceive(mContext, intent);
 
+        // One background task to run which will setup the new user
+        assertEquals(1, mBackgroundExecutor.runAllReady());
+
         verify(mDevicePolicyManager, atMost(1)).getKeyguardDisabledFeatures(any(), eq(newUserId));
 
         assertTrue(mLockscreenUserManager.userAllowsNotificationsInPublic(newUserId));
@@ -821,10 +834,8 @@
                     (() -> mOverviewProxyService),
                     NotificationLockscreenUserManagerTest.this.mKeyguardManager,
                     mStatusBarStateController,
-                    Handler.createAsync(TestableLooper.get(
-                            NotificationLockscreenUserManagerTest.this).getLooper()),
-                    Handler.createAsync(TestableLooper.get(
-                            NotificationLockscreenUserManagerTest.this).getLooper()),
+                    mockExecutorHandler(mMainExecutor),
+                    mockExecutorHandler(mBackgroundExecutor),
                     mBackgroundExecutor,
                     mDeviceProvisionedController,
                     mKeyguardStateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutorTest.java
index 87206c5..31bad2c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutorTest.java
@@ -132,6 +132,40 @@
      * Test FakeExecutor that is told to delay execution on items.
      */
     @Test
+    public void testAtTime() {
+        FakeSystemClock clock = new FakeSystemClock();
+        FakeExecutor fakeExecutor = new FakeExecutor(clock);
+        RunnableImpl runnable = new RunnableImpl();
+
+        // Add three delayed runnables.
+        fakeExecutor.executeAtTime(runnable, 10001);
+        fakeExecutor.executeAtTime(runnable, 10050);
+        fakeExecutor.executeAtTime(runnable, 10100);
+        assertEquals(0, runnable.mRunCount);
+        assertEquals(10000, clock.uptimeMillis());
+        assertEquals(3, fakeExecutor.numPending());
+        // Delayed runnables should not advance the clock and therefore should not run.
+        assertFalse(fakeExecutor.runNextReady());
+        assertEquals(0, fakeExecutor.runAllReady());
+        assertEquals(3, fakeExecutor.numPending());
+
+        // Advance the clock to the next runnable. One runnable should execute.
+        assertEquals(1, fakeExecutor.advanceClockToNext());
+        assertEquals(1, fakeExecutor.runAllReady());
+        assertEquals(2, fakeExecutor.numPending());
+        assertEquals(1, runnable.mRunCount);
+        // Advance the clock to the last runnable.
+        assertEquals(99, fakeExecutor.advanceClockToLast());
+        assertEquals(2, fakeExecutor.runAllReady());
+        // Now all remaining runnables should execute.
+        assertEquals(0, fakeExecutor.numPending());
+        assertEquals(3, runnable.mRunCount);
+    }
+
+    /**
+     * Test FakeExecutor that is told to delay execution on items.
+     */
+    @Test
     public void testDelayed_AdvanceAndRun() {
         FakeSystemClock clock = new FakeSystemClock();
         FakeExecutor fakeExecutor = new FakeExecutor(clock);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/MockExecutorHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/MockExecutorHandlerTest.kt
new file mode 100644
index 0000000..d1d2598
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/MockExecutorHandlerTest.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2019 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.systemui.util.concurrency
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class MockExecutorHandlerTest : SysuiTestCase() {
+    /** Test FakeExecutor that receives non-delayed items to execute. */
+    @Test
+    fun testNoDelay() {
+        val clock = FakeSystemClock()
+        val fakeExecutor = FakeExecutor(clock)
+        val handler = mockExecutorHandler(fakeExecutor)
+        val runnable = RunnableImpl()
+        assertEquals(10000, clock.uptimeMillis())
+        assertEquals(0, runnable.mRunCount)
+
+        // Execute two runnables. They should not run and should be left pending.
+        handler.post(runnable)
+        assertEquals(0, runnable.mRunCount)
+        assertEquals(10000, clock.uptimeMillis())
+        assertEquals(1, fakeExecutor.numPending())
+        handler.post(runnable)
+        assertEquals(0, runnable.mRunCount)
+        assertEquals(10000, clock.uptimeMillis())
+        assertEquals(2, fakeExecutor.numPending())
+
+        // Run one pending runnable.
+        assertTrue(fakeExecutor.runNextReady())
+        assertEquals(1, runnable.mRunCount)
+        assertEquals(10000, clock.uptimeMillis())
+        assertEquals(1, fakeExecutor.numPending())
+        // Run a second pending runnable.
+        assertTrue(fakeExecutor.runNextReady())
+        assertEquals(2, runnable.mRunCount)
+        assertEquals(10000, clock.uptimeMillis())
+        assertEquals(0, fakeExecutor.numPending())
+
+        // No more runnables to run.
+        assertFalse(fakeExecutor.runNextReady())
+
+        // Add two more runnables.
+        handler.post(runnable)
+        handler.post(runnable)
+        assertEquals(2, runnable.mRunCount)
+        assertEquals(10000, clock.uptimeMillis())
+        assertEquals(2, fakeExecutor.numPending())
+        // Execute all pending runnables in batch.
+        assertEquals(2, fakeExecutor.runAllReady())
+        assertEquals(4, runnable.mRunCount)
+        assertEquals(10000, clock.uptimeMillis())
+        assertEquals(0, fakeExecutor.runAllReady())
+    }
+
+    /** Test FakeExecutor that is told to delay execution on items. */
+    @Test
+    fun testDelayed() {
+        val clock = FakeSystemClock()
+        val fakeExecutor = FakeExecutor(clock)
+        val handler = mockExecutorHandler(fakeExecutor)
+        val runnable = RunnableImpl()
+
+        // Add three delayed runnables.
+        handler.postDelayed(runnable, 1)
+        handler.postDelayed(runnable, 50)
+        handler.postDelayed(runnable, 100)
+        assertEquals(0, runnable.mRunCount)
+        assertEquals(10000, clock.uptimeMillis())
+        assertEquals(3, fakeExecutor.numPending())
+        // Delayed runnables should not advance the clock and therefore should not run.
+        assertFalse(fakeExecutor.runNextReady())
+        assertEquals(0, fakeExecutor.runAllReady())
+        assertEquals(3, fakeExecutor.numPending())
+
+        // Advance the clock to the next runnable. One runnable should execute.
+        assertEquals(1, fakeExecutor.advanceClockToNext())
+        assertEquals(1, fakeExecutor.runAllReady())
+        assertEquals(2, fakeExecutor.numPending())
+        assertEquals(1, runnable.mRunCount)
+        // Advance the clock to the last runnable.
+        assertEquals(99, fakeExecutor.advanceClockToLast())
+        assertEquals(2, fakeExecutor.runAllReady())
+        // Now all remaining runnables should execute.
+        assertEquals(0, fakeExecutor.numPending())
+        assertEquals(3, runnable.mRunCount)
+    }
+
+    /** Test FakeExecutor that is told to delay execution on items. */
+    @Test
+    fun testAtTime() {
+        val clock = FakeSystemClock()
+        val fakeExecutor = FakeExecutor(clock)
+        val handler = mockExecutorHandler(fakeExecutor)
+        val runnable = RunnableImpl()
+
+        // Add three delayed runnables.
+        handler.postAtTime(runnable, 10001)
+        handler.postAtTime(runnable, 10050)
+        handler.postAtTime(runnable, 10100)
+        assertEquals(0, runnable.mRunCount)
+        assertEquals(10000, clock.uptimeMillis())
+        assertEquals(3, fakeExecutor.numPending())
+        // Delayed runnables should not advance the clock and therefore should not run.
+        assertFalse(fakeExecutor.runNextReady())
+        assertEquals(0, fakeExecutor.runAllReady())
+        assertEquals(3, fakeExecutor.numPending())
+
+        // Advance the clock to the next runnable. One runnable should execute.
+        assertEquals(1, fakeExecutor.advanceClockToNext())
+        assertEquals(1, fakeExecutor.runAllReady())
+        assertEquals(2, fakeExecutor.numPending())
+        assertEquals(1, runnable.mRunCount)
+        // Advance the clock to the last runnable.
+        assertEquals(99, fakeExecutor.advanceClockToLast())
+        assertEquals(2, fakeExecutor.runAllReady())
+        // Now all remaining runnables should execute.
+        assertEquals(0, fakeExecutor.numPending())
+        assertEquals(3, runnable.mRunCount)
+    }
+
+    /**
+     * Verifies that `Handler.removeMessages`, which doesn't make sense with executor backing,
+     * causes an error in the test (rather than failing silently like most mocks).
+     */
+    @Test(expected = RuntimeException::class)
+    fun testRemoveMessages_fails() {
+        val clock = FakeSystemClock()
+        val fakeExecutor = FakeExecutor(clock)
+        val handler = mockExecutorHandler(fakeExecutor)
+
+        handler.removeMessages(1)
+    }
+
+    private class RunnableImpl : Runnable {
+        var mRunCount = 0
+        override fun run() {
+            mRunCount++
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt
new file mode 100644
index 0000000..e639326
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2023 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.systemui.log
+
+import android.util.Log
+import android.util.Log.TerribleFailureHandler
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+class LogWtfHandlerRule : TestRule {
+
+    private var started = false
+    private var handler = ThrowAndFailAtEnd
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                started = true
+                val originalWtfHandler = Log.setWtfHandler(handler)
+                var failure: Throwable? = null
+                try {
+                    base.evaluate()
+                } catch (ex: Throwable) {
+                    failure = ex.runAndAddSuppressed { handler.onTestFailure(ex) }
+                } finally {
+                    failure = failure.runAndAddSuppressed { handler.onTestFinished() }
+                    Log.setWtfHandler(originalWtfHandler)
+                }
+                if (failure != null) {
+                    throw failure
+                }
+            }
+        }
+    }
+
+    fun Throwable?.runAndAddSuppressed(block: () -> Unit): Throwable? {
+        try {
+            block()
+        } catch (t: Throwable) {
+            if (this == null) {
+                return t
+            }
+            addSuppressed(t)
+        }
+        return this
+    }
+
+    fun setWtfHandler(handler: TerribleFailureTestHandler) {
+        check(!started) { "Should only be called before the test starts" }
+        this.handler = handler
+    }
+
+    fun interface TerribleFailureTestHandler : TerribleFailureHandler {
+        fun onTestFailure(failure: Throwable) {}
+        fun onTestFinished() {}
+    }
+
+    companion object Handlers {
+        val ThrowAndFailAtEnd
+            get() =
+                object : TerribleFailureTestHandler {
+                    val failures = mutableListOf<Log.TerribleFailure>()
+
+                    override fun onTerribleFailure(
+                        tag: String,
+                        what: Log.TerribleFailure,
+                        system: Boolean
+                    ) {
+                        failures.add(what)
+                        throw what
+                    }
+
+                    override fun onTestFailure(failure: Throwable) {
+                        super.onTestFailure(failure)
+                    }
+
+                    override fun onTestFinished() {
+                        if (failures.isNotEmpty()) {
+                            throw AssertionError("Unexpected Log.wtf calls: $failures", failures[0])
+                        }
+                    }
+                }
+
+        val JustThrow = TerribleFailureTestHandler { _, what, _ -> throw what }
+
+        val JustFailAtEnd
+            get() =
+                object : TerribleFailureTestHandler {
+                    val failures = mutableListOf<Log.TerribleFailure>()
+
+                    override fun onTerribleFailure(
+                        tag: String,
+                        what: Log.TerribleFailure,
+                        system: Boolean
+                    ) {
+                        failures.add(what)
+                    }
+
+                    override fun onTestFinished() {
+                        if (failures.isNotEmpty()) {
+                            throw AssertionError("Unexpected Log.wtf calls: $failures", failures[0])
+                        }
+                    }
+                }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/MockExecutorHandler.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/MockExecutorHandler.kt
new file mode 100644
index 0000000..184d4b5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/MockExecutorHandler.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2023 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.systemui.util.concurrency
+
+import android.os.Handler
+import java.util.concurrent.Executor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito
+import org.mockito.Mockito.doAnswer
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.stubbing.Answer
+
+/**
+ * Wrap an [Executor] in a mock [Handler] that execute when [Handler.post] is called, and throws an
+ * exception otherwise. This is useful when a class requires a Handler only because Handlers are
+ * used by ContentObserver, and no other methods are used.
+ */
+fun mockExecutorHandler(executor: Executor): Handler {
+    val handlerMock = Mockito.mock(Handler::class.java, RuntimeExceptionAnswer())
+    doAnswer { invocation: InvocationOnMock ->
+            executor.execute(invocation.getArgument(0))
+            true
+        }
+        .`when`(handlerMock)
+        .post(any())
+    if (executor is DelayableExecutor) {
+        doAnswer { invocation: InvocationOnMock ->
+                val runnable = invocation.getArgument<Runnable>(0)
+                val uptimeMillis = invocation.getArgument<Long>(1)
+                executor.executeAtTime(runnable, uptimeMillis)
+                true
+            }
+            .`when`(handlerMock)
+            .postAtTime(any(), anyLong())
+        doAnswer { invocation: InvocationOnMock ->
+                val runnable = invocation.getArgument<Runnable>(0)
+                val delayInMillis = invocation.getArgument<Long>(1)
+                executor.executeDelayed(runnable, delayInMillis)
+                true
+            }
+            .`when`(handlerMock)
+            .postDelayed(any(), anyLong())
+    }
+    return handlerMock
+}
+
+private class RuntimeExceptionAnswer : Answer<Any> {
+    override fun answer(invocation: InvocationOnMock): Any {
+        throw RuntimeException(invocation.method.name + " is not stubbed")
+    }
+}