Merge "Split up KeyguardBottomArea" into udc-qpr-dev
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 9b19937..7d9c0a3 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -244,13 +244,7 @@
 
     private DreamOverlayConnectionHandler mOverlayConnection;
 
-    private final IDreamOverlayCallback mOverlayCallback = new IDreamOverlayCallback.Stub() {
-        @Override
-        public void onExitRequested() {
-            // Simply finish dream when exit is requested.
-            mHandler.post(() -> finish());
-        }
-    };
+    private IDreamOverlayCallback mOverlayCallback;
 
 
     public DreamService() {
@@ -877,6 +871,13 @@
         mDreamComponent = new ComponentName(this, getClass());
         mShouldShowComplications = fetchShouldShowComplications(this /*context*/,
                 fetchServiceInfo(this /*context*/, mDreamComponent));
+        mOverlayCallback = new IDreamOverlayCallback.Stub() {
+            @Override
+            public void onExitRequested() {
+                // Simply finish dream when exit is requested.
+                mHandler.post(() -> finish());
+            }
+        };
 
         super.onCreate();
     }
@@ -1083,7 +1084,7 @@
 
         // Just in case destroy came in before detach, let's take care of that now
         detach();
-
+        mOverlayCallback = null;
         super.onDestroy();
     }
 
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index d80819f..0e7c6d1 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -181,7 +181,7 @@
     private static final int TOUCH_SLOP = 8;
 
     /** Distance a stylus touch can wander before we think the user is handwriting in dips. */
-    private static final int HANDWRITING_SLOP = 4;
+    private static final int HANDWRITING_SLOP = 2;
 
     /**
      * Defines the minimum size of the touch target for a scrollbar in dips
diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
index 34eac35..8028b14 100644
--- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
+++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
@@ -75,7 +75,7 @@
     private static final int HW_BOUNDS_OFFSETS_TOP_PX = 20;
     private static final int HW_BOUNDS_OFFSETS_RIGHT_PX = 30;
     private static final int HW_BOUNDS_OFFSETS_BOTTOM_PX = 40;
-    private int mHandwritingSlop = 4;
+    private int mHandwritingSlop = 2;
 
     private static final Rect sHwArea1;
     private static final Rect sHwArea2;
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index e2599a3..17037b0 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -307,6 +307,8 @@
         "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt",
         "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt",
         "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt",
+        "tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt",
+        "tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt",
     ],
     path: "tests/src",
 }
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index bb8002a..2711dad 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -35,6 +35,9 @@
         },
         {
             "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+        },
+        {
+            "exclude-annotation": "androidx.test.filters.FlakyTest"
         }
       ]
     }
@@ -142,6 +145,9 @@
         },
         {
             "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+        },
+        {
+            "exclude-annotation": "androidx.test.filters.FlakyTest"
         }
       ]
     }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/DarkIconDispatcher.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/DarkIconDispatcher.java
index b33c544..a5e5aaa 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/DarkIconDispatcher.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/DarkIconDispatcher.java
@@ -26,6 +26,7 @@
 import com.android.systemui.plugins.annotations.ProvidesInterface;
 
 import java.util.ArrayList;
+import java.util.Collection;
 
 /**
  * Dispatches events to {@link DarkReceiver}s about changes in darkness, tint area and dark
@@ -78,7 +79,7 @@
      * @return the tint to apply to view depending on the desired tint color and
      *         the screen tintArea in which to apply that tint
      */
-    static int getTint(ArrayList<Rect> tintAreas, View view, int color) {
+    static int getTint(Collection<Rect> tintAreas, View view, int color) {
         if (isInAreas(tintAreas, view)) {
             return color;
         } else {
@@ -90,7 +91,7 @@
      * @return true if more than half of the view area are in any of the given
      *         areas, false otherwise
      */
-    static boolean isInAreas(ArrayList<Rect> areas, View view) {
+    static boolean isInAreas(Collection<Rect> areas, View view) {
         if (areas.isEmpty()) {
             return true;
         }
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index ab75498..9f4fc39 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -26,6 +26,8 @@
     <color name="qs_detail_button_white">#B3FFFFFF</color><!-- 70% white -->
     <color name="status_bar_clock_color">#FFFFFFFF</color>
     <color name="qs_tile_disabled_color">#9E9E9E</color> <!-- 38% black -->
+    <color name="status_bar_icons_hover_color_light">#38FFFFFF</color> <!-- 22% white -->
+    <color name="status_bar_icons_hover_color_dark">#38000000</color> <!-- 22% black -->
 
     <!-- The color of the background in the separated list of the Global Actions menu -->
     <color name="global_actions_separated_background">#F5F5F5</color>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index d7a4b25..de8287e 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -475,6 +475,8 @@
     <!-- Margin start of the system icons super container -->
     <dimen name="system_icons_super_container_margin_start">16dp</dimen>
 
+    <dimen name="status_icons_hover_state_background_radius">16dp</dimen>
+
     <!-- Width for the notification panel and related windows -->
     <dimen name="match_parent">-1px</dimen>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 983b09f..ee9b132 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -439,6 +439,8 @@
     <string name="face_reenroll_failure_dialog_content">Couldn\u2019t set up face unlock. Go to Settings to try again.</string>
     <!-- Message shown when the system-provided fingerprint dialog is shown, asking for authentication -->
     <string name="fingerprint_dialog_touch_sensor">Touch the fingerprint sensor</string>
+    <!-- Content description after successful auth when confirmation required -->
+    <string name="fingerprint_dialog_authenticated_confirmation">Press the unlock icon to continue</string>
     <!-- Message shown to inform the user a face cannot be recognized and fingerprint should instead be used.[CHAR LIMIT=50] -->
     <string name="fingerprint_dialog_use_fingerprint_instead">Can\u2019t recognize face. Use fingerprint instead.</string>
     <!-- Message shown to inform the user a face cannot be recognized and fingerprint should instead be used.[CHAR LIMIT=50] -->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index f1cb37c..53229b9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -3563,6 +3563,7 @@
      */
     @VisibleForTesting
     void handleUserSwitching(int userId, CountDownLatch latch) {
+        mLogger.logUserSwitching(userId, "from UserTracker");
         Assert.isMainThread();
         clearBiometricRecognized();
         boolean trustUsuallyManaged = mTrustManager.isTrustUsuallyManaged(userId);
@@ -3583,6 +3584,7 @@
      */
     @VisibleForTesting
     void handleUserSwitchComplete(int userId) {
+        mLogger.logUserSwitchComplete(userId, "from UserTracker");
         Assert.isMainThread();
         for (int i = 0; i < mCallbacks.size(); i++) {
             KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
@@ -4036,6 +4038,11 @@
 
     @AnyThread
     public void setSwitchingUser(boolean switching) {
+        if (switching) {
+            mLogger.logUserSwitching(getCurrentUser(), "from setSwitchingUser");
+        } else {
+            mLogger.logUserSwitchComplete(getCurrentUser(), "from setSwitchingUser");
+        }
         mSwitchingUser = switching;
         // Since this comes in on a binder thread, we need to post it first
         mHandler.post(() -> updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index eec5b3e..c26e546 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -602,10 +602,10 @@
 
     fun allowFingerprintOnCurrentOccludingActivityChanged(allow: Boolean) {
         logBuffer.log(
-                TAG,
-                VERBOSE,
-                { bool1 = allow },
-                { "allowFingerprintOnCurrentOccludingActivityChanged: $bool1" }
+            TAG,
+            VERBOSE,
+            { bool1 = allow },
+            { "allowFingerprintOnCurrentOccludingActivityChanged: $bool1" }
         )
     }
 
@@ -727,4 +727,28 @@
             { "notifying about enrollments changed: $str1" }
         )
     }
+
+    fun logUserSwitching(userId: Int, context: String) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                int1 = userId
+                str1 = context
+            },
+            { "userCurrentlySwitching: $str1, userId: $int1" }
+        )
+    }
+
+    fun logUserSwitchComplete(userId: Int, context: String) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                int1 = userId
+                str1 = context
+            },
+            { "userSwitchComplete: $str1, userId: $int1" }
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
index 9807b9e..a1b15f44 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
@@ -142,13 +142,18 @@
             STATE_IDLE,
             STATE_AUTHENTICATING_ANIMATING_IN,
             STATE_AUTHENTICATING,
-            STATE_PENDING_CONFIRMATION,
             STATE_AUTHENTICATED ->
                 if (isSideFps) {
                     R.string.security_settings_sfps_enroll_find_sensor_message
                 } else {
                     R.string.fingerprint_dialog_touch_sensor
                 }
+            STATE_PENDING_CONFIRMATION ->
+                if (isSideFps) {
+                    R.string.security_settings_sfps_enroll_find_sensor_message
+                } else {
+                    R.string.fingerprint_dialog_authenticated_confirmation
+                }
             STATE_ERROR,
             STATE_HELP -> R.string.biometric_dialog_try_again
             else -> null
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
index 37ce444..11418e6 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
@@ -50,7 +50,9 @@
 import com.airbnb.lottie.LottieAnimationView
 import com.airbnb.lottie.LottieProperty
 import com.airbnb.lottie.model.KeyPath
+import com.android.app.animation.Interpolators
 import com.android.internal.annotations.VisibleForTesting
+import com.android.keyguard.KeyguardPINView
 import com.android.systemui.Dumpable
 import com.android.systemui.R
 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
@@ -112,7 +114,7 @@
     private val isReverseDefaultRotation =
         context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
 
-    private var overlayHideAnimator: ViewPropertyAnimator? = null
+    private var overlayShowAnimator: ViewPropertyAnimator? = null
 
     private var overlayView: View? = null
         set(value) {
@@ -122,13 +124,23 @@
                 windowManager.removeView(oldView)
                 orientationListener.disable()
             }
-            overlayHideAnimator?.cancel()
-            overlayHideAnimator = null
+            overlayShowAnimator?.cancel()
+            overlayShowAnimator = null
 
             field = value
             field?.let { newView ->
+                if (requests.contains(SideFpsUiRequestSource.PRIMARY_BOUNCER)) {
+                    newView.alpha = 0f
+                    overlayShowAnimator =
+                        newView
+                            .animate()
+                            .alpha(1f)
+                            .setDuration(KeyguardPINView.ANIMATION_DURATION)
+                            .setInterpolator(Interpolators.ALPHA_IN)
+                }
                 windowManager.addView(newView, overlayViewParams)
                 orientationListener.enable()
+                overlayShowAnimator?.start()
             }
         }
     @VisibleForTesting var overlayOffsets: SensorLocationInternal = SensorLocationInternal.DEFAULT
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/PluginModule.java b/packages/SystemUI/src/com/android/systemui/dagger/PluginModule.java
index a3e26b8..d727a70 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/PluginModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/PluginModule.java
@@ -28,6 +28,7 @@
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.phone.ActivityStarterImpl;
 import com.android.systemui.statusbar.phone.DarkIconDispatcherImpl;
+import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher;
 import com.android.systemui.volume.VolumeDialogControllerImpl;
 
 import dagger.Binds;
@@ -49,6 +50,10 @@
     @Binds
     abstract DarkIconDispatcher provideDarkIconDispatcher(DarkIconDispatcherImpl controllerImpl);
 
+    @Binds
+    abstract SysuiDarkIconDispatcher provideSysuiDarkIconDispatcher(
+            DarkIconDispatcherImpl controllerImpl);
+
     /** */
     @Binds
     abstract FalsingManager provideFalsingManager(FalsingManagerProxy falsingManagerImpl);
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 66813f9..f3339c0 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -354,6 +354,10 @@
     // TODO(b/280426085): Tracking Bug
     @JvmField val NEW_BLUETOOTH_REPOSITORY = releasedFlag(612, "new_bluetooth_repository")
 
+    // TODO(b/292533677): Tracking Bug
+    val WIFI_TRACKER_LIB_FOR_WIFI_ICON =
+        unreleasedFlag(613, "wifi_tracker_lib_for_wifi_icon")
+
     // 700 - dialer/calls
     // TODO(b/254512734): Tracking Bug
     val ONGOING_CALL_STATUS_BAR_CHIP = releasedFlag(700, "ongoing_call_status_bar_chip")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt
index c849b84..508f71a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt
@@ -20,6 +20,7 @@
 import android.content.res.Resources
 import android.hardware.biometrics.BiometricSourceType
 import android.hardware.biometrics.BiometricSourceType.FINGERPRINT
+import android.hardware.fingerprint.FingerprintManager
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED
 import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
@@ -129,7 +130,14 @@
     val type: BiometricMessageType,
     val id: Int,
     val message: String?,
-)
+) {
+    fun isFingerprintLockoutMessage(): Boolean {
+        return source == FINGERPRINT &&
+            type == BiometricMessageType.ERROR &&
+            (id == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT ||
+                id == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT)
+    }
+}
 
 enum class BiometricMessageType {
     HELP,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractor.kt
index a2287c7..ff8d5c9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractor.kt
@@ -18,7 +18,6 @@
 
 import android.content.Context
 import android.content.Intent
-import android.hardware.fingerprint.FingerprintManager
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.dagger.SysUISingleton
@@ -35,6 +34,7 @@
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNot
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
@@ -74,15 +74,13 @@
     private val fingerprintLockoutEvents: Flow<Unit> =
         fingerprintAuthRepository.authenticationStatus
             .ifKeyguardOccludedByApp()
-            .filter {
-                it is ErrorFingerprintAuthenticationStatus &&
-                    (it.msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT ||
-                        it.msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT)
-            }
+            .filter { it is ErrorFingerprintAuthenticationStatus && it.isLockoutMessage() }
             .map {} // maps FingerprintAuthenticationStatus => Unit
     val message: Flow<BiometricMessage?> =
         merge(
-                biometricMessageInteractor.fingerprintErrorMessage,
+                biometricMessageInteractor.fingerprintErrorMessage.filterNot {
+                    it.isFingerprintLockoutMessage()
+                },
                 biometricMessageInteractor.fingerprintFailMessage,
                 biometricMessageInteractor.fingerprintHelpMessage,
             )
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt
index 7fc6016..ae18681 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.shared.model
 
+import android.hardware.fingerprint.FingerprintManager
 import android.os.SystemClock.elapsedRealtime
 
 /**
@@ -49,4 +50,9 @@
     val msg: String? = null,
     // present to break equality check if the same error occurs repeatedly.
     val createdAt: Long = elapsedRealtime(),
-) : FingerprintAuthenticationStatus()
+) : FingerprintAuthenticationStatus() {
+    fun isLockoutMessage(): Boolean {
+        return msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT ||
+            msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
index b891792..8601b3d 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
@@ -24,8 +24,10 @@
         viewModel: SceneContainerViewModel,
         containerConfig: SceneContainerConfig,
         scenes: Set<Scene>,
+        layoutInsetController: LayoutInsetsController,
     ) {
         this.viewModel = viewModel
+        setLayoutInsetsController(layoutInsetController)
         SceneWindowRootViewBinder.bind(
             view = this@SceneWindowRootView,
             viewModel = viewModel,
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
index 11e1eb9..4afe679 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
@@ -1,9 +1,15 @@
 package com.android.systemui.scene.ui.view
 
+import android.annotation.SuppressLint
 import android.content.Context
 import android.util.AttributeSet
+import android.util.Pair
+import android.view.DisplayCutout
 import android.view.View
+import android.view.WindowInsets
 import android.widget.FrameLayout
+import androidx.core.view.updateMargins
+import com.android.systemui.R
 import com.android.systemui.compose.ComposeFacade
 
 /**
@@ -25,6 +31,10 @@
         attrs,
     ) {
 
+    private lateinit var layoutInsetsController: LayoutInsetsController
+    private var leftInset = 0
+    private var rightInset = 0
+
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
 
@@ -41,6 +51,67 @@
         }
     }
 
+    override fun generateLayoutParams(attrs: AttributeSet?): FrameLayout.LayoutParams? {
+        return LayoutParams(context, attrs)
+    }
+
+    override fun generateDefaultLayoutParams(): FrameLayout.LayoutParams? {
+        return LayoutParams(
+            FrameLayout.LayoutParams.MATCH_PARENT,
+            FrameLayout.LayoutParams.MATCH_PARENT
+        )
+    }
+
+    override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets? {
+        val insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
+        if (fitsSystemWindows) {
+            val paddingChanged = insets.top != paddingTop || insets.bottom != paddingBottom
+
+            // Drop top inset, and pass through bottom inset.
+            if (paddingChanged) {
+                setPadding(0, 0, 0, 0)
+            }
+        } else {
+            val changed =
+                paddingLeft != 0 || paddingRight != 0 || paddingTop != 0 || paddingBottom != 0
+            if (changed) {
+                setPadding(0, 0, 0, 0)
+            }
+        }
+        leftInset = 0
+        rightInset = 0
+
+        val displayCutout = rootWindowInsets.displayCutout
+        val pairInsets: Pair<Int, Int> =
+            layoutInsetsController.getinsets(windowInsets, displayCutout)
+        leftInset = pairInsets.first
+        rightInset = pairInsets.second
+        applyMargins()
+        return windowInsets
+    }
+
+    fun setLayoutInsetsController(layoutInsetsController: LayoutInsetsController) {
+        this.layoutInsetsController = layoutInsetsController
+    }
+
+    private fun applyMargins() {
+        val count = childCount
+        for (i in 0 until count) {
+            val child = getChildAt(i)
+            if (child.layoutParams is LayoutParams) {
+                val layoutParams = child.layoutParams as LayoutParams
+                if (
+                    !layoutParams.ignoreRightInset &&
+                        (layoutParams.rightMargin != rightInset ||
+                            layoutParams.leftMargin != leftInset)
+                ) {
+                    layoutParams.updateMargins(left = leftInset, right = rightInset)
+                    child.requestLayout()
+                }
+            }
+        }
+    }
+
     /**
      * Returns `true` if this view is the true root of the view-hierarchy; `false` otherwise.
      *
@@ -50,4 +121,44 @@
         // TODO(b/283300105): remove this check once there's only one subclass of WindowRootView.
         return parent.let { it !is View || it.id == android.R.id.content }
     }
+
+    /** Controller responsible for calculating insets for the shade window. */
+    interface LayoutInsetsController {
+
+        /** Update the insets and calculate them accordingly. */
+        fun getinsets(
+            windowInsets: WindowInsets?,
+            displayCutout: DisplayCutout?,
+        ): Pair<Int, Int>
+    }
+
+    private class LayoutParams : FrameLayout.LayoutParams {
+        var ignoreRightInset = false
+
+        constructor(
+            width: Int,
+            height: Int,
+        ) : super(
+            width,
+            height,
+        )
+
+        @SuppressLint("CustomViewStyleable")
+        constructor(
+            context: Context,
+            attrs: AttributeSet?,
+        ) : super(
+            context,
+            attrs,
+        ) {
+            val obtainedAttributes =
+                context.obtainStyledAttributes(attrs, R.styleable.StatusBarWindowView_Layout)
+            ignoreRightInset =
+                obtainedAttributes.getBoolean(
+                    R.styleable.StatusBarWindowView_Layout_ignoreRightInset,
+                    false
+                )
+            obtainedAttributes.recycle()
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
index 2b62b7d..a9c4aeb 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
@@ -17,19 +17,15 @@
 package com.android.systemui.shade;
 
 import static android.os.Trace.TRACE_TAG_APP;
-import static android.view.WindowInsets.Type.systemBars;
 
 import static com.android.systemui.statusbar.phone.CentralSurfaces.DEBUG;
 
 import android.annotation.ColorInt;
 import android.annotation.DrawableRes;
 import android.annotation.LayoutRes;
-import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.Configuration;
-import android.content.res.TypedArray;
 import android.graphics.Canvas;
-import android.graphics.Insets;
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
@@ -37,9 +33,7 @@
 import android.os.Bundle;
 import android.os.Trace;
 import android.util.AttributeSet;
-import android.util.Pair;
 import android.view.ActionMode;
-import android.view.DisplayCutout;
 import android.view.InputQueue;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
@@ -51,13 +45,10 @@
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.Window;
-import android.view.WindowInsets;
 import android.view.WindowInsetsController;
-import android.widget.FrameLayout;
 
 import com.android.internal.view.FloatingActionMode;
 import com.android.internal.widget.floatingtoolbar.FloatingToolbar;
-import com.android.systemui.R;
 import com.android.systemui.scene.ui.view.WindowRootView;
 
 /**
@@ -68,9 +59,6 @@
 public class NotificationShadeWindowView extends WindowRootView {
     public static final String TAG = "NotificationShadeWindowView";
 
-    private int mRightInset = 0;
-    private int mLeftInset = 0;
-
     // Implements the floating action mode for TextView's Cut/Copy/Past menu. Normally provided by
     // DecorView, but since this is a special window we have to roll our own.
     private View mFloatingActionModeOriginatingView;
@@ -79,7 +67,6 @@
     private ViewTreeObserver.OnPreDrawListener mFloatingToolbarPreDrawListener;
 
     private InteractionEventHandler mInteractionEventHandler;
-    private LayoutInsetsController mLayoutInsetProvider;
 
     public NotificationShadeWindowView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -87,64 +74,6 @@
     }
 
     @Override
-    public WindowInsets onApplyWindowInsets(WindowInsets windowInsets) {
-        final Insets insets = windowInsets.getInsetsIgnoringVisibility(systemBars());
-        if (getFitsSystemWindows()) {
-            boolean paddingChanged = insets.top != getPaddingTop()
-                    || insets.bottom != getPaddingBottom();
-
-            // Drop top inset, and pass through bottom inset.
-            if (paddingChanged) {
-                setPadding(0, 0, 0, 0);
-            }
-        } else {
-            boolean changed = getPaddingLeft() != 0
-                    || getPaddingRight() != 0
-                    || getPaddingTop() != 0
-                    || getPaddingBottom() != 0;
-            if (changed) {
-                setPadding(0, 0, 0, 0);
-            }
-        }
-
-        mLeftInset = 0;
-        mRightInset = 0;
-        DisplayCutout displayCutout = getRootWindowInsets().getDisplayCutout();
-        Pair<Integer, Integer> pairInsets = mLayoutInsetProvider
-                .getinsets(windowInsets, displayCutout);
-        mLeftInset = pairInsets.first;
-        mRightInset = pairInsets.second;
-        applyMargins();
-        return windowInsets;
-    }
-
-    private void applyMargins() {
-        final int count = getChildCount();
-        for (int i = 0; i < count; i++) {
-            View child = getChildAt(i);
-            if (child.getLayoutParams() instanceof LayoutParams) {
-                LayoutParams lp = (LayoutParams) child.getLayoutParams();
-                if (!lp.ignoreRightInset
-                        && (lp.rightMargin != mRightInset || lp.leftMargin != mLeftInset)) {
-                    lp.rightMargin = mRightInset;
-                    lp.leftMargin = mLeftInset;
-                    child.requestLayout();
-                }
-            }
-        }
-    }
-
-    @Override
-    public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
-        return new LayoutParams(getContext(), attrs);
-    }
-
-    @Override
-    protected FrameLayout.LayoutParams generateDefaultLayoutParams() {
-        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
-    }
-
-    @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         setWillNotDraw(!DEBUG);
@@ -172,10 +101,6 @@
         mInteractionEventHandler = listener;
     }
 
-    protected void setLayoutInsetsController(LayoutInsetsController provider) {
-        mLayoutInsetProvider = provider;
-    }
-
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
         Boolean result = mInteractionEventHandler.handleDispatchTouchEvent(ev);
@@ -227,24 +152,6 @@
         }
     }
 
-    private static class LayoutParams extends FrameLayout.LayoutParams {
-
-        public boolean ignoreRightInset;
-
-        LayoutParams(int width, int height) {
-            super(width, height);
-        }
-
-        LayoutParams(Context c, AttributeSet attrs) {
-            super(c, attrs);
-
-            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.StatusBarWindowView_Layout);
-            ignoreRightInset = a.getBoolean(
-                    R.styleable.StatusBarWindowView_Layout_ignoreRightInset, false);
-            a.recycle();
-        }
-    }
-
     @Override
     public ActionMode startActionModeForChild(View originalView, ActionMode.Callback callback,
             int type) {
@@ -357,18 +264,6 @@
         }
     }
 
-    /**
-     * Controller responsible for calculating insets for the shade window.
-     */
-    public interface LayoutInsetsController {
-
-        /**
-         * Update the insets and calculate them accordingly.
-         */
-        Pair<Integer, Integer> getinsets(@Nullable WindowInsets windowInsets,
-                @Nullable DisplayCutout displayCutout);
-    }
-
     interface InteractionEventHandler {
         /**
          * Returns a result for {@link ViewGroup#dispatchTouchEvent(MotionEvent)} or null to defer
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
index 529f12e..c6cb9c4 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
@@ -61,6 +61,7 @@
 import com.android.systemui.statusbar.phone.StatusBarIconController
 import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.phone.StatusIconContainer
+import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory
 import com.android.systemui.statusbar.policy.Clock
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.NextAlarmController
@@ -99,6 +100,7 @@
     private val qsBatteryModeController: QsBatteryModeController,
     private val nextAlarmController: NextAlarmController,
     private val activityStarter: ActivityStarter,
+    private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
 ) : ViewController<View>(header), Dumpable {
 
     companion object {
@@ -326,6 +328,9 @@
         demoModeController.addCallback(demoModeReceiver)
         statusBarIconController.addIconGroup(iconManager)
         nextAlarmController.addCallback(nextAlarmCallback)
+        systemIcons.setOnHoverListener(
+            statusOverlayHoverListenerFactory.createListener(systemIcons)
+        )
     }
 
     override fun onViewDetached() {
@@ -336,6 +341,7 @@
         demoModeController.removeCallback(demoModeReceiver)
         statusBarIconController.removeIconGroup(iconManager)
         nextAlarmController.removeCallback(nextAlarmCallback)
+        systemIcons.setOnHoverListener(null)
     }
 
     fun disable(state1: Int, state2: Int, animate: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
index fc6479e..e02c427 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
@@ -42,6 +42,7 @@
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.LightRevealScrim
+import com.android.systemui.statusbar.NotificationInsetsController
 import com.android.systemui.statusbar.NotificationShelf
 import com.android.systemui.statusbar.NotificationShelfController
 import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent
@@ -78,6 +79,7 @@
             containerConfigProvider: Provider<SceneContainerConfig>,
             @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
             scenesProvider: Provider<Set<@JvmSuppressWildcards Scene>>,
+            layoutInsetController: NotificationInsetsController,
         ): WindowRootView {
             return if (
                 featureFlags.isEnabled(Flags.SCENE_CONTAINER) && ComposeFacade.isComposeAvailable()
@@ -88,6 +90,7 @@
                     viewModel = viewModelProvider.get(),
                     containerConfig = containerConfigProvider.get(),
                     scenes = scenesProvider.get(),
+                    layoutInsetController = layoutInsetController,
                 )
                 sceneWindowRootView
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsController.java
index 39d7d66..27f42c6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsController.java
@@ -16,11 +16,11 @@
 
 package com.android.systemui.statusbar;
 
-import com.android.systemui.shade.NotificationShadeWindowView;
+import com.android.systemui.scene.ui.view.WindowRootView;
 
 /**
  * Calculates insets for the notification shade window view.
  */
 public abstract class NotificationInsetsController
-        implements NotificationShadeWindowView.LayoutInsetsController {
+        implements WindowRootView.LayoutInsetsController {
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java
index c9ce12a..ff40f70 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java
@@ -16,13 +16,7 @@
 
 package com.android.systemui.statusbar.connectivity;
 
-import android.content.Context;
 import android.content.Intent;
-import android.net.ConnectivityManager;
-import android.net.wifi.WifiManager;
-import android.os.Handler;
-import android.os.SimpleClock;
-import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
@@ -35,25 +29,18 @@
 import androidx.lifecycle.LifecycleOwner;
 import androidx.lifecycle.LifecycleRegistry;
 
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.settings.UserTracker;
 import com.android.wifitrackerlib.MergedCarrierEntry;
 import com.android.wifitrackerlib.WifiEntry;
 import com.android.wifitrackerlib.WifiPickerTracker;
 
 import java.io.PrintWriter;
-import java.time.Clock;
-import java.time.ZoneOffset;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.Executor;
 
-import javax.inject.Inject;
-
 /** */
 public class AccessPointControllerImpl implements AccessPointController,
         WifiPickerTracker.WifiPickerTrackerCallback,
@@ -272,77 +259,4 @@
             }
         }
     };
-
-    /**
-     * Factory for creating {@link WifiPickerTracker}.
-     *
-     * Uses the same time intervals as the Settings page for Wifi.
-     */
-    @SysUISingleton
-    public static class WifiPickerTrackerFactory {
-
-        // Max age of tracked WifiEntries
-        private static final long MAX_SCAN_AGE_MILLIS = 15_000;
-        // Interval between initiating WifiPickerTracker scans
-        private static final long SCAN_INTERVAL_MILLIS = 10_000;
-
-        private final Context mContext;
-        private final @Nullable WifiManager mWifiManager;
-        private final ConnectivityManager mConnectivityManager;
-        private final Handler mMainHandler;
-        private final Handler mWorkerHandler;
-        private final Clock mClock = new SimpleClock(ZoneOffset.UTC) {
-            @Override
-            public long millis() {
-                return SystemClock.elapsedRealtime();
-            }
-        };
-
-        @Inject
-        public WifiPickerTrackerFactory(
-                Context context,
-                @Nullable WifiManager wifiManager,
-                ConnectivityManager connectivityManager,
-                @Main Handler mainHandler,
-                @Background Handler workerHandler
-        ) {
-            mContext = context;
-            mWifiManager = wifiManager;
-            mConnectivityManager = connectivityManager;
-            mMainHandler = mainHandler;
-            mWorkerHandler = workerHandler;
-        }
-
-        private boolean isSupported() {
-            return mWifiManager != null;
-        }
-
-        /**
-         * Create a {@link WifiPickerTracker}
-         *
-         * @param lifecycle
-         * @param listener
-         * @return a new {@link WifiPickerTracker} or {@code null} if {@link WifiManager} is null.
-         */
-        public @Nullable WifiPickerTracker create(
-                Lifecycle lifecycle,
-                WifiPickerTracker.WifiPickerTrackerCallback listener
-        ) {
-            if (mWifiManager == null) {
-                return null;
-            }
-            return new WifiPickerTracker(
-                    lifecycle,
-                    mContext,
-                    mWifiManager,
-                    mConnectivityManager,
-                    mMainHandler,
-                    mWorkerHandler,
-                    mClock,
-                    MAX_SCAN_AGE_MILLIS,
-                    SCAN_INTERVAL_MILLIS,
-                    listener
-            );
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiPickerTrackerFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiPickerTrackerFactory.kt
new file mode 100644
index 0000000..ddbfcef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiPickerTrackerFactory.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.statusbar.connectivity
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.wifi.WifiManager
+import android.os.Handler
+import android.os.SimpleClock
+import androidx.lifecycle.Lifecycle
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.util.time.SystemClock
+import com.android.wifitrackerlib.WifiPickerTracker
+import com.android.wifitrackerlib.WifiPickerTracker.WifiPickerTrackerCallback
+import java.time.Clock
+import java.time.ZoneOffset
+import javax.inject.Inject
+
+/**
+ * Factory for creating [WifiPickerTracker] for SysUI.
+ *
+ * Uses the same time intervals as the Settings page for Wifi.
+ */
+@SysUISingleton
+class WifiPickerTrackerFactory
+@Inject
+constructor(
+    private val context: Context,
+    private val wifiManager: WifiManager?,
+    private val connectivityManager: ConnectivityManager,
+    private val systemClock: SystemClock,
+    @Main private val mainHandler: Handler,
+    @Background private val workerHandler: Handler,
+) {
+    private val clock: Clock =
+        object : SimpleClock(ZoneOffset.UTC) {
+            override fun millis(): Long {
+                return systemClock.elapsedRealtime()
+            }
+        }
+    val isSupported: Boolean
+        get() = wifiManager != null
+
+    /**
+     * Creates a [WifiPickerTracker] instance.
+     *
+     * @return a new [WifiPickerTracker] or null if [WifiManager] is null.
+     */
+    fun create(
+        lifecycle: Lifecycle,
+        listener: WifiPickerTrackerCallback,
+    ): WifiPickerTracker? {
+        return if (wifiManager == null) {
+            null
+        } else
+            WifiPickerTracker(
+                lifecycle,
+                context,
+                wifiManager,
+                connectivityManager,
+                mainHandler,
+                workerHandler,
+                clock,
+                MAX_SCAN_AGE_MILLIS,
+                SCAN_INTERVAL_MILLIS,
+                listener,
+            )
+    }
+
+    companion object {
+        /** Max age of tracked WifiEntries. */
+        private const val MAX_SCAN_AGE_MILLIS: Long = 15000
+        /** Interval between initiating WifiPickerTracker scans. */
+        private const val SCAN_INTERVAL_MILLIS: Long = 10000
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 1c7a186..f726c4e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -72,6 +72,7 @@
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl;
 import com.android.systemui.statusbar.phone.StatusBarIconList;
+import com.android.systemui.statusbar.phone.StatusBarNotificationPresenterModule;
 import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallFlags;
@@ -96,7 +97,7 @@
  * their own version of CentralSurfaces can include just dependencies, without injecting
  * CentralSurfaces itself.
  */
-@Module
+@Module(includes = {StatusBarNotificationPresenterModule.class})
 public interface CentralSurfacesDependenciesModule {
     /** */
     @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 6eeb25f..f96fb26 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -623,7 +623,7 @@
 
     private final ActivityLaunchAnimator mActivityLaunchAnimator;
     private NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
-    protected NotificationPresenter mPresenter;
+    private final NotificationPresenter mPresenter;
     private NotificationActivityStarter mNotificationActivityStarter;
     private final Lazy<NotificationShadeDepthController> mNotificationShadeDepthControllerLazy;
     private final Optional<Bubbles> mBubblesOptional;
@@ -724,6 +724,7 @@
             Lazy<NotificationShadeWindowViewController> notificationShadeWindowViewControllerLazy,
             NotificationShelfController notificationShelfController,
             NotificationStackScrollLayoutController notificationStackScrollLayoutController,
+            NotificationPresenter notificationPresenter,
             DozeParameters dozeParameters,
             ScrimController scrimController,
             Lazy<LockscreenWallpaper> lockscreenWallpaperLazy,
@@ -830,6 +831,7 @@
         mStackScrollerController = notificationStackScrollLayoutController;
         mStackScroller = mStackScrollerController.getView();
         mNotifListContainer = mStackScrollerController.getNotificationListContainer();
+        mPresenter = notificationPresenter;
         mDozeServiceHost = dozeServiceHost;
         mPowerManager = powerManager;
         mDozeParameters = dozeParameters;
@@ -1604,7 +1606,6 @@
         mShadeController.setNotificationShadeWindowViewController(
                 getNotificationShadeWindowViewController());
         mBackActionInteractor.setup(mQsController, mShadeSurface);
-        mPresenter = mCentralSurfacesComponent.getNotificationPresenter();
         mNotificationActivityStarter = mCentralSurfacesComponent.getNotificationActivityStarter();
 
         mHeadsUpManager.addListener(mCentralSurfacesComponent.getStatusBarHeadsUpChangeListener());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java
index 2677c3f2..c4495ed 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java
@@ -32,6 +32,11 @@
 
 import javax.inject.Inject;
 
+import kotlinx.coroutines.flow.FlowKt;
+import kotlinx.coroutines.flow.MutableStateFlow;
+import kotlinx.coroutines.flow.StateFlow;
+import kotlinx.coroutines.flow.StateFlowKt;
+
 /**
  */
 @SysUISingleton
@@ -47,6 +52,9 @@
     private int mDarkModeIconColorSingleTone;
     private int mLightModeIconColorSingleTone;
 
+    private final MutableStateFlow<DarkChange> mDarkChangeFlow = StateFlowKt.MutableStateFlow(
+            DarkChange.EMPTY);
+
     /**
      */
     @Inject
@@ -66,6 +74,11 @@
         return mTransitionsController;
     }
 
+    @Override
+    public StateFlow<DarkChange> darkChangeFlow() {
+        return FlowKt.asStateFlow(mDarkChangeFlow);
+    }
+
     public void addDarkReceiver(DarkReceiver receiver) {
         mReceivers.put(receiver, receiver);
         receiver.onDarkChanged(mTintAreas, mDarkIntensity, mIconTint);
@@ -122,6 +135,7 @@
     }
 
     private void applyIconTint() {
+        mDarkChangeFlow.setValue(new DarkChange(mTintAreas, mDarkIntensity, mIconTint));
         for (int i = 0; i < mReceivers.size(); i++) {
             mReceivers.valueAt(i).onDarkChanged(mTintAreas, mDarkIntensity, mIconTint);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
index 720eeba..5c1f824b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -34,7 +34,6 @@
 import android.view.Gravity;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
 import android.view.WindowInsets;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
@@ -43,11 +42,11 @@
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.app.animation.Interpolators;
 import com.android.settingslib.Utils;
 import com.android.systemui.R;
 import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange;
 import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
 import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder;
 import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel;
@@ -55,6 +54,11 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 
+import kotlinx.coroutines.flow.FlowKt;
+import kotlinx.coroutines.flow.MutableStateFlow;
+import kotlinx.coroutines.flow.StateFlow;
+import kotlinx.coroutines.flow.StateFlowKt;
+
 /**
  * The header group on Keyguard.
  */
@@ -83,6 +87,8 @@
     private int mStatusBarPaddingEnd;
     private int mMinDotWidth;
     private View mSystemIconsContainer;
+    private final MutableStateFlow<DarkChange> mDarkChange = StateFlowKt.MutableStateFlow(
+            DarkChange.EMPTY);
 
     private View mCutoutSpace;
     private ViewGroup mStatusIconArea;
@@ -374,49 +380,6 @@
         return mKeyguardUserAvatarEnabled;
     }
 
-    private void animateNextLayoutChange() {
-        final int systemIconsCurrentX = mSystemIconsContainer.getLeft();
-        final boolean userAvatarVisible = mMultiUserAvatar.getParent() == mStatusIconArea;
-        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
-            @Override
-            public boolean onPreDraw() {
-                getViewTreeObserver().removeOnPreDrawListener(this);
-                boolean userAvatarHiding = userAvatarVisible
-                        && mMultiUserAvatar.getParent() != mStatusIconArea;
-                mSystemIconsContainer.setX(systemIconsCurrentX);
-                mSystemIconsContainer.animate()
-                        .translationX(0)
-                        .setDuration(400)
-                        .setStartDelay(userAvatarHiding ? 300 : 0)
-                        .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
-                        .start();
-                if (userAvatarHiding) {
-                    getOverlay().add(mMultiUserAvatar);
-                    mMultiUserAvatar.animate()
-                            .alpha(0f)
-                            .setDuration(300)
-                            .setStartDelay(0)
-                            .setInterpolator(Interpolators.ALPHA_OUT)
-                            .withEndAction(() -> {
-                                mMultiUserAvatar.setAlpha(1f);
-                                getOverlay().remove(mMultiUserAvatar);
-                            })
-                            .start();
-
-                } else {
-                    mMultiUserAvatar.setAlpha(0f);
-                    mMultiUserAvatar.animate()
-                            .alpha(1f)
-                            .setDuration(300)
-                            .setStartDelay(200)
-                            .setInterpolator(Interpolators.ALPHA_IN);
-                }
-                return true;
-            }
-        });
-
-    }
-
     @Override
     public void setVisibility(int visibility) {
         super.setVisibility(visibility);
@@ -474,6 +437,7 @@
             iconManager.setTint(iconColor);
         }
 
+        mDarkChange.setValue(new DarkChange(mEmptyTintRect, intensity, iconColor));
         applyDarkness(R.id.battery, mEmptyTintRect, intensity, iconColor);
         applyDarkness(R.id.clock, mEmptyTintRect, intensity, iconColor);
     }
@@ -536,4 +500,8 @@
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
         Trace.endSection();
     }
+
+    public StateFlow<DarkChange> darkChangeFlow() {
+        return FlowKt.asStateFlow(mDarkChange);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 680f19a..be336e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -69,8 +69,6 @@
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.settings.SecureSettings;
 
-import kotlin.Unit;
-
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
@@ -78,6 +76,8 @@
 
 import javax.inject.Inject;
 
+import kotlin.Unit;
+
 /** View Controller for {@link com.android.systemui.statusbar.phone.KeyguardStatusBarView}. */
 public class KeyguardStatusBarViewController extends ViewController<KeyguardStatusBarView> {
     private static final String TAG = "KeyguardStatusBarViewController";
@@ -119,6 +119,9 @@
     private final Object mLock = new Object();
     private final KeyguardLogger mLogger;
 
+    private View mSystemIconsContainer;
+    private final StatusOverlayHoverListenerFactory mStatusOverlayHoverListenerFactory;
+
     // TODO(b/273443374): remove
     private NotificationMediaManager mNotificationMediaManager;
 
@@ -286,7 +289,8 @@
             CommandQueue commandQueue,
             @Main Executor mainExecutor,
             KeyguardLogger logger,
-            NotificationMediaManager notificationMediaManager
+            NotificationMediaManager notificationMediaManager,
+            StatusOverlayHoverListenerFactory statusOverlayHoverListenerFactory
     ) {
         super(view);
         mCarrierTextController = carrierTextController;
@@ -339,6 +343,7 @@
                 this::updateViewState
         );
         mNotificationMediaManager = notificationMediaManager;
+        mStatusOverlayHoverListenerFactory = statusOverlayHoverListenerFactory;
     }
 
     @Override
@@ -363,6 +368,10 @@
             mTintedIconManager.setBlockList(getBlockedIcons());
             mStatusBarIconController.addIconGroup(mTintedIconManager);
         }
+        mSystemIconsContainer = mView.findViewById(R.id.system_icons);
+        StatusOverlayHoverListener hoverListener = mStatusOverlayHoverListenerFactory
+                .createDarkAwareListener(mSystemIconsContainer, mView.darkChangeFlow());
+        mSystemIconsContainer.setOnHoverListener(hoverListener);
         mView.setOnApplyWindowInsetsListener(
                 (view, windowInsets) -> mView.updateWindowInsets(windowInsets, mInsetsProvider));
         mSecureSettings.registerContentObserverForUser(
@@ -376,6 +385,7 @@
 
     @Override
     protected void onViewDetached() {
+        mSystemIconsContainer.setOnHoverListener(null);
         mConfigurationController.removeCallback(mConfigurationListener);
         mAnimationScheduler.removeCallback(mAnimationCallback);
         mUserInfoController.removeCallback(mOnUserInfoChangedListener);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index 4e136de..8c3050d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -63,9 +63,12 @@
     private val userChipViewModel: StatusBarUserChipViewModel,
     private val viewUtil: ViewUtil,
     private val featureFlags: FeatureFlags,
-    private val configurationController: ConfigurationController
+    private val configurationController: ConfigurationController,
+    private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
 ) : ViewController<PhoneStatusBarView>(view) {
 
+    private lateinit var statusContainer: View
+
     private val configurationListener = object : ConfigurationController.ConfigurationListener {
         override fun onConfigChanged(newConfig: Configuration?) {
             mView.updateResources()
@@ -73,6 +76,9 @@
     }
 
     override fun onViewAttached() {
+        statusContainer = mView.findViewById(R.id.system_icons)
+        statusContainer.setOnHoverListener(
+            statusOverlayHoverListenerFactory.createDarkAwareListener(statusContainer))
         if (moveFromCenterAnimationController == null) return
 
         val statusBarLeftSide: View = mView.findViewById(R.id.status_bar_start_side_except_heads_up)
@@ -104,6 +110,7 @@
     }
 
     override fun onViewDetached() {
+        statusContainer.setOnHoverListener(null)
         progressProvider?.setReadyToHandleTransition(false)
         moveFromCenterAnimationController?.onViewDetached()
         configurationController.removeCallback(configurationListener)
@@ -245,6 +252,7 @@
         private val shadeLogger: ShadeLogger,
         private val viewUtil: ViewUtil,
         private val configurationController: ConfigurationController,
+        private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
     ) {
         fun create(
             view: PhoneStatusBarView
@@ -268,7 +276,8 @@
                     userChipViewModel,
                     viewUtil,
                     featureFlags,
-                    configurationController
+                    configurationController,
+                    statusOverlayHoverListenerFactory,
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index ec0c00e..8de213f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -215,8 +215,7 @@
     public void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row) {
         mLogger.logStartingActivityFromClick(entry);
 
-        if (mRemoteInputManager.isRemoteInputActive(entry)
-                && !TextUtils.isEmpty(row.getActiveRemoteInputText())) {
+        if (mRemoteInputManager.isRemoteInputActive(entry)) {
             // We have an active remote input typed and the user clicked on the notification.
             // this was probably unintentional, so we're closing the edit text instead.
             mRemoteInputManager.closeRemoteInputs();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index ad8530d..ecc996c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -32,6 +32,7 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.InitController;
 import com.android.systemui.R;
+import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 import com.android.systemui.power.domain.interactor.PowerInteractor;
@@ -59,12 +60,11 @@
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
-import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import javax.inject.Inject;
 
-@CentralSurfacesComponent.CentralSurfacesScope
+@SysUISingleton
 class StatusBarNotificationPresenter implements NotificationPresenter, CommandQueue.Callbacks {
     private static final String TAG = "StatusBarNotificationPresenter";
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListener.kt
new file mode 100644
index 0000000..881741a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListener.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.statusbar.phone
+
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.graphics.Color
+import android.graphics.drawable.PaintDrawable
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.OnHoverListener
+import androidx.annotation.ColorInt
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.plugins.DarkIconDispatcher
+import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+class StatusOverlayHoverListenerFactory
+@Inject
+constructor(
+    @Main private val resources: Resources,
+    private val configurationController: ConfigurationController,
+    private val darkIconDispatcher: SysuiDarkIconDispatcher,
+) {
+
+    /** Creates listener always using the same light color for overlay */
+    fun createListener(view: View) =
+        StatusOverlayHoverListener(
+            view,
+            configurationController,
+            resources,
+            flowOf(HoverTheme.LIGHT),
+        )
+
+    /**
+     * Creates listener using [DarkIconDispatcher] to determine light or dark color of the overlay
+     */
+    fun createDarkAwareListener(view: View) =
+        createDarkAwareListener(view, darkIconDispatcher.darkChangeFlow())
+
+    /**
+     * Creates listener using provided [DarkChange] producer to determine light or dark color of the
+     * overlay
+     */
+    fun createDarkAwareListener(view: View, darkFlow: StateFlow<DarkChange>) =
+        StatusOverlayHoverListener(
+            view,
+            configurationController,
+            resources,
+            darkFlow.map { toHoverTheme(view, it) },
+        )
+
+    private fun toHoverTheme(view: View, darkChange: DarkChange): HoverTheme {
+        val calculatedTint = DarkIconDispatcher.getTint(darkChange.areas, view, darkChange.tint)
+        // currently calculated tint is either white or some shade of black.
+        // So checking for Color.WHITE is deterministic compared to checking for Color.BLACK.
+        // In the future checking Color.luminance() might be more appropriate.
+        return if (calculatedTint == Color.WHITE) HoverTheme.LIGHT else HoverTheme.DARK
+    }
+}
+
+/**
+ * theme of hover drawable - it's different from device theme. This theme depends on view's
+ * background and/or dark value returned from [DarkIconDispatcher]
+ */
+enum class HoverTheme {
+    LIGHT,
+    DARK
+}
+
+/**
+ * [OnHoverListener] that adds [Drawable] overlay on top of the status icons when cursor/stylus
+ * starts hovering over them and removes overlay when status icons are no longer hovered
+ */
+class StatusOverlayHoverListener(
+    view: View,
+    configurationController: ConfigurationController,
+    private val resources: Resources,
+    private val themeFlow: Flow<HoverTheme>,
+) : OnHoverListener {
+
+    @ColorInt private var darkColor: Int = 0
+    @ColorInt private var lightColor: Int = 0
+    private var cornerRadius = 0f
+
+    private var lastTheme = HoverTheme.LIGHT
+
+    val backgroundColor
+        get() = if (lastTheme == HoverTheme.LIGHT) lightColor else darkColor
+
+    init {
+        view.repeatWhenAttached {
+            lifecycleScope.launch {
+                val configurationListener =
+                    object : ConfigurationListener {
+                        override fun onConfigChanged(newConfig: Configuration?) {
+                            updateResources()
+                        }
+                    }
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    configurationController.addCallback(configurationListener)
+                }
+                configurationController.removeCallback(configurationListener)
+            }
+            lifecycleScope.launch { themeFlow.collect { lastTheme = it } }
+        }
+        updateResources()
+    }
+
+    override fun onHover(v: View, event: MotionEvent): Boolean {
+        if (event.action == MotionEvent.ACTION_HOVER_ENTER) {
+            val drawable =
+                PaintDrawable(backgroundColor).apply {
+                    setCornerRadius(cornerRadius)
+                    setBounds(0, 0, v.width, v.height)
+                }
+            v.overlay.add(drawable)
+        } else if (event.action == MotionEvent.ACTION_HOVER_EXIT) {
+            v.overlay.clear()
+        }
+        return true
+    }
+
+    private fun updateResources() {
+        lightColor = resources.getColor(R.color.status_bar_icons_hover_color_light)
+        darkColor = resources.getColor(R.color.status_bar_icons_hover_color_dark)
+        cornerRadius = resources.getDimension(R.dimen.status_icons_hover_state_background_radius)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SysuiDarkIconDispatcher.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SysuiDarkIconDispatcher.java
index d537721..f5e9034 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SysuiDarkIconDispatcher.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SysuiDarkIconDispatcher.java
@@ -16,9 +16,16 @@
 
 package com.android.systemui.statusbar.phone;
 
+import android.graphics.Rect;
+
 import com.android.systemui.Dumpable;
 import com.android.systemui.plugins.DarkIconDispatcher;
 
+import java.util.ArrayList;
+import java.util.Collection;
+
+import kotlinx.coroutines.flow.StateFlow;
+
 /**
  * Dispatches events to {@link DarkReceiver}s about changes in darkness, tint area
  * and dark intensity.
@@ -29,4 +36,26 @@
      * @return LightBarTransitionsController
      */
     LightBarTransitionsController getTransitionsController();
+
+    /**
+     * Flow equivalent of registering {@link DarkReceiver} using
+     * {@link DarkIconDispatcher#addDarkReceiver(DarkReceiver)}
+     */
+    StateFlow<DarkChange> darkChangeFlow();
+
+    /** Model for {@link #darkChangeFlow()} */
+    class DarkChange {
+
+        public static final DarkChange EMPTY = new DarkChange(new ArrayList<>(), 0, 0);
+
+        public DarkChange(Collection<Rect> areas, float darkIntensity, int tint) {
+            this.areas = areas;
+            this.darkIntensity = darkIntensity;
+            this.tint = tint;
+        }
+
+        public final Collection<Rect> areas;
+        public final float darkIntensity;
+        public final int tint;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java
index e77f419..3a3663d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java
@@ -22,13 +22,11 @@
 
 import com.android.systemui.scene.ui.view.WindowRootView;
 import com.android.systemui.shade.ShadeHeaderController;
-import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.phone.CentralSurfacesCommandQueueCallbacks;
 import com.android.systemui.statusbar.phone.CentralSurfacesImpl;
 import com.android.systemui.statusbar.phone.StatusBarHeadsUpChangeListener;
 import com.android.systemui.statusbar.phone.StatusBarNotificationActivityStarterModule;
-import com.android.systemui.statusbar.phone.StatusBarNotificationPresenterModule;
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment;
 
 import dagger.Subcomponent;
@@ -51,7 +49,6 @@
 @Subcomponent(modules = {
         StatusBarViewModule.class,
         StatusBarNotificationActivityStarterModule.class,
-        StatusBarNotificationPresenterModule.class,
 })
 @CentralSurfacesComponent.CentralSurfacesScope
 public interface CentralSurfacesComponent {
@@ -97,6 +94,4 @@
     CollapsedStatusBarFragment createCollapsedStatusBarFragment();
 
     NotificationActivityStarter getNotificationActivityStarter();
-
-    NotificationPresenter getNotificationPresenter();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 0e99c67..e1fd37f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -19,6 +19,8 @@
 import android.net.wifi.WifiManager
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogBufferFactory
 import com.android.systemui.log.table.TableLogBuffer
@@ -48,9 +50,12 @@
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModelImpl
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryDagger
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryViaTrackerLibDagger
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.DisabledWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryViaTrackerLib
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
 import dagger.Binds
@@ -114,14 +119,19 @@
         impl: CollapsedStatusBarViewBinderImpl
     ): CollapsedStatusBarViewBinder
 
+    @Binds
+    @IntoMap
+    @ClassKey(WifiRepositoryDagger::class)
+    abstract fun bindWifiRepositoryDagger(impl: WifiRepositoryDagger): CoreStartable
+
     companion object {
         @Provides
         @SysUISingleton
-        fun provideRealWifiRepository(
+        fun provideWifiRepositoryDagger(
             wifiManager: WifiManager?,
             disabledWifiRepository: DisabledWifiRepository,
             wifiRepositoryImplFactory: WifiRepositoryImpl.Factory,
-        ): RealWifiRepository {
+        ): WifiRepositoryDagger {
             // If we have a null [WifiManager], then the wifi repository should be permanently
             // disabled.
             return if (wifiManager == null) {
@@ -133,6 +143,36 @@
 
         @Provides
         @SysUISingleton
+        fun provideWifiRepositoryViaTrackerLibDagger(
+            wifiManager: WifiManager?,
+            disabledWifiRepository: DisabledWifiRepository,
+            wifiRepositoryFromTrackerLibFactory: WifiRepositoryViaTrackerLib.Factory,
+        ): WifiRepositoryViaTrackerLibDagger {
+            // If we have a null [WifiManager], then the wifi repository should be permanently
+            // disabled.
+            return if (wifiManager == null) {
+                disabledWifiRepository
+            } else {
+                wifiRepositoryFromTrackerLibFactory.create(wifiManager)
+            }
+        }
+
+        @Provides
+        @SysUISingleton
+        fun provideRealWifiRepository(
+            wifiRepository: WifiRepositoryDagger,
+            wifiRepositoryFromTrackerLib: WifiRepositoryViaTrackerLibDagger,
+            flags: FeatureFlags,
+        ): RealWifiRepository {
+            return if (flags.isEnabled(Flags.WIFI_TRACKER_LIB_FOR_WIFI_ICON)) {
+                wifiRepositoryFromTrackerLib
+            } else {
+                wifiRepository
+            }
+        }
+
+        @Provides
+        @SysUISingleton
         @Named(FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON)
         fun provideFirstMobileSubShowingNetworkTypeIconProvider(
             mobileIconsViewModel: MobileIconsViewModel,
@@ -151,6 +191,14 @@
 
         @Provides
         @SysUISingleton
+        @WifiTrackerLibInputLog
+        fun provideWifiTrackerLibInputLogBuffer(factory: LogBufferFactory): LogBuffer {
+            // WifiTrackerLib is pretty noisy, so give it more room than WifiInputLog.
+            return factory.create("WifiTrackerLibInputLog", 200)
+        }
+
+        @Provides
+        @SysUISingleton
         @WifiTableLog
         fun provideWifiTableLogBuffer(factory: TableLogBufferFactory): TableLogBuffer {
             return factory.create("WifiTableLog", 100)
@@ -158,6 +206,13 @@
 
         @Provides
         @SysUISingleton
+        @WifiTrackerLibTableLog
+        fun provideWifiTrackerLibTableLogBuffer(factory: TableLogBufferFactory): TableLogBuffer {
+            return factory.create("WifiTrackerLibTableLog", 100)
+        }
+
+        @Provides
+        @SysUISingleton
         @AirplaneTableLog
         fun provideAirplaneTableLogBuffer(factory: TableLogBufferFactory): TableLogBuffer {
             return factory.create("AirplaneTableLog", 30)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTrackerLibInputLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTrackerLibInputLog.kt
new file mode 100644
index 0000000..b84b01e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTrackerLibInputLog.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.statusbar.pipeline.dagger
+
+import javax.inject.Qualifier
+
+/** Wifi logs for inputs into [WifiRepositoryViaTrackerLib]. */
+@Qualifier
+@MustBeDocumented
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+annotation class WifiTrackerLibInputLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTrackerLibTableLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTrackerLibTableLog.kt
new file mode 100644
index 0000000..7ca7030
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTrackerLibTableLog.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.statusbar.pipeline.dagger
+
+import javax.inject.Qualifier
+
+/** Wifi logs from [WifiRepositoryViaTrackerLib] in table format. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class WifiTrackerLibTableLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
index f800cf4..b11b472 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.data.repository
 
+import com.android.systemui.CoreStartable
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import kotlinx.coroutines.flow.StateFlow
@@ -42,6 +43,13 @@
         val currentNetwork = wifiNetwork.value
         return currentNetwork is WifiNetworkModel.Active && currentNetwork.hasValidSsid()
     }
+
+    companion object {
+        /** Column name to use for [isWifiEnabled] for table logging. */
+        const val COL_NAME_IS_ENABLED = "isEnabled"
+        /** Column name to use for [isWifiDefault] for table logging. */
+        const val COL_NAME_IS_DEFAULT = "isDefault"
+    }
 }
 
 /**
@@ -53,3 +61,8 @@
  * repository.
  */
 interface RealWifiRepository : WifiRepository
+
+/** Used only by Dagger to bind [WifiRepositoryImpl]. */
+interface WifiRepositoryDagger : RealWifiRepository, CoreStartable
+/** Used only by Dagger to bind [WifiRepositoryViaTrackerLib]. */
+interface WifiRepositoryViaTrackerLibDagger : RealWifiRepository
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt
index 86a668a..9ed7c6a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt
@@ -18,7 +18,8 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryDagger
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryViaTrackerLibDagger
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -32,7 +33,10 @@
  * wifi information.
  */
 @SysUISingleton
-class DisabledWifiRepository @Inject constructor() : RealWifiRepository {
+class DisabledWifiRepository @Inject constructor() :
+    WifiRepositoryDagger, WifiRepositoryViaTrackerLibDagger {
+    override fun start() {}
+
     override val isWifiEnabled: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()
 
     override val isWifiDefault: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
index 7f35dfb..995de6d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
@@ -43,8 +43,10 @@
 import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl.Companion.getMainOrUnderlyingWifiInfo
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_DEFAULT
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_ENABLED
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryDagger
 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiInputLogger
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import java.util.concurrent.Executor
@@ -64,6 +66,7 @@
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
 /** Real implementation of [WifiRepository]. */
@@ -81,9 +84,21 @@
     @WifiTableLog wifiTableLogBuffer: TableLogBuffer,
     @Main mainExecutor: Executor,
     @Background private val bgDispatcher: CoroutineDispatcher,
-    @Application scope: CoroutineScope,
+    @Application private val scope: CoroutineScope,
     private val wifiManager: WifiManager,
-) : RealWifiRepository {
+) : WifiRepositoryDagger {
+
+    override fun start() {
+        // There are two possible [WifiRepository] implementations: This class (old) and
+        // [WifiRepositoryFromTrackerLib] (new). While we migrate to the new class, we want this old
+        // class to still be running in the background so that we can collect logs and compare
+        // discrepancies. This #start method collects on the flows to ensure that the logs are
+        // collected.
+        scope.launch { isWifiEnabled.collect {} }
+        scope.launch { isWifiDefault.collect {} }
+        scope.launch { wifiNetwork.collect {} }
+        scope.launch { wifiActivity.collect {} }
+    }
 
     private val wifiStateChangeEvents: Flow<Unit> =
         broadcastDispatcher
@@ -104,7 +119,7 @@
             .logDiffsForTable(
                 wifiTableLogBuffer,
                 columnPrefix = "",
-                columnName = "isEnabled",
+                columnName = COL_NAME_IS_ENABLED,
                 initialValue = false,
             )
             .stateIn(
@@ -125,7 +140,7 @@
             .logDiffsForTable(
                 wifiTableLogBuffer,
                 columnPrefix = "",
-                columnName = "isDefault",
+                columnName = COL_NAME_IS_DEFAULT,
                 initialValue = false,
             )
             .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
@@ -234,6 +249,8 @@
         // NetworkCallback inside [wifiNetwork] for our wifi network information.
         val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive
 
+        const val WIFI_STATE_DEFAULT = WifiManager.WIFI_STATE_DISABLED
+
         private fun createWifiNetworkModel(
             wifiInfo: WifiInfo,
             network: Network,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLib.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLib.kt
new file mode 100644
index 0000000..1271367
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLib.kt
@@ -0,0 +1,295 @@
+/*
+ * 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.statusbar.pipeline.wifi.data.repository.prod
+
+import android.net.wifi.WifiManager
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
+import com.android.systemui.statusbar.connectivity.WifiPickerTrackerFactory
+import com.android.systemui.statusbar.pipeline.dagger.WifiTrackerLibInputLog
+import com.android.systemui.statusbar.pipeline.dagger.WifiTrackerLibTableLog
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_DEFAULT
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_ENABLED
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryViaTrackerLibDagger
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl.Companion.WIFI_STATE_DEFAULT
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
+import com.android.wifitrackerlib.MergedCarrierEntry
+import com.android.wifitrackerlib.WifiEntry
+import com.android.wifitrackerlib.WifiPickerTracker
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * An implementation of [WifiRepository] that uses [com.android.wifitrackerlib] as the source of
+ * truth for wifi information.
+ *
+ * Serves as a possible replacement for [WifiRepositoryImpl]. See b/292534484.
+ */
+@SysUISingleton
+class WifiRepositoryViaTrackerLib
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    @Main private val mainExecutor: Executor,
+    private val wifiPickerTrackerFactory: WifiPickerTrackerFactory,
+    private val wifiManager: WifiManager,
+    @WifiTrackerLibInputLog private val inputLogger: LogBuffer,
+    @WifiTrackerLibTableLog private val wifiTrackerLibTableLogBuffer: TableLogBuffer,
+) : WifiRepositoryViaTrackerLibDagger, LifecycleOwner {
+
+    override val lifecycle =
+        LifecycleRegistry(this).also {
+            mainExecutor.execute { it.currentState = Lifecycle.State.CREATED }
+        }
+
+    private var wifiPickerTracker: WifiPickerTracker? = null
+
+    private val wifiPickerTrackerInfo: StateFlow<WifiPickerTrackerInfo> = run {
+        var current =
+            WifiPickerTrackerInfo(
+                state = WIFI_STATE_DEFAULT,
+                isDefault = false,
+                network = WIFI_NETWORK_DEFAULT,
+            )
+        callbackFlow {
+                val callback =
+                    object : WifiPickerTracker.WifiPickerTrackerCallback {
+                        override fun onWifiEntriesChanged() {
+                            val connectedEntry = wifiPickerTracker?.connectedWifiEntry
+                            logOnWifiEntriesChanged(connectedEntry)
+
+                            // [WifiPickerTracker.connectedWifiEntry] will return the same instance
+                            // but with updated internals. For example, when its validation status
+                            // changes from false to true, the same instance is re-used but with the
+                            // validated field updated.
+                            //
+                            // Because it's the same instance, the flow won't re-emit the value
+                            // (even though the internals have changed). So, we need to transform it
+                            // into our internal model immediately. [toWifiNetworkModel] always
+                            // returns a new instance, so the flow is guaranteed to emit.
+                            send(
+                                newNetwork = connectedEntry?.toWifiNetworkModel()
+                                        ?: WIFI_NETWORK_DEFAULT,
+                                newIsDefault = connectedEntry?.isDefaultNetwork ?: false,
+                            )
+                        }
+
+                        override fun onWifiStateChanged() {
+                            val state = wifiPickerTracker?.wifiState
+                            logOnWifiStateChanged(state)
+                            send(newState = state ?: WIFI_STATE_DEFAULT)
+                        }
+
+                        override fun onNumSavedNetworksChanged() {}
+
+                        override fun onNumSavedSubscriptionsChanged() {}
+
+                        private fun send(
+                            newState: Int = current.state,
+                            newIsDefault: Boolean = current.isDefault,
+                            newNetwork: WifiNetworkModel = current.network,
+                        ) {
+                            val new = WifiPickerTrackerInfo(newState, newIsDefault, newNetwork)
+                            current = new
+                            trySend(new)
+                        }
+                    }
+
+                // TODO(b/292591403): [WifiPickerTrackerFactory] currently scans to see all
+                // available wifi networks every 10s. Because SysUI only needs to display the
+                // **connected** network, we don't need scans to be running. We should disable these
+                // scans (ideal) or at least run them very infrequently.
+                wifiPickerTracker = wifiPickerTrackerFactory.create(lifecycle, callback)
+                // The lifecycle must be STARTED in order for the callback to receive events.
+                mainExecutor.execute { lifecycle.currentState = Lifecycle.State.STARTED }
+                awaitClose {
+                    mainExecutor.execute { lifecycle.currentState = Lifecycle.State.CREATED }
+                }
+            }
+            // TODO(b/292534484): Update to Eagerly once scans are disabled. (Here and other flows)
+            .stateIn(scope, SharingStarted.WhileSubscribed(), current)
+    }
+
+    override val isWifiEnabled: StateFlow<Boolean> =
+        wifiPickerTrackerInfo
+            .map { it.state == WifiManager.WIFI_STATE_ENABLED }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                wifiTrackerLibTableLogBuffer,
+                columnPrefix = "",
+                columnName = COL_NAME_IS_ENABLED,
+                initialValue = false,
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+    override val wifiNetwork: StateFlow<WifiNetworkModel> =
+        wifiPickerTrackerInfo
+            .map { it.network }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                wifiTrackerLibTableLogBuffer,
+                columnPrefix = "",
+                initialValue = WIFI_NETWORK_DEFAULT,
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), WIFI_NETWORK_DEFAULT)
+
+    /** Converts WifiTrackerLib's [WifiEntry] into our internal model. */
+    private fun WifiEntry.toWifiNetworkModel(): WifiNetworkModel {
+        if (!this.isPrimaryNetwork) {
+            return WIFI_NETWORK_DEFAULT
+        }
+        return if (this is MergedCarrierEntry) {
+            WifiNetworkModel.CarrierMerged(
+                networkId = NETWORK_ID,
+                // TODO(b/292534484): Fetch the real subscription ID from [MergedCarrierEntry].
+                subscriptionId = TEMP_SUB_ID,
+                level = this.level,
+                // WifiManager APIs to calculate the signal level start from 0, so
+                // maxSignalLevel + 1 represents the total level buckets count.
+                numberOfLevels = wifiManager.maxSignalLevel + 1,
+            )
+        } else {
+            WifiNetworkModel.Active(
+                networkId = NETWORK_ID,
+                isValidated = this.hasInternetAccess(),
+                level = this.level,
+                ssid = this.ssid,
+                // TODO(b/292534484): Fetch the real values from [WifiEntry] (#getTitle might be
+                // appropriate).
+                isPasspointAccessPoint = false,
+                isOnlineSignUpForPasspointAccessPoint = false,
+                passpointProviderFriendlyName = null,
+            )
+        }
+    }
+
+    override val isWifiDefault: StateFlow<Boolean> =
+        wifiPickerTrackerInfo
+            .map { it.isDefault }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                wifiTrackerLibTableLogBuffer,
+                columnPrefix = "",
+                columnName = COL_NAME_IS_DEFAULT,
+                initialValue = false,
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+    // TODO(b/292534484): Re-use WifiRepositoryImpl code to implement wifi activity since
+    // WifiTrackerLib doesn't expose activity details.
+    override val wifiActivity: StateFlow<DataActivityModel> =
+        MutableStateFlow(DataActivityModel(false, false))
+
+    private fun logOnWifiEntriesChanged(connectedEntry: WifiEntry?) {
+        inputLogger.log(
+            TAG,
+            LogLevel.DEBUG,
+            { str1 = connectedEntry.toString() },
+            { "onWifiEntriesChanged. ConnectedEntry=$str1" },
+        )
+    }
+
+    private fun logOnWifiStateChanged(state: Int?) {
+        inputLogger.log(
+            TAG,
+            LogLevel.DEBUG,
+            { int1 = state ?: -1 },
+            { "onWifiStateChanged. State=${if (int1 == -1) null else int1}" },
+        )
+    }
+
+    /**
+     * Data class storing all the information fetched from [WifiPickerTracker].
+     *
+     * Used so that we only register a single callback on [WifiPickerTracker].
+     */
+    data class WifiPickerTrackerInfo(
+        /** The current wifi state. See [WifiManager.getWifiState]. */
+        val state: Int,
+        /** True if wifi is currently the default connection and false otherwise. */
+        val isDefault: Boolean,
+        /** The currently primary wifi network. */
+        val network: WifiNetworkModel,
+    )
+
+    @SysUISingleton
+    class Factory
+    @Inject
+    constructor(
+        @Application private val scope: CoroutineScope,
+        @Main private val mainExecutor: Executor,
+        private val wifiPickerTrackerFactory: WifiPickerTrackerFactory,
+        @WifiTrackerLibInputLog private val inputLogger: LogBuffer,
+        @WifiTrackerLibTableLog private val wifiTrackerLibTableLogBuffer: TableLogBuffer,
+    ) {
+        fun create(wifiManager: WifiManager): WifiRepositoryViaTrackerLib {
+            return WifiRepositoryViaTrackerLib(
+                scope,
+                mainExecutor,
+                wifiPickerTrackerFactory,
+                wifiManager,
+                inputLogger,
+                wifiTrackerLibTableLogBuffer,
+            )
+        }
+    }
+
+    companion object {
+        private const val TAG = "WifiTrackerLibInputLog"
+
+        /**
+         * [WifiNetworkModel.Active.networkId] is only used at the repository layer. It's used by
+         * [WifiRepositoryImpl], which tracks the ID in order to correctly apply the framework
+         * callbacks within the repository.
+         *
+         * Since this class does not need to manually apply framework callbacks and since the
+         * network ID is not used beyond the repository, it's safe to use an invalid ID in this
+         * repository.
+         *
+         * The [WifiNetworkModel.Active.networkId] field should be deleted once we've fully migrated
+         * to [WifiRepositoryViaTrackerLib].
+         */
+        private const val NETWORK_ID = -1
+
+        /**
+         * A temporary subscription ID until WifiTrackerLib exposes a method to fetch the
+         * subscription ID.
+         *
+         * Use -2 because [SubscriptionManager.INVALID_SUBSCRIPTION_ID] is -1.
+         */
+        private const val TEMP_SUB_ID = -2
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
index e1a7b6d..c2a8e70 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
@@ -29,6 +29,7 @@
 import com.android.systemui.statusbar.connectivity.AccessPointControllerImpl;
 import com.android.systemui.statusbar.connectivity.NetworkController;
 import com.android.systemui.statusbar.connectivity.NetworkControllerImpl;
+import com.android.systemui.statusbar.connectivity.WifiPickerTrackerFactory;
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
 import com.android.systemui.statusbar.policy.BluetoothController;
 import com.android.systemui.statusbar.policy.BluetoothControllerImpl;
@@ -165,7 +166,7 @@
             UserManager userManager,
             UserTracker userTracker,
             @Main Executor mainExecutor,
-            AccessPointControllerImpl.WifiPickerTrackerFactory wifiPickerTrackerFactory
+            WifiPickerTrackerFactory wifiPickerTrackerFactory
     ) {
         AccessPointControllerImpl controller = new AccessPointControllerImpl(
                 userManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index a6ad4b2..a86937f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -89,6 +89,22 @@
             utils.authenticationRepository.setLockscreenEnabled(false)
 
             val isUnlocked by collectLastValue(underTest.isUnlocked)
+            // Toggle isUnlocked, twice.
+            //
+            // This is done because the underTest.isUnlocked flow doesn't receive values from
+            // just changing the state above; the actual isUnlocked state needs to change to
+            // cause the logic under test to "pick up" the current state again.
+            //
+            // It is done twice to make sure that we don't actually change the isUnlocked
+            // state from what it originally was.
+            utils.authenticationRepository.setUnlocked(
+                !utils.authenticationRepository.isUnlocked.value
+            )
+            runCurrent()
+            utils.authenticationRepository.setUnlocked(
+                !utils.authenticationRepository.isUnlocked.value
+            )
+            runCurrent()
             assertThat(isUnlocked).isTrue()
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
index 3169b09..2b08c66 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
@@ -840,6 +840,27 @@
 
             assertThat((lpFlags and PRIVATE_FLAG_TRUSTED_OVERLAY) != 0).isTrue()
         }
+
+    @Test
+    fun primaryBouncerRequestAnimatesAlphaIn() = testWithDisplay {
+        sideFpsController.show(SideFpsUiRequestSource.PRIMARY_BOUNCER, REASON_AUTH_KEYGUARD)
+        executor.runAllReady()
+        verify(sideFpsView).animate()
+    }
+
+    @Test
+    fun alternateBouncerRequestsDoesNotAnimateAlphaIn() = testWithDisplay {
+        sideFpsController.show(SideFpsUiRequestSource.ALTERNATE_BOUNCER, REASON_AUTH_KEYGUARD)
+        executor.runAllReady()
+        verify(sideFpsView, never()).animate()
+    }
+
+    @Test
+    fun autoShowRequestsDoesNotAnimateAlphaIn() = testWithDisplay {
+        sideFpsController.show(SideFpsUiRequestSource.AUTO_SHOW, REASON_AUTH_KEYGUARD)
+        executor.runAllReady()
+        verify(sideFpsView, never()).animate()
+    }
 }
 
 private fun insetsForSmallNavbar() = insetsWithBottom(60)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt
index 6e52d1a..baa5ee8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt
@@ -233,12 +233,12 @@
             // ERROR message
             fingerprintAuthRepository.setAuthenticationStatus(
                 ErrorFingerprintAuthenticationStatus(
-                    FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
+                    FingerprintManager.FINGERPRINT_ERROR_CANCELED,
                     "testError",
                 )
             )
             assertThat(message?.source).isEqualTo(BiometricSourceType.FINGERPRINT)
-            assertThat(message?.id).isEqualTo(FingerprintManager.FINGERPRINT_ERROR_LOCKOUT)
+            assertThat(message?.id).isEqualTo(FingerprintManager.FINGERPRINT_ERROR_CANCELED)
             assertThat(message?.message).isEqualTo("testError")
             assertThat(message?.type).isEqualTo(BiometricMessageType.ERROR)
 
@@ -262,6 +262,34 @@
             assertThat(message?.type).isEqualTo(BiometricMessageType.FAIL)
         }
 
+    @Test
+    fun message_fpError_lockoutFilteredOut() =
+        testScope.runTest {
+            val message by collectLastValue(underTest.message)
+
+            givenOnOccludingApp(true)
+            givenPrimaryAuthRequired(false)
+            runCurrent()
+
+            // permanent lockout error message
+            fingerprintAuthRepository.setAuthenticationStatus(
+                ErrorFingerprintAuthenticationStatus(
+                    FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT,
+                    "testPermanentLockoutMessageFiltered",
+                )
+            )
+            assertThat(message).isNull()
+
+            // temporary lockout error message
+            fingerprintAuthRepository.setAuthenticationStatus(
+                ErrorFingerprintAuthenticationStatus(
+                    FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
+                    "testLockoutMessageFiltered",
+                )
+            )
+            assertThat(message).isNull()
+        }
+
     private fun givenOnOccludingApp(isOnOccludingApp: Boolean) {
         keyguardRepository.setKeyguardOccluded(isOnOccludingApp)
         keyguardRepository.setKeyguardShowing(isOnOccludingApp)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
index bf25f29..2501f85 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
@@ -55,6 +55,7 @@
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
 import com.android.systemui.statusbar.phone.StatusBarIconController
 import com.android.systemui.statusbar.phone.StatusIconContainer
+import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory
 import com.android.systemui.statusbar.policy.Clock
 import com.android.systemui.statusbar.policy.FakeConfigurationController
 import com.android.systemui.statusbar.policy.NextAlarmController
@@ -123,6 +124,7 @@
     @Mock private lateinit var qsBatteryModeController: QsBatteryModeController
     @Mock private lateinit var nextAlarmController: NextAlarmController
     @Mock private lateinit var activityStarter: ActivityStarter
+    @Mock private lateinit var mStatusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory
 
     @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
     var viewVisibility = View.GONE
@@ -194,6 +196,7 @@
                 qsBatteryModeController,
                 nextAlarmController,
                 activityStarter,
+                mStatusOverlayHoverListenerFactory
             )
         whenever(view.isAttachedToWindow).thenReturn(true)
         shadeHeaderController.init()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt
index c57b64d..5b919d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt
@@ -23,6 +23,7 @@
 import androidx.lifecycle.Lifecycle
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.capture
 import com.android.wifitrackerlib.WifiEntry
 import com.android.wifitrackerlib.WifiPickerTracker
@@ -31,7 +32,6 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.anyList
 import org.mockito.Captor
 import org.mockito.Mock
@@ -52,7 +52,7 @@
     private lateinit var userTracker: UserTracker
     @Mock
     private lateinit var wifiPickerTrackerFactory:
-            AccessPointControllerImpl.WifiPickerTrackerFactory
+            WifiPickerTrackerFactory
     @Mock
     private lateinit var wifiPickerTracker: WifiPickerTracker
     @Mock
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 5300851..4872deb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -513,6 +513,7 @@
                 mNotificationShadeWindowViewControllerLazy,
                 mNotificationShelfController,
                 mStackScrollerController,
+                mNotificationPresenter,
                 mDozeParameters,
                 mScrimController,
                 mLockscreenWallpaperLazy,
@@ -590,7 +591,6 @@
         // TODO(b/277764509): we should be able to call mCentralSurfaces.start() and have all the
         //  below values initialized automatically.
         mCentralSurfaces.mDozeScrimController = mDozeScrimController;
-        mCentralSurfaces.mPresenter = mNotificationPresenter;
         mCentralSurfaces.mKeyguardIndicationController = mKeyguardIndicationController;
         mCentralSurfaces.mBarService = mBarService;
         mCentralSurfaces.mGestureWakeLock = mPowerManager.newWakeLock(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
index e838a480..d100c687 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
@@ -122,6 +122,7 @@
     @Mock private KeyguardLogger mLogger;
 
     @Mock private NotificationMediaManager mNotificationMediaManager;
+    @Mock private StatusOverlayHoverListenerFactory mStatusOverlayHoverListenerFactory;
 
     private TestShadeViewStateProvider mShadeViewStateProvider;
     private KeyguardStatusBarView mKeyguardStatusBarView;
@@ -171,7 +172,8 @@
                 mCommandQueue,
                 mFakeExecutor,
                 mLogger,
-                mNotificationMediaManager
+                mNotificationMediaManager,
+                mStatusOverlayHoverListenerFactory
         );
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
index 7de0075..2e92bb9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
@@ -69,6 +69,8 @@
     @Mock
     private lateinit var configurationController: ConfigurationController
     @Mock
+    private lateinit var mStatusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory
+    @Mock
     private lateinit var userChipViewModel: StatusBarUserChipViewModel
     @Mock
     private lateinit var centralSurfacesImpl: CentralSurfacesImpl
@@ -204,7 +206,8 @@
             sceneInteractor,
             shadeLogger,
             viewUtil,
-            configurationController
+            configurationController,
+            mStatusOverlayHoverListenerFactory
         ).create(view).also {
             it.init()
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListenerTest.kt
new file mode 100644
index 0000000..63508e1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListenerTest.kt
@@ -0,0 +1,192 @@
+/*
+ * 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.statusbar.phone
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.PaintDrawable
+import android.os.SystemClock
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import android.testing.ViewUtils
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroupOverlay
+import android.widget.LinearLayout
+import androidx.annotation.ColorInt
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange
+import com.android.systemui.statusbar.policy.FakeConfigurationController
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper(setAsMainLooper = true)
+@SmallTest
+class StatusOverlayHoverListenerTest : SysuiTestCase() {
+
+    private val viewOverlay = mock<ViewGroupOverlay>()
+    private val overlayCaptor = argumentCaptor<Drawable>()
+    private val darkDispatcher = mock<SysuiDarkIconDispatcher>()
+    private val darkChange: MutableStateFlow<DarkChange> = MutableStateFlow(DarkChange.EMPTY)
+
+    private val factory =
+        StatusOverlayHoverListenerFactory(
+            context.resources,
+            FakeConfigurationController(),
+            darkDispatcher
+        )
+    private val view = TestableStatusContainer(context, viewOverlay)
+
+    private lateinit var looper: TestableLooper
+
+    @Before
+    fun setUp() {
+        looper = TestableLooper.get(this)
+        whenever(darkDispatcher.darkChangeFlow()).thenReturn(darkChange)
+    }
+
+    @Test
+    fun onHoverStarted_addsOverlay() {
+        view.setUpHoverListener()
+
+        view.hoverStarted()
+
+        assertThat(overlayDrawable).isNotNull()
+    }
+
+    @Test
+    fun onHoverEnded_removesOverlay() {
+        view.setUpHoverListener()
+
+        view.hoverStarted() // stopped callback will be called only if hover has started
+        view.hoverStopped()
+
+        verify(viewOverlay).clear()
+    }
+
+    @Test
+    fun onHoverStarted_overlayHasLightColor() {
+        view.setUpHoverListener()
+
+        view.hoverStarted()
+
+        assertThat(overlayColor)
+            .isEqualTo(context.resources.getColor(R.color.status_bar_icons_hover_color_light))
+    }
+
+    @Test
+    fun onDarkAwareHoverStarted_withBlackIcons_overlayHasDarkColor() {
+        view.setUpDarkAwareHoverListener()
+        setIconsTint(Color.BLACK)
+
+        view.hoverStarted()
+
+        assertThat(overlayColor)
+            .isEqualTo(context.resources.getColor(R.color.status_bar_icons_hover_color_dark))
+    }
+
+    @Test
+    fun onHoverStarted_withBlackIcons_overlayHasLightColor() {
+        view.setUpHoverListener()
+        setIconsTint(Color.BLACK)
+
+        view.hoverStarted()
+
+        assertThat(overlayColor)
+            .isEqualTo(context.resources.getColor(R.color.status_bar_icons_hover_color_light))
+    }
+
+    @Test
+    fun onDarkAwareHoverStarted_withWhiteIcons_overlayHasLightColor() {
+        view.setUpDarkAwareHoverListener()
+        setIconsTint(Color.WHITE)
+
+        view.hoverStarted()
+
+        assertThat(overlayColor)
+            .isEqualTo(context.resources.getColor(R.color.status_bar_icons_hover_color_light))
+    }
+
+    private fun View.setUpHoverListener() {
+        setOnHoverListener(factory.createListener(view))
+        attachView(view)
+    }
+
+    private fun View.setUpDarkAwareHoverListener() {
+        setOnHoverListener(factory.createDarkAwareListener(view))
+        attachView(view)
+    }
+
+    private fun attachView(view: View) {
+        ViewUtils.attachView(view)
+        // attaching is async so processAllMessages is required for view.repeatWhenAttached to run
+        looper.processAllMessages()
+    }
+
+    private val overlayDrawable: Drawable
+        get() {
+            verify(viewOverlay).add(overlayCaptor.capture())
+            return overlayCaptor.value
+        }
+
+    private val overlayColor
+        get() = (overlayDrawable as PaintDrawable).paint.color
+
+    private fun setIconsTint(@ColorInt color: Int) {
+        // passing empty ArrayList is equivalent to just accepting passed color as icons color
+        darkChange.value = DarkChange(/* areas= */ ArrayList(), /* darkIntensity= */ 1f, color)
+    }
+
+    private fun TestableStatusContainer.hoverStarted() {
+        injectHoverEvent(hoverEvent(MotionEvent.ACTION_HOVER_ENTER))
+    }
+
+    private fun TestableStatusContainer.hoverStopped() {
+        injectHoverEvent(hoverEvent(MotionEvent.ACTION_HOVER_EXIT))
+    }
+
+    class TestableStatusContainer(context: Context, private val mockOverlay: ViewGroupOverlay) :
+        LinearLayout(context) {
+
+        fun injectHoverEvent(event: MotionEvent) = dispatchHoverEvent(event)
+
+        override fun getOverlay() = mockOverlay
+    }
+
+    private fun hoverEvent(action: Int): MotionEvent {
+        return MotionEvent.obtain(
+            /* downTime= */ SystemClock.uptimeMillis(),
+            /* eventTime= */ SystemClock.uptimeMillis(),
+            /* action= */ action,
+            /* x= */ 0f,
+            /* y= */ 0f,
+            /* metaState= */ 0
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
index 3cf5f52..fef042b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
@@ -36,6 +36,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
@@ -48,23 +49,25 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.Executor
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
+/**
+ * Note: Any new tests added here may also need to be added to [WifiRepositoryViaTrackerLibTest].
+ */
 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -81,7 +84,7 @@
     private lateinit var executor: Executor
     private lateinit var connectivityRepository: ConnectivityRepository
 
-    private val dispatcher = UnconfinedTestDispatcher()
+    private val dispatcher = StandardTestDispatcher()
     private val testScope = TestScope(dispatcher)
 
     @Before
@@ -109,6 +112,7 @@
             whenever(wifiManager.isWifiEnabled).thenReturn(true)
 
             underTest = createRepo()
+            testScope.runCurrent()
 
             assertThat(underTest.isWifiEnabled.value).isTrue()
         }
@@ -116,53 +120,43 @@
     @Test
     fun isWifiEnabled_networkCapabilitiesChanged_valueUpdated() =
         testScope.runTest {
-            // We need to call launch on the flows so that they start updating
-            val networkJob = underTest.wifiNetwork.launchIn(this)
-            val enabledJob = underTest.isWifiEnabled.launchIn(this)
+            collectLastValue(underTest.wifiNetwork)
+            val latest by collectLastValue(underTest.isWifiEnabled)
 
             whenever(wifiManager.isWifiEnabled).thenReturn(true)
             getNetworkCallback()
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
 
-            assertThat(underTest.isWifiEnabled.value).isTrue()
+            assertThat(latest).isTrue()
 
             whenever(wifiManager.isWifiEnabled).thenReturn(false)
             getNetworkCallback()
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
 
-            assertThat(underTest.isWifiEnabled.value).isFalse()
-
-            networkJob.cancel()
-            enabledJob.cancel()
+            assertThat(latest).isFalse()
         }
 
     @Test
     fun isWifiEnabled_networkLost_valueUpdated() =
         testScope.runTest {
-            // We need to call launch on the flows so that they start updating
-            val networkJob = underTest.wifiNetwork.launchIn(this)
-            val enabledJob = underTest.isWifiEnabled.launchIn(this)
+            collectLastValue(underTest.wifiNetwork)
+            val latest by collectLastValue(underTest.isWifiEnabled)
 
             whenever(wifiManager.isWifiEnabled).thenReturn(true)
             getNetworkCallback().onLost(NETWORK)
 
-            assertThat(underTest.isWifiEnabled.value).isTrue()
+            assertThat(latest).isTrue()
 
             whenever(wifiManager.isWifiEnabled).thenReturn(false)
             getNetworkCallback().onLost(NETWORK)
 
-            assertThat(underTest.isWifiEnabled.value).isFalse()
-
-            networkJob.cancel()
-            enabledJob.cancel()
+            assertThat(latest).isFalse()
         }
 
     @Test
     fun isWifiEnabled_intentsReceived_valueUpdated() =
         testScope.runTest {
-            underTest = createRepo()
-
-            val job = underTest.isWifiEnabled.launchIn(this)
+            val latest by collectLastValue(underTest.isWifiEnabled)
 
             whenever(wifiManager.isWifiEnabled).thenReturn(true)
             fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
@@ -170,7 +164,7 @@
                 Intent(WifiManager.WIFI_STATE_CHANGED_ACTION),
             )
 
-            assertThat(underTest.isWifiEnabled.value).isTrue()
+            assertThat(latest).isTrue()
 
             whenever(wifiManager.isWifiEnabled).thenReturn(false)
             fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
@@ -178,76 +172,61 @@
                 Intent(WifiManager.WIFI_STATE_CHANGED_ACTION),
             )
 
-            assertThat(underTest.isWifiEnabled.value).isFalse()
-
-            job.cancel()
+            assertThat(latest).isFalse()
         }
 
     @Test
     fun isWifiEnabled_bothIntentAndNetworkUpdates_valueAlwaysUpdated() =
         testScope.runTest {
-            underTest = createRepo()
-
-            val networkJob = underTest.wifiNetwork.launchIn(this)
-            val enabledJob = underTest.isWifiEnabled.launchIn(this)
+            collectLastValue(underTest.wifiNetwork)
+            val latest by collectLastValue(underTest.isWifiEnabled)
 
             whenever(wifiManager.isWifiEnabled).thenReturn(false)
             fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
                 context,
                 Intent(WifiManager.WIFI_STATE_CHANGED_ACTION),
             )
-            assertThat(underTest.isWifiEnabled.value).isFalse()
+            assertThat(latest).isFalse()
 
             whenever(wifiManager.isWifiEnabled).thenReturn(true)
             getNetworkCallback().onLost(NETWORK)
-            assertThat(underTest.isWifiEnabled.value).isTrue()
+            assertThat(latest).isTrue()
 
             whenever(wifiManager.isWifiEnabled).thenReturn(false)
             getNetworkCallback()
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
-            assertThat(underTest.isWifiEnabled.value).isFalse()
+            assertThat(latest).isFalse()
 
             whenever(wifiManager.isWifiEnabled).thenReturn(true)
             fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
                 context,
                 Intent(WifiManager.WIFI_STATE_CHANGED_ACTION),
             )
-            assertThat(underTest.isWifiEnabled.value).isTrue()
-
-            networkJob.cancel()
-            enabledJob.cancel()
+            assertThat(latest).isTrue()
         }
 
     @Test
     fun isWifiDefault_initiallyGetsDefault() =
-        testScope.runTest {
-            val job = underTest.isWifiDefault.launchIn(this)
-
-            assertThat(underTest.isWifiDefault.value).isFalse()
-
-            job.cancel()
-        }
+        testScope.runTest { assertThat(underTest.isWifiDefault.value).isFalse() }
 
     @Test
     fun isWifiDefault_wifiNetwork_isTrue() =
         testScope.runTest {
-            val job = underTest.isWifiDefault.launchIn(this)
+            val latest by collectLastValue(underTest.isWifiDefault)
 
             val wifiInfo = mock<WifiInfo>().apply { whenever(this.ssid).thenReturn(SSID) }
 
             getDefaultNetworkCallback()
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
 
-            assertThat(underTest.isWifiDefault.value).isTrue()
-
-            job.cancel()
+            assertThat(latest).isTrue()
         }
 
     /** Regression test for b/266628069. */
     @Test
     fun isWifiDefault_transportInfoIsNotWifi_andNoWifiTransport_false() =
         testScope.runTest {
-            val job = underTest.isWifiDefault.launchIn(this)
+            val latest by collectLastValue(underTest.isWifiDefault)
 
             val transportInfo =
                 VpnTransportInfo(
@@ -264,16 +243,14 @@
 
             getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, networkCapabilities)
 
-            assertThat(underTest.isWifiDefault.value).isFalse()
-
-            job.cancel()
+            assertThat(latest).isFalse()
         }
 
     /** Regression test for b/266628069. */
     @Test
     fun isWifiDefault_transportInfoIsNotWifi_butHasWifiTransport_true() =
         testScope.runTest {
-            val job = underTest.isWifiDefault.launchIn(this)
+            val latest by collectLastValue(underTest.isWifiDefault)
 
             val transportInfo =
                 VpnTransportInfo(
@@ -290,15 +267,13 @@
 
             getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, networkCapabilities)
 
-            assertThat(underTest.isWifiDefault.value).isTrue()
-
-            job.cancel()
+            assertThat(latest).isTrue()
         }
 
     @Test
     fun isWifiDefault_carrierMergedViaCellular_isTrue() =
         testScope.runTest {
-            val job = underTest.isWifiDefault.launchIn(this)
+            val latest by collectLastValue(underTest.isWifiDefault)
 
             val carrierMergedInfo =
                 mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) }
@@ -312,15 +287,13 @@
 
             getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
 
-            assertThat(underTest.isWifiDefault.value).isTrue()
-
-            job.cancel()
+            assertThat(latest).isTrue()
         }
 
     @Test
     fun isWifiDefault_carrierMergedViaCellular_withVcnTransport_isTrue() =
         testScope.runTest {
-            val job = underTest.isWifiDefault.launchIn(this)
+            val latest by collectLastValue(underTest.isWifiDefault)
 
             val capabilities =
                 mock<NetworkCapabilities>().apply {
@@ -331,15 +304,13 @@
 
             getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
 
-            assertThat(underTest.isWifiDefault.value).isTrue()
-
-            job.cancel()
+            assertThat(latest).isTrue()
         }
 
     @Test
     fun isWifiDefault_carrierMergedViaWifi_isTrue() =
         testScope.runTest {
-            val job = underTest.isWifiDefault.launchIn(this)
+            val latest by collectLastValue(underTest.isWifiDefault)
 
             val carrierMergedInfo =
                 mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) }
@@ -353,15 +324,13 @@
 
             getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
 
-            assertThat(underTest.isWifiDefault.value).isTrue()
-
-            job.cancel()
+            assertThat(latest).isTrue()
         }
 
     @Test
     fun isWifiDefault_carrierMergedViaWifi_withVcnTransport_isTrue() =
         testScope.runTest {
-            val job = underTest.isWifiDefault.launchIn(this)
+            val latest by collectLastValue(underTest.isWifiDefault)
 
             val capabilities =
                 mock<NetworkCapabilities>().apply {
@@ -372,15 +341,13 @@
 
             getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
 
-            assertThat(underTest.isWifiDefault.value).isTrue()
-
-            job.cancel()
+            assertThat(latest).isTrue()
         }
 
     @Test
     fun isWifiDefault_cellularAndWifiTransports_usesCellular_isTrue() =
         testScope.runTest {
-            val job = underTest.isWifiDefault.launchIn(this)
+            val latest by collectLastValue(underTest.isWifiDefault)
 
             val capabilities =
                 mock<NetworkCapabilities>().apply {
@@ -391,15 +358,13 @@
 
             getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
 
-            assertThat(underTest.isWifiDefault.value).isTrue()
-
-            job.cancel()
+            assertThat(latest).isTrue()
         }
 
     @Test
     fun isWifiDefault_cellularNotVcnNetwork_isFalse() =
         testScope.runTest {
-            val job = underTest.isWifiDefault.launchIn(this)
+            val latest by collectLastValue(underTest.isWifiDefault)
 
             val capabilities =
                 mock<NetworkCapabilities>().apply {
@@ -409,15 +374,13 @@
 
             getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
 
-            assertThat(underTest.isWifiDefault.value).isFalse()
-
-            job.cancel()
+            assertThat(latest).isFalse()
         }
 
     @Test
     fun isWifiDefault_isCarrierMergedViaUnderlyingWifi_isTrue() =
         testScope.runTest {
-            val job = underTest.isWifiDefault.launchIn(this)
+            val latest by collectLastValue(underTest.isWifiDefault)
 
             val underlyingNetwork = mock<Network>()
             val carrierMergedInfo =
@@ -444,15 +407,13 @@
             getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities)
 
             // THEN the wifi network is carrier merged, so wifi is default
-            assertThat(underTest.isWifiDefault.value).isTrue()
-
-            job.cancel()
+            assertThat(latest).isTrue()
         }
 
     @Test
     fun isWifiDefault_isCarrierMergedViaUnderlyingCellular_isTrue() =
         testScope.runTest {
-            val job = underTest.isWifiDefault.launchIn(this)
+            val latest by collectLastValue(underTest.isWifiDefault)
 
             val underlyingCarrierMergedNetwork = mock<Network>()
             val carrierMergedInfo =
@@ -478,46 +439,38 @@
             getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities)
 
             // THEN the wifi network is carrier merged, so wifi is default
-            assertThat(underTest.isWifiDefault.value).isTrue()
-
-            job.cancel()
+            assertThat(latest).isTrue()
         }
 
     @Test
     fun isWifiDefault_wifiNetworkLost_isFalse() =
         testScope.runTest {
-            val job = underTest.isWifiDefault.launchIn(this)
+            val latest by collectLastValue(underTest.isWifiDefault)
 
             // First, add a network
             getDefaultNetworkCallback()
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
-            assertThat(underTest.isWifiDefault.value).isTrue()
+            assertThat(latest).isTrue()
 
             // WHEN the network is lost
             getDefaultNetworkCallback().onLost(NETWORK)
 
             // THEN we update to false
-            assertThat(underTest.isWifiDefault.value).isFalse()
-
-            job.cancel()
+            assertThat(latest).isFalse()
         }
 
     @Test
     fun wifiNetwork_initiallyGetsDefault() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             assertThat(latest).isEqualTo(WIFI_NETWORK_DEFAULT)
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_primaryWifiNetworkAdded_flowHasNetwork() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             val wifiInfo =
                 mock<WifiInfo>().apply {
@@ -533,15 +486,12 @@
             val latestActive = latest as WifiNetworkModel.Active
             assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
             assertThat(latestActive.ssid).isEqualTo(SSID)
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_isCarrierMerged_flowHasCarrierMerged() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             val wifiInfo =
                 mock<WifiInfo>().apply {
@@ -553,15 +503,12 @@
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
 
             assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_isCarrierMergedViaUnderlyingWifi_flowHasCarrierMerged() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             val underlyingNetwork = mock<Network>()
             val carrierMergedInfo =
@@ -590,15 +537,12 @@
 
             // THEN the wifi network is carrier merged
             assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_isCarrierMergedViaUnderlyingCellular_flowHasCarrierMerged() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             val underlyingCarrierMergedNetwork = mock<Network>()
             val carrierMergedInfo =
@@ -628,15 +572,12 @@
 
             // THEN the wifi network is carrier merged
             assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_carrierMergedButInvalidSubId_flowHasInvalid() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             val wifiInfo =
                 mock<WifiInfo>().apply {
@@ -652,15 +593,12 @@
                 )
 
             assertThat(latest).isInstanceOf(WifiNetworkModel.Invalid::class.java)
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_isCarrierMerged_getsCorrectValues() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             val rssi = -57
             val wifiInfo =
@@ -687,15 +625,12 @@
             assertThat(latestCarrierMerged.level).isEqualTo(2)
             // numberOfLevels = maxSignalLevel + 1
             assertThat(latestCarrierMerged.numberOfLevels).isEqualTo(6)
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_notValidated_networkNotValidated() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             getNetworkCallback()
                 .onCapabilitiesChanged(
@@ -704,15 +639,12 @@
                 )
 
             assertThat((latest as WifiNetworkModel.Active).isValidated).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_validated_networkValidated() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             getNetworkCallback()
                 .onCapabilitiesChanged(
@@ -721,15 +653,12 @@
                 )
 
             assertThat((latest as WifiNetworkModel.Active).isValidated).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_nonPrimaryWifiNetworkAdded_flowHasNoNetwork() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             val wifiInfo =
                 mock<WifiInfo>().apply {
@@ -741,16 +670,13 @@
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
 
             assertThat(latest is WifiNetworkModel.Inactive).isTrue()
-
-            job.cancel()
         }
 
     /** Regression test for b/266628069. */
     @Test
     fun wifiNetwork_transportInfoIsNotWifi_flowHasNoNetwork() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             val transportInfo =
                 VpnTransportInfo(
@@ -761,15 +687,12 @@
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(transportInfo))
 
             assertThat(latest is WifiNetworkModel.Inactive).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_cellularVcnNetworkAdded_flowHasNetwork() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             val capabilities =
                 mock<NetworkCapabilities>().apply {
@@ -783,15 +706,12 @@
             val latestActive = latest as WifiNetworkModel.Active
             assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
             assertThat(latestActive.ssid).isEqualTo(SSID)
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_nonPrimaryCellularVcnNetworkAdded_flowHasNoNetwork() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             val wifiInfo =
                 mock<WifiInfo>().apply {
@@ -807,15 +727,12 @@
             getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
 
             assertThat(latest is WifiNetworkModel.Inactive).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_cellularNotVcnNetworkAdded_flowHasNoNetwork() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             val capabilities =
                 mock<NetworkCapabilities>().apply {
@@ -826,15 +743,12 @@
             getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
 
             assertThat(latest is WifiNetworkModel.Inactive).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_cellularAndWifiTransports_usesCellular() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             val capabilities =
                 mock<NetworkCapabilities>().apply {
@@ -849,15 +763,12 @@
             val latestActive = latest as WifiNetworkModel.Active
             assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
             assertThat(latestActive.ssid).isEqualTo(SSID)
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_newPrimaryWifiNetwork_flowHasNewNetwork() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             // Start with the original network
             getNetworkCallback()
@@ -882,15 +793,12 @@
             val latestActive = latest as WifiNetworkModel.Active
             assertThat(latestActive.networkId).isEqualTo(newNetworkId)
             assertThat(latestActive.ssid).isEqualTo(newSsid)
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_newNonPrimaryWifiNetwork_flowHasOldNetwork() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             // Start with the original network
             getNetworkCallback()
@@ -915,15 +823,12 @@
             val latestActive = latest as WifiNetworkModel.Active
             assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
             assertThat(latestActive.ssid).isEqualTo(SSID)
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_newNetworkCapabilities_flowHasNewData() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             val wifiInfo =
                 mock<WifiInfo>().apply {
@@ -958,30 +863,24 @@
             assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
             assertThat(latestActive.ssid).isEqualTo(newSsid)
             assertThat(latestActive.isValidated).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_noCurrentNetwork_networkLost_flowHasNoNetwork() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             // WHEN we receive #onLost without any #onCapabilitiesChanged beforehand
             getNetworkCallback().onLost(NETWORK)
 
             // THEN there's no crash and we still have no network
             assertThat(latest is WifiNetworkModel.Inactive).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_currentActiveNetworkLost_flowHasNoNetwork() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             getNetworkCallback()
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
@@ -992,16 +891,13 @@
 
             // THEN we update to no network
             assertThat(latest is WifiNetworkModel.Inactive).isTrue()
-
-            job.cancel()
         }
 
     /** Possible regression test for b/278618530. */
     @Test
     fun wifiNetwork_currentCarrierMergedNetworkLost_flowHasNoNetwork() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             val wifiInfo =
                 mock<WifiInfo>().apply {
@@ -1019,15 +915,12 @@
 
             // THEN we update to no network
             assertThat(latest is WifiNetworkModel.Inactive).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_unknownNetworkLost_flowHasPreviousNetwork() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             getNetworkCallback()
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
@@ -1042,15 +935,12 @@
             val latestActive = latest as WifiNetworkModel.Active
             assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
             assertThat(latestActive.ssid).isEqualTo(SSID)
-
-            job.cancel()
         }
 
     @Test
     fun wifiNetwork_notCurrentNetworkLost_flowHasCurrentNetwork() =
         testScope.runTest {
-            var latest: WifiNetworkModel? = null
-            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiNetwork)
 
             getNetworkCallback()
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
@@ -1067,16 +957,13 @@
 
             // THEN we still have the new network
             assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(newNetworkId)
-
-            job.cancel()
         }
 
     /** Regression test for b/244173280. */
     @Test
     fun wifiNetwork_multipleSubscribers_newSubscribersGetCurrentValue() =
         testScope.runTest {
-            var latest1: WifiNetworkModel? = null
-            val job1 = underTest.wifiNetwork.onEach { latest1 = it }.launchIn(this)
+            val latest1 by collectLastValue(underTest.wifiNetwork)
 
             getNetworkCallback()
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
@@ -1087,23 +974,19 @@
             assertThat(latest1Active.ssid).isEqualTo(SSID)
 
             // WHEN we add a second subscriber after having already emitted a value
-            var latest2: WifiNetworkModel? = null
-            val job2 = underTest.wifiNetwork.onEach { latest2 = it }.launchIn(this)
+            val latest2 by collectLastValue(underTest.wifiNetwork)
 
             // THEN the second subscribe receives the already-emitted value
             assertThat(latest2 is WifiNetworkModel.Active).isTrue()
             val latest2Active = latest2 as WifiNetworkModel.Active
             assertThat(latest2Active.networkId).isEqualTo(NETWORK_ID)
             assertThat(latest2Active.ssid).isEqualTo(SSID)
-
-            job1.cancel()
-            job2.cancel()
         }
 
     @Test
     fun isWifiConnectedWithValidSsid_inactiveNetwork_false() =
         testScope.runTest {
-            val job = underTest.wifiNetwork.launchIn(this)
+            collectLastValue(underTest.wifiNetwork)
 
             val wifiInfo =
                 mock<WifiInfo>().apply {
@@ -1116,14 +999,12 @@
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
 
             assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun isWifiConnectedWithValidSsid_carrierMergedNetwork_false() =
         testScope.runTest {
-            val job = underTest.wifiNetwork.launchIn(this)
+            collectLastValue(underTest.wifiNetwork)
 
             val wifiInfo =
                 mock<WifiInfo>().apply {
@@ -1133,16 +1014,15 @@
 
             getNetworkCallback()
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
+            testScope.runCurrent()
 
             assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun isWifiConnectedWithValidSsid_invalidNetwork_false() =
         testScope.runTest {
-            val job = underTest.wifiNetwork.launchIn(this)
+            collectLastValue(underTest.wifiNetwork)
 
             val wifiInfo =
                 mock<WifiInfo>().apply {
@@ -1156,16 +1036,15 @@
                     NETWORK,
                     createWifiNetworkCapabilities(wifiInfo),
                 )
+            testScope.runCurrent()
 
             assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun isWifiConnectedWithValidSsid_activeNetwork_nullSsid_false() =
         testScope.runTest {
-            val job = underTest.wifiNetwork.launchIn(this)
+            collectLastValue(underTest.wifiNetwork)
 
             val wifiInfo =
                 mock<WifiInfo>().apply {
@@ -1175,16 +1054,15 @@
 
             getNetworkCallback()
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
+            testScope.runCurrent()
 
             assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun isWifiConnectedWithValidSsid_activeNetwork_unknownSsid_false() =
         testScope.runTest {
-            val job = underTest.wifiNetwork.launchIn(this)
+            collectLastValue(underTest.wifiNetwork)
 
             val wifiInfo =
                 mock<WifiInfo>().apply {
@@ -1194,16 +1072,15 @@
 
             getNetworkCallback()
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
+            testScope.runCurrent()
 
             assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun isWifiConnectedWithValidSsid_activeNetwork_validSsid_true() =
         testScope.runTest {
-            val job = underTest.wifiNetwork.launchIn(this)
+            collectLastValue(underTest.wifiNetwork)
 
             val wifiInfo =
                 mock<WifiInfo>().apply {
@@ -1213,16 +1090,15 @@
 
             getNetworkCallback()
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
+            testScope.runCurrent()
 
             assertThat(underTest.isWifiConnectedWithValidSsid()).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun isWifiConnectedWithValidSsid_activeToInactive_trueToFalse() =
         testScope.runTest {
-            val job = underTest.wifiNetwork.launchIn(this)
+            collectLastValue(underTest.wifiNetwork)
 
             // Start with active
             val wifiInfo =
@@ -1232,71 +1108,60 @@
                 }
             getNetworkCallback()
                 .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
+            testScope.runCurrent()
+
             assertThat(underTest.isWifiConnectedWithValidSsid()).isTrue()
 
             // WHEN the network is lost
             getNetworkCallback().onLost(NETWORK)
+            testScope.runCurrent()
 
             // THEN the isWifiConnected updates
             assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun wifiActivity_callbackGivesNone_activityFlowHasNone() =
         testScope.runTest {
-            var latest: DataActivityModel? = null
-            val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiActivity)
 
             getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_NONE)
 
             assertThat(latest)
                 .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
-
-            job.cancel()
         }
 
     @Test
     fun wifiActivity_callbackGivesIn_activityFlowHasIn() =
         testScope.runTest {
-            var latest: DataActivityModel? = null
-            val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiActivity)
 
             getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_IN)
 
             assertThat(latest)
                 .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = false))
-
-            job.cancel()
         }
 
     @Test
     fun wifiActivity_callbackGivesOut_activityFlowHasOut() =
         testScope.runTest {
-            var latest: DataActivityModel? = null
-            val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiActivity)
 
             getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_OUT)
 
             assertThat(latest)
                 .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = true))
-
-            job.cancel()
         }
 
     @Test
     fun wifiActivity_callbackGivesInout_activityFlowHasInAndOut() =
         testScope.runTest {
-            var latest: DataActivityModel? = null
-            val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.wifiActivity)
 
             getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_INOUT)
 
             assertThat(latest)
                 .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = true))
-
-            job.cancel()
         }
 
     private fun createRepo(): WifiRepositoryImpl {
@@ -1314,18 +1179,21 @@
     }
 
     private fun getTrafficStateCallback(): TrafficStateCallback {
+        testScope.runCurrent()
         val callbackCaptor = argumentCaptor<TrafficStateCallback>()
         verify(wifiManager).registerTrafficStateCallback(any(), callbackCaptor.capture())
         return callbackCaptor.value!!
     }
 
     private fun getNetworkCallback(): ConnectivityManager.NetworkCallback {
+        testScope.runCurrent()
         val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>()
         verify(connectivityManager).registerNetworkCallback(any(), callbackCaptor.capture())
         return callbackCaptor.value!!
     }
 
     private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback {
+        testScope.runCurrent()
         val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>()
         verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture())
         return callbackCaptor.value!!
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLibTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLibTest.kt
new file mode 100644
index 0000000..7002cbb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLibTest.kt
@@ -0,0 +1,665 @@
+/*
+ * 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.statusbar.pipeline.wifi.data.repository.prod
+
+import android.net.wifi.WifiManager
+import android.net.wifi.WifiManager.UNKNOWN_SSID
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.connectivity.WifiPickerTrackerFactory
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.android.wifitrackerlib.MergedCarrierEntry
+import com.android.wifitrackerlib.WifiEntry
+import com.android.wifitrackerlib.WifiPickerTracker
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * Note: Most of these tests are duplicates of [WifiRepositoryImplTest] tests.
+ *
+ * Any new tests added here may also need to be added to [WifiRepositoryImplTest].
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class WifiRepositoryViaTrackerLibTest : SysuiTestCase() {
+
+    private lateinit var underTest: WifiRepositoryViaTrackerLib
+
+    private val executor = FakeExecutor(FakeSystemClock())
+    private val logger = LogBuffer("name", maxSize = 100, logcatEchoTracker = mock())
+    private val tableLogger = mock<TableLogBuffer>()
+    private val wifiManager =
+        mock<WifiManager>().apply { whenever(this.maxSignalLevel).thenReturn(10) }
+    private val wifiPickerTrackerFactory = mock<WifiPickerTrackerFactory>()
+    private val wifiPickerTracker = mock<WifiPickerTracker>()
+
+    private val callbackCaptor = argumentCaptor<WifiPickerTracker.WifiPickerTrackerCallback>()
+
+    private val dispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+
+    @Before
+    fun setUp() {
+        whenever(wifiPickerTrackerFactory.create(any(), capture(callbackCaptor)))
+            .thenReturn(wifiPickerTracker)
+        underTest = createRepo()
+    }
+
+    @Test
+    fun isWifiEnabled_enabled_true() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isWifiEnabled)
+
+            whenever(wifiPickerTracker.wifiState).thenReturn(WifiManager.WIFI_STATE_ENABLED)
+            getCallback().onWifiStateChanged()
+
+            assertThat(latest).isTrue()
+        }
+
+    @Test
+    fun isWifiEnabled_enabling_false() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isWifiEnabled)
+
+            whenever(wifiPickerTracker.wifiState).thenReturn(WifiManager.WIFI_STATE_ENABLING)
+            getCallback().onWifiStateChanged()
+
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun isWifiEnabled_disabling_true() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isWifiEnabled)
+
+            whenever(wifiPickerTracker.wifiState).thenReturn(WifiManager.WIFI_STATE_DISABLING)
+            getCallback().onWifiStateChanged()
+
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun isWifiEnabled_disabled_false() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isWifiEnabled)
+
+            whenever(wifiPickerTracker.wifiState).thenReturn(WifiManager.WIFI_STATE_DISABLED)
+            getCallback().onWifiStateChanged()
+
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun isWifiEnabled_respondsToUpdates() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isWifiEnabled)
+            executor.runAllReady()
+
+            whenever(wifiPickerTracker.wifiState).thenReturn(WifiManager.WIFI_STATE_ENABLED)
+            getCallback().onWifiStateChanged()
+
+            assertThat(latest).isTrue()
+
+            whenever(wifiPickerTracker.wifiState).thenReturn(WifiManager.WIFI_STATE_DISABLED)
+            getCallback().onWifiStateChanged()
+
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun isWifiDefault_initiallyGetsDefault() =
+        testScope.runTest { assertThat(underTest.isWifiDefault.value).isFalse() }
+
+    @Test
+    fun isWifiDefault_wifiNetwork_isTrue() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isWifiDefault)
+
+            val wifiEntry =
+                mock<WifiEntry>().apply { whenever(this.isDefaultNetwork).thenReturn(true) }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+
+            assertThat(latest).isTrue()
+        }
+
+    @Test
+    fun isWifiDefault_carrierMerged_isTrue() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isWifiDefault)
+
+            val wifiEntry =
+                mock<MergedCarrierEntry>().apply {
+                    whenever(this.isDefaultNetwork).thenReturn(true)
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+
+            assertThat(latest).isTrue()
+        }
+
+    @Test
+    fun isWifiDefault_wifiNetworkNotDefault_isFalse() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isWifiDefault)
+
+            val wifiEntry =
+                mock<WifiEntry>().apply { whenever(this.isDefaultNetwork).thenReturn(false) }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun isWifiDefault_carrierMergedNotDefault_isFalse() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isWifiDefault)
+
+            val wifiEntry =
+                mock<MergedCarrierEntry>().apply {
+                    whenever(this.isDefaultNetwork).thenReturn(false)
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun isWifiDefault_noWifiNetwork_isFalse() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isWifiDefault)
+
+            // First, add a network
+            val wifiEntry =
+                mock<WifiEntry>().apply { whenever(this.isDefaultNetwork).thenReturn(true) }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+
+            assertThat(latest).isTrue()
+
+            // WHEN the network is lost
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null)
+            getCallback().onWifiEntriesChanged()
+
+            // THEN we update to false
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun wifiNetwork_initiallyGetsDefault() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wifiNetwork)
+
+            assertThat(latest).isEqualTo(WIFI_NETWORK_DEFAULT)
+        }
+
+    @Test
+    fun wifiNetwork_primaryWifiNetworkAdded_flowHasNetwork() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wifiNetwork)
+
+            val wifiEntry =
+                mock<WifiEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(true)
+                    whenever(this.level).thenReturn(3)
+                    whenever(this.ssid).thenReturn(SSID)
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+
+            assertThat(latest is WifiNetworkModel.Active).isTrue()
+            val latestActive = latest as WifiNetworkModel.Active
+            assertThat(latestActive.level).isEqualTo(3)
+            assertThat(latestActive.ssid).isEqualTo(SSID)
+        }
+
+    @Test
+    fun wifiNetwork_isCarrierMerged_flowHasCarrierMerged() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wifiNetwork)
+
+            val wifiEntry =
+                mock<MergedCarrierEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(true)
+                    whenever(this.level).thenReturn(3)
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+
+            assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue()
+            val latestMerged = latest as WifiNetworkModel.CarrierMerged
+            assertThat(latestMerged.level).isEqualTo(3)
+            // numberOfLevels = maxSignalLevel + 1
+        }
+
+    @Test
+    fun wifiNetwork_isCarrierMerged_getsMaxSignalLevel() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wifiNetwork)
+
+            val wifiEntry =
+                mock<MergedCarrierEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(true)
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            whenever(wifiManager.maxSignalLevel).thenReturn(5)
+
+            getCallback().onWifiEntriesChanged()
+
+            assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue()
+            val latestMerged = latest as WifiNetworkModel.CarrierMerged
+            // numberOfLevels = maxSignalLevel + 1
+            assertThat(latestMerged.numberOfLevels).isEqualTo(6)
+        }
+
+    /* TODO(b/292534484): Re-enable this test once WifiTrackerLib gives us the subscription ID.
+    @Test
+    fun wifiNetwork_carrierMergedButInvalidSubId_flowHasInvalid() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wifiNetwork)
+
+            val wifiInfo =
+                mock<WifiInfo>().apply {
+                    whenever(this.isPrimary).thenReturn(true)
+                    whenever(this.isCarrierMerged).thenReturn(true)
+                    whenever(this.subscriptionId).thenReturn(INVALID_SUBSCRIPTION_ID)
+                }
+
+            getNetworkCallback()
+                .onCapabilitiesChanged(
+                    NETWORK,
+                    createWifiNetworkCapabilities(wifiInfo),
+                )
+
+            assertThat(latest).isInstanceOf(WifiNetworkModel.Invalid::class.java)
+        }
+
+     */
+
+    @Test
+    fun wifiNetwork_notValidated_networkNotValidated() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wifiNetwork)
+
+            val wifiEntry =
+                mock<WifiEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(true)
+                    whenever(this.hasInternetAccess()).thenReturn(false)
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+
+            assertThat((latest as WifiNetworkModel.Active).isValidated).isFalse()
+        }
+
+    @Test
+    fun wifiNetwork_validated_networkValidated() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wifiNetwork)
+
+            val wifiEntry =
+                mock<WifiEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(true)
+                    whenever(this.hasInternetAccess()).thenReturn(true)
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+
+            assertThat((latest as WifiNetworkModel.Active).isValidated).isTrue()
+        }
+
+    @Test
+    fun wifiNetwork_nonPrimaryWifiNetworkAdded_flowHasNoNetwork() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wifiNetwork)
+
+            val wifiEntry =
+                mock<WifiEntry>().apply { whenever(this.isPrimaryNetwork).thenReturn(false) }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+
+            assertThat(latest).isEqualTo(WifiNetworkModel.Inactive)
+        }
+
+    @Test
+    fun wifiNetwork_nonPrimaryCarrierMergedNetworkAdded_flowHasNoNetwork() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wifiNetwork)
+
+            val wifiEntry =
+                mock<MergedCarrierEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(false)
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+
+            assertThat(latest).isEqualTo(WifiNetworkModel.Inactive)
+        }
+
+    @Test
+    fun wifiNetwork_newPrimaryWifiNetwork_flowHasNewNetwork() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wifiNetwork)
+
+            // Start with the original network
+            val wifiEntry =
+                mock<WifiEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(true)
+                    whenever(this.level).thenReturn(3)
+                    whenever(this.ssid).thenReturn("AB")
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+
+            assertThat(latest is WifiNetworkModel.Active).isTrue()
+            var latestActive = latest as WifiNetworkModel.Active
+            assertThat(latestActive.level).isEqualTo(3)
+            assertThat(latestActive.ssid).isEqualTo("AB")
+
+            // WHEN we update to a new primary network
+            val newWifiEntry =
+                mock<WifiEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(true)
+                    whenever(this.level).thenReturn(4)
+                    whenever(this.ssid).thenReturn("CD")
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(newWifiEntry)
+            getCallback().onWifiEntriesChanged()
+
+            // THEN we use the new network
+            assertThat(latest is WifiNetworkModel.Active).isTrue()
+            latestActive = latest as WifiNetworkModel.Active
+            assertThat(latestActive.level).isEqualTo(4)
+            assertThat(latestActive.ssid).isEqualTo("CD")
+        }
+
+    @Test
+    fun wifiNetwork_noCurrentNetwork_networkLost_flowHasNoNetwork() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wifiNetwork)
+
+            // WHEN we receive a null network without any networks beforehand
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null)
+            getCallback().onWifiEntriesChanged()
+
+            // THEN there's no crash and we still have no network
+            assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+        }
+
+    @Test
+    fun wifiNetwork_currentActiveNetworkLost_flowHasNoNetwork() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wifiNetwork)
+
+            val wifiEntry =
+                mock<WifiEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(true)
+                    whenever(this.ssid).thenReturn(SSID)
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+
+            assertThat((latest as WifiNetworkModel.Active).ssid).isEqualTo(SSID)
+
+            // WHEN we lose our current network
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null)
+            getCallback().onWifiEntriesChanged()
+
+            // THEN we update to no network
+            assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+        }
+
+    /** Possible regression test for b/278618530. */
+    @Test
+    fun wifiNetwork_currentCarrierMergedNetworkLost_flowHasNoNetwork() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wifiNetwork)
+
+            val wifiEntry =
+                mock<MergedCarrierEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(true)
+                    whenever(this.level).thenReturn(3)
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+
+            assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue()
+            assertThat((latest as WifiNetworkModel.CarrierMerged).level).isEqualTo(3)
+
+            // WHEN we lose our current network
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null)
+            getCallback().onWifiEntriesChanged()
+
+            // THEN we update to no network
+            assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+        }
+
+    /** Regression test for b/244173280. */
+    @Test
+    fun wifiNetwork_multipleSubscribers_newSubscribersGetCurrentValue() =
+        testScope.runTest {
+            val latest1 by collectLastValue(underTest.wifiNetwork)
+
+            val wifiEntry =
+                mock<WifiEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(true)
+                    whenever(this.level).thenReturn(1)
+                    whenever(this.ssid).thenReturn(SSID)
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+
+            assertThat(latest1 is WifiNetworkModel.Active).isTrue()
+            val latest1Active = latest1 as WifiNetworkModel.Active
+            assertThat(latest1Active.level).isEqualTo(1)
+            assertThat(latest1Active.ssid).isEqualTo(SSID)
+
+            // WHEN we add a second subscriber after having already emitted a value
+            val latest2 by collectLastValue(underTest.wifiNetwork)
+
+            // THEN the second subscribe receives the already-emitted value
+            assertThat(latest2 is WifiNetworkModel.Active).isTrue()
+            val latest2Active = latest2 as WifiNetworkModel.Active
+            assertThat(latest2Active.level).isEqualTo(1)
+            assertThat(latest2Active.ssid).isEqualTo(SSID)
+        }
+
+    @Test
+    fun isWifiConnectedWithValidSsid_inactiveNetwork_false() =
+        testScope.runTest {
+            collectLastValue(underTest.wifiNetwork)
+
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null)
+            getCallback().onWifiEntriesChanged()
+
+            assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse()
+        }
+
+    @Test
+    fun isWifiConnectedWithValidSsid_nonPrimaryNetwork_false() =
+        testScope.runTest {
+            collectLastValue(underTest.wifiNetwork)
+
+            val wifiEntry =
+                mock<WifiEntry>().apply { whenever(this.isPrimaryNetwork).thenReturn(false) }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+            testScope.runCurrent()
+
+            assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse()
+        }
+
+    @Test
+    fun isWifiConnectedWithValidSsid_carrierMergedNetwork_false() =
+        testScope.runTest {
+            collectLastValue(underTest.wifiNetwork)
+
+            val wifiEntry =
+                mock<MergedCarrierEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(true)
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+            testScope.runCurrent()
+
+            assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse()
+        }
+
+    /* TODO(b/292534484): Re-enable this test once WifiTrackerLib gives us the subscription ID.
+       @Test
+       fun isWifiConnectedWithValidSsid_invalidNetwork_false() =
+       testScope.runTest {
+           collectLastValue(underTest.wifiNetwork)
+
+           val wifiInfo =
+               mock<WifiInfo>().apply {
+                   whenever(this.isPrimary).thenReturn(true)
+                   whenever(this.isCarrierMerged).thenReturn(true)
+                   whenever(this.subscriptionId).thenReturn(INVALID_SUBSCRIPTION_ID)
+               }
+
+           getNetworkCallback()
+               .onCapabilitiesChanged(
+                   NETWORK,
+                   createWifiNetworkCapabilities(wifiInfo),
+               )
+           testScope.runCurrent()
+
+           assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse()
+       }
+
+    */
+
+    @Test
+    fun isWifiConnectedWithValidSsid_activeNetwork_nullSsid_false() =
+        testScope.runTest {
+            collectLastValue(underTest.wifiNetwork)
+
+            val wifiEntry =
+                mock<WifiEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(true)
+                    whenever(this.ssid).thenReturn(null)
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+            testScope.runCurrent()
+
+            assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse()
+        }
+
+    @Test
+    fun isWifiConnectedWithValidSsid_activeNetwork_unknownSsid_false() =
+        testScope.runTest {
+            collectLastValue(underTest.wifiNetwork)
+
+            val wifiEntry =
+                mock<WifiEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(true)
+                    whenever(this.ssid).thenReturn(UNKNOWN_SSID)
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+            testScope.runCurrent()
+
+            assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse()
+        }
+
+    @Test
+    fun isWifiConnectedWithValidSsid_activeNetwork_validSsid_true() =
+        testScope.runTest {
+            collectLastValue(underTest.wifiNetwork)
+
+            val wifiEntry =
+                mock<WifiEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(true)
+                    whenever(this.ssid).thenReturn("fakeSsid")
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+            testScope.runCurrent()
+
+            assertThat(underTest.isWifiConnectedWithValidSsid()).isTrue()
+        }
+
+    @Test
+    fun isWifiConnectedWithValidSsid_activeToInactive_trueToFalse() =
+        testScope.runTest {
+            collectLastValue(underTest.wifiNetwork)
+
+            // Start with active
+            val wifiEntry =
+                mock<WifiEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(true)
+                    whenever(this.ssid).thenReturn("fakeSsid")
+                }
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
+            getCallback().onWifiEntriesChanged()
+            testScope.runCurrent()
+
+            assertThat(underTest.isWifiConnectedWithValidSsid()).isTrue()
+
+            // WHEN the network is lost
+            whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null)
+            getCallback().onWifiEntriesChanged()
+            testScope.runCurrent()
+
+            // THEN the isWifiConnected updates
+            assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse()
+        }
+
+    private fun getCallback(): WifiPickerTracker.WifiPickerTrackerCallback {
+        testScope.runCurrent()
+        return callbackCaptor.value
+    }
+
+    private fun createRepo(): WifiRepositoryViaTrackerLib {
+        return WifiRepositoryViaTrackerLib(
+            testScope.backgroundScope,
+            executor,
+            wifiPickerTrackerFactory,
+            wifiManager,
+            logger,
+            tableLogger,
+        )
+    }
+
+    private companion object {
+        const val SSID = "AB"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
index fc2277b..6fe88c1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
@@ -17,7 +17,9 @@
 package com.android.systemui.statusbar.pipeline.wifi.domain.interactor
 
 import android.net.wifi.WifiManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
@@ -25,18 +27,22 @@
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
 import kotlinx.coroutines.yield
 import org.junit.Before
 import org.junit.Test
+import org.junit.runner.RunWith
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 @SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
 class WifiInteractorImplTest : SysuiTestCase() {
 
     private lateinit var underTest: WifiInteractor
@@ -44,6 +50,8 @@
     private lateinit var connectivityRepository: FakeConnectivityRepository
     private lateinit var wifiRepository: FakeWifiRepository
 
+    private val testScope = TestScope()
+
     @Before
     fun setUp() {
         connectivityRepository = FakeConnectivityRepository()
@@ -53,11 +61,12 @@
 
     @Test
     fun ssid_unavailableNetwork_outputsNull() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             wifiRepository.setWifiNetwork(WifiNetworkModel.Unavailable)
 
             var latest: String? = "default"
             val job = underTest.ssid.onEach { latest = it }.launchIn(this)
+            runCurrent()
 
             assertThat(latest).isNull()
 
@@ -66,11 +75,12 @@
 
     @Test
     fun ssid_inactiveNetwork_outputsNull() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
 
             var latest: String? = "default"
             val job = underTest.ssid.onEach { latest = it }.launchIn(this)
+            runCurrent()
 
             assertThat(latest).isNull()
 
@@ -79,13 +89,14 @@
 
     @Test
     fun ssid_carrierMergedNetwork_outputsNull() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             wifiRepository.setWifiNetwork(
                 WifiNetworkModel.CarrierMerged(networkId = 1, subscriptionId = 2, level = 1)
             )
 
             var latest: String? = "default"
             val job = underTest.ssid.onEach { latest = it }.launchIn(this)
+            runCurrent()
 
             assertThat(latest).isNull()
 
@@ -94,7 +105,7 @@
 
     @Test
     fun ssid_isPasspointAccessPoint_outputsPasspointName() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             wifiRepository.setWifiNetwork(
                 WifiNetworkModel.Active(
                     networkId = 1,
@@ -106,6 +117,7 @@
 
             var latest: String? = null
             val job = underTest.ssid.onEach { latest = it }.launchIn(this)
+            runCurrent()
 
             assertThat(latest).isEqualTo("friendly")
 
@@ -114,7 +126,7 @@
 
     @Test
     fun ssid_isOnlineSignUpForPasspoint_outputsPasspointName() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             wifiRepository.setWifiNetwork(
                 WifiNetworkModel.Active(
                     networkId = 1,
@@ -126,6 +138,7 @@
 
             var latest: String? = null
             val job = underTest.ssid.onEach { latest = it }.launchIn(this)
+            runCurrent()
 
             assertThat(latest).isEqualTo("friendly")
 
@@ -134,7 +147,7 @@
 
     @Test
     fun ssid_unknownSsid_outputsNull() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             wifiRepository.setWifiNetwork(
                 WifiNetworkModel.Active(
                     networkId = 1,
@@ -145,6 +158,7 @@
 
             var latest: String? = "default"
             val job = underTest.ssid.onEach { latest = it }.launchIn(this)
+            runCurrent()
 
             assertThat(latest).isNull()
 
@@ -153,7 +167,7 @@
 
     @Test
     fun ssid_validSsid_outputsSsid() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             wifiRepository.setWifiNetwork(
                 WifiNetworkModel.Active(
                     networkId = 1,
@@ -164,6 +178,7 @@
 
             var latest: String? = null
             val job = underTest.ssid.onEach { latest = it }.launchIn(this)
+            runCurrent()
 
             assertThat(latest).isEqualTo("MyAwesomeWifiNetwork")
 
@@ -172,7 +187,7 @@
 
     @Test
     fun isEnabled_matchesRepoIsEnabled() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             var latest: Boolean? = null
             val job = underTest.isEnabled.onEach { latest = it }.launchIn(this)
 
@@ -193,7 +208,7 @@
 
     @Test
     fun isDefault_matchesRepoIsDefault() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             var latest: Boolean? = null
             val job = underTest.isDefault.onEach { latest = it }.launchIn(this)
 
@@ -214,7 +229,7 @@
 
     @Test
     fun wifiNetwork_matchesRepoWifiNetwork() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val wifiNetwork =
                 WifiNetworkModel.Active(
                     networkId = 45,
@@ -227,6 +242,7 @@
 
             var latest: WifiNetworkModel? = null
             val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+            runCurrent()
 
             assertThat(latest).isEqualTo(wifiNetwork)
 
@@ -235,7 +251,7 @@
 
     @Test
     fun activity_matchesRepoWifiActivity() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             var latest: DataActivityModel? = null
             val job = underTest.activity.onEach { latest = it }.launchIn(this)
 
@@ -259,11 +275,12 @@
 
     @Test
     fun isForceHidden_repoHasWifiHidden_outputsTrue() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
 
             var latest: Boolean? = null
             val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
+            runCurrent()
 
             assertThat(latest).isTrue()
 
@@ -272,16 +289,15 @@
 
     @Test
     fun isForceHidden_repoDoesNotHaveWifiHidden_outputsFalse() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             connectivityRepository.setForceHiddenIcons(setOf())
 
             var latest: Boolean? = null
             val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
+            runCurrent()
 
             assertThat(latest).isFalse()
 
             job.cancel()
         }
 }
-
-private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index cb469ea..bdeba2a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -16,8 +16,11 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
@@ -36,23 +39,19 @@
 import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel.Companion.viewModelForLocation
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.cancel
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.yield
-import org.junit.After
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
+import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 @SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
 class WifiViewModelTest : SysuiTestCase() {
 
     private lateinit var underTest: WifiViewModel
@@ -66,7 +65,7 @@
     private lateinit var interactor: WifiInteractor
     private lateinit var airplaneModeViewModel: AirplaneModeViewModel
     private val shouldShowSignalSpacerProviderFlow = MutableStateFlow(false)
-    private lateinit var scope: CoroutineScope
+    private val testScope = TestScope()
 
     @Before
     fun setUp() {
@@ -76,7 +75,6 @@
         wifiRepository = FakeWifiRepository()
         wifiRepository.setIsWifiEnabled(true)
         interactor = WifiInteractorImpl(connectivityRepository, wifiRepository)
-        scope = CoroutineScope(IMMEDIATE)
         airplaneModeViewModel =
             AirplaneModeViewModelImpl(
                 AirplaneModeInteractor(
@@ -84,17 +82,12 @@
                     connectivityRepository,
                 ),
                 tableLogBuffer,
-                scope,
+                testScope.backgroundScope,
             )
 
         createAndSetViewModel()
     }
 
-    @After
-    fun tearDown() {
-        scope.cancel()
-    }
-
     // See [WifiViewModelIconParameterizedTest] for additional view model tests.
 
     // Note on testing: [WifiViewModel] exposes 3 different instances of
@@ -104,104 +97,65 @@
 
     @Test
     fun wifiIcon_allLocationViewModelsReceiveSameData() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val home = viewModelForLocation(underTest, StatusBarLocation.HOME)
             val keyguard = viewModelForLocation(underTest, StatusBarLocation.KEYGUARD)
             val qs = viewModelForLocation(underTest, StatusBarLocation.QS)
 
-            var latestHome: WifiIcon? = null
-            val jobHome = home.wifiIcon.onEach { latestHome = it }.launchIn(this)
-
-            var latestKeyguard: WifiIcon? = null
-            val jobKeyguard = keyguard.wifiIcon.onEach { latestKeyguard = it }.launchIn(this)
-
-            var latestQs: WifiIcon? = null
-            val jobQs = qs.wifiIcon.onEach { latestQs = it }.launchIn(this)
+            val latestHome by collectLastValue(home.wifiIcon)
+            val latestKeyguard by collectLastValue(keyguard.wifiIcon)
+            val latestQs by collectLastValue(qs.wifiIcon)
 
             wifiRepository.setWifiNetwork(
                 WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 1)
             )
-            yield()
 
             assertThat(latestHome).isInstanceOf(WifiIcon.Visible::class.java)
             assertThat(latestHome).isEqualTo(latestKeyguard)
             assertThat(latestKeyguard).isEqualTo(latestQs)
-
-            jobHome.cancel()
-            jobKeyguard.cancel()
-            jobQs.cancel()
         }
 
     @Test
     fun activity_showActivityConfigFalse_outputsFalse() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(false)
             createAndSetViewModel()
             wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-            var activityIn: Boolean? = null
-            val activityInJob =
-                underTest.isActivityInViewVisible.onEach { activityIn = it }.launchIn(this)
-
-            var activityOut: Boolean? = null
-            val activityOutJob =
-                underTest.isActivityOutViewVisible.onEach { activityOut = it }.launchIn(this)
-
-            var activityContainer: Boolean? = null
-            val activityContainerJob =
-                underTest.isActivityContainerVisible
-                    .onEach { activityContainer = it }
-                    .launchIn(this)
+            val activityIn by collectLastValue(underTest.isActivityInViewVisible)
+            val activityOut by collectLastValue(underTest.isActivityOutViewVisible)
+            val activityContainer by collectLastValue(underTest.isActivityContainerVisible)
 
             // Verify that on launch, we receive false.
             assertThat(activityIn).isFalse()
             assertThat(activityOut).isFalse()
             assertThat(activityContainer).isFalse()
-
-            activityInJob.cancel()
-            activityOutJob.cancel()
-            activityContainerJob.cancel()
         }
 
     @Test
     fun activity_showActivityConfigFalse_noUpdatesReceived() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(false)
             createAndSetViewModel()
             wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-            var activityIn: Boolean? = null
-            val activityInJob =
-                underTest.isActivityInViewVisible.onEach { activityIn = it }.launchIn(this)
-
-            var activityOut: Boolean? = null
-            val activityOutJob =
-                underTest.isActivityOutViewVisible.onEach { activityOut = it }.launchIn(this)
-
-            var activityContainer: Boolean? = null
-            val activityContainerJob =
-                underTest.isActivityContainerVisible
-                    .onEach { activityContainer = it }
-                    .launchIn(this)
+            val activityIn by collectLastValue(underTest.isActivityInViewVisible)
+            val activityOut by collectLastValue(underTest.isActivityOutViewVisible)
+            val activityContainer by collectLastValue(underTest.isActivityContainerVisible)
 
             // WHEN we update the repo to have activity
             val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
             wifiRepository.setWifiActivity(activity)
-            yield()
 
             // THEN we didn't update to the new activity (because our config is false)
             assertThat(activityIn).isFalse()
             assertThat(activityOut).isFalse()
             assertThat(activityContainer).isFalse()
-
-            activityInJob.cancel()
-            activityOutJob.cancel()
-            activityContainerJob.cancel()
         }
 
     @Test
     fun activity_nullSsid_outputsFalse() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
 
@@ -209,38 +163,23 @@
                 WifiNetworkModel.Active(NETWORK_ID, ssid = null, level = 1)
             )
 
-            var activityIn: Boolean? = null
-            val activityInJob =
-                underTest.isActivityInViewVisible.onEach { activityIn = it }.launchIn(this)
-
-            var activityOut: Boolean? = null
-            val activityOutJob =
-                underTest.isActivityOutViewVisible.onEach { activityOut = it }.launchIn(this)
-
-            var activityContainer: Boolean? = null
-            val activityContainerJob =
-                underTest.isActivityContainerVisible
-                    .onEach { activityContainer = it }
-                    .launchIn(this)
+            val activityIn by collectLastValue(underTest.isActivityInViewVisible)
+            val activityOut by collectLastValue(underTest.isActivityOutViewVisible)
+            val activityContainer by collectLastValue(underTest.isActivityContainerVisible)
 
             // WHEN we update the repo to have activity
             val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
             wifiRepository.setWifiActivity(activity)
-            yield()
 
             // THEN we still output false because our network's SSID is null
             assertThat(activityIn).isFalse()
             assertThat(activityOut).isFalse()
             assertThat(activityContainer).isFalse()
-
-            activityInJob.cancel()
-            activityOutJob.cancel()
-            activityContainerJob.cancel()
         }
 
     @Test
     fun activity_allLocationViewModelsReceiveSameData() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
             wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -249,250 +188,187 @@
             val keyguard = viewModelForLocation(underTest, StatusBarLocation.KEYGUARD)
             val qs = viewModelForLocation(underTest, StatusBarLocation.QS)
 
-            var latestHome: Boolean? = null
-            val jobHome = home.isActivityInViewVisible.onEach { latestHome = it }.launchIn(this)
-
-            var latestKeyguard: Boolean? = null
-            val jobKeyguard =
-                keyguard.isActivityInViewVisible.onEach { latestKeyguard = it }.launchIn(this)
-
-            var latestQs: Boolean? = null
-            val jobQs = qs.isActivityInViewVisible.onEach { latestQs = it }.launchIn(this)
+            val latestHome by collectLastValue(home.isActivityInViewVisible)
+            val latestKeyguard by collectLastValue(keyguard.isActivityInViewVisible)
+            val latestQs by collectLastValue(qs.isActivityInViewVisible)
 
             val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
             wifiRepository.setWifiActivity(activity)
-            yield()
 
             assertThat(latestHome).isTrue()
             assertThat(latestKeyguard).isTrue()
             assertThat(latestQs).isTrue()
-
-            jobHome.cancel()
-            jobKeyguard.cancel()
-            jobQs.cancel()
         }
 
     @Test
     fun activityIn_hasActivityInTrue_outputsTrue() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
             wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-            var latest: Boolean? = null
-            val job = underTest.isActivityInViewVisible.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isActivityInViewVisible)
 
             val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
             wifiRepository.setWifiActivity(activity)
-            yield()
 
             assertThat(latest).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun activityIn_hasActivityInFalse_outputsFalse() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
             wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-            var latest: Boolean? = null
-            val job = underTest.isActivityInViewVisible.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isActivityInViewVisible)
 
             val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = true)
             wifiRepository.setWifiActivity(activity)
-            yield()
 
             assertThat(latest).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun activityOut_hasActivityOutTrue_outputsTrue() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
             wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-            var latest: Boolean? = null
-            val job = underTest.isActivityOutViewVisible.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isActivityOutViewVisible)
 
             val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = true)
             wifiRepository.setWifiActivity(activity)
-            yield()
 
             assertThat(latest).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun activityOut_hasActivityOutFalse_outputsFalse() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
             wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-            var latest: Boolean? = null
-            val job = underTest.isActivityOutViewVisible.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isActivityOutViewVisible)
 
             val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
             wifiRepository.setWifiActivity(activity)
-            yield()
 
             assertThat(latest).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun activityContainer_hasActivityInTrue_outputsTrue() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
             wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-            var latest: Boolean? = null
-            val job = underTest.isActivityContainerVisible.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isActivityContainerVisible)
 
             val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
             wifiRepository.setWifiActivity(activity)
-            yield()
 
             assertThat(latest).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun activityContainer_hasActivityOutTrue_outputsTrue() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
             wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-            var latest: Boolean? = null
-            val job = underTest.isActivityContainerVisible.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isActivityContainerVisible)
 
             val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = true)
             wifiRepository.setWifiActivity(activity)
-            yield()
 
             assertThat(latest).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun activityContainer_inAndOutTrue_outputsTrue() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
             wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-            var latest: Boolean? = null
-            val job = underTest.isActivityContainerVisible.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isActivityContainerVisible)
 
             val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
             wifiRepository.setWifiActivity(activity)
-            yield()
 
             assertThat(latest).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun activityContainer_inAndOutFalse_outputsFalse() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
             wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
-            var latest: Boolean? = null
-            val job = underTest.isActivityContainerVisible.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isActivityContainerVisible)
 
             val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
             wifiRepository.setWifiActivity(activity)
-            yield()
 
             assertThat(latest).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun airplaneSpacer_notAirplaneMode_outputsFalse() =
-        runBlocking(IMMEDIATE) {
-            var latest: Boolean? = null
-            val job = underTest.isAirplaneSpacerVisible.onEach { latest = it }.launchIn(this)
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isAirplaneSpacerVisible)
 
             airplaneModeRepository.setIsAirplaneMode(false)
-            yield()
 
             assertThat(latest).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun airplaneSpacer_airplaneForceHidden_outputsFalse() =
-        runBlocking(IMMEDIATE) {
-            var latest: Boolean? = null
-            val job = underTest.isAirplaneSpacerVisible.onEach { latest = it }.launchIn(this)
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isAirplaneSpacerVisible)
 
             airplaneModeRepository.setIsAirplaneMode(true)
             connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE))
-            yield()
 
             assertThat(latest).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun airplaneSpacer_airplaneIconVisible_outputsTrue() =
-        runBlocking(IMMEDIATE) {
-            var latest: Boolean? = null
-            val job = underTest.isAirplaneSpacerVisible.onEach { latest = it }.launchIn(this)
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isAirplaneSpacerVisible)
 
             airplaneModeRepository.setIsAirplaneMode(true)
-            yield()
 
             assertThat(latest).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun signalSpacer_firstSubNotShowingNetworkTypeIcon_outputsFalse() =
-        runBlocking(IMMEDIATE) {
-            var latest: Boolean? = null
-            val job = underTest.isSignalSpacerVisible.onEach { latest = it }.launchIn(this)
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isSignalSpacerVisible)
 
             shouldShowSignalSpacerProviderFlow.value = false
-            yield()
 
             assertThat(latest).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun signalSpacer_firstSubIsShowingNetworkTypeIcon_outputsTrue() =
-        runBlocking(IMMEDIATE) {
-            var latest: Boolean? = null
-            val job = underTest.isSignalSpacerVisible.onEach { latest = it }.launchIn(this)
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isSignalSpacerVisible)
 
             shouldShowSignalSpacerProviderFlow.value = true
-            yield()
 
             assertThat(latest).isTrue()
-
-            job.cancel()
         }
 
     private fun createAndSetViewModel() {
@@ -507,7 +383,7 @@
                 context,
                 tableLogBuffer,
                 interactor,
-                scope,
+                testScope.backgroundScope,
                 wifiConstants,
             )
     }
@@ -518,5 +394,3 @@
             WifiNetworkModel.Active(NETWORK_ID, ssid = "AB", level = 1)
     }
 }
-
-private val IMMEDIATE = Dispatchers.Main.immediate