Merge "[Catalyst] Support SET api for int value" into main
diff --git a/android-sdk-flags/OWNERS b/android-sdk-flags/OWNERS
new file mode 100644
index 0000000..01f45dd
--- /dev/null
+++ b/android-sdk-flags/OWNERS
@@ -0,0 +1 @@
+include /SDK_OWNERS
diff --git a/android-sdk-flags/flags.aconfig b/android-sdk-flags/flags.aconfig
index cfe298e..19c7bf6 100644
--- a/android-sdk-flags/flags.aconfig
+++ b/android-sdk-flags/flags.aconfig
@@ -6,6 +6,7 @@
     namespace: "android_sdk"
     description: "Use the new SDK major.minor versioning scheme (e.g. Android 40.1) which replaces the old single-integer scheme (e.g. Android 15)."
     bug: "350458259"
+    is_exported: true
 
     # Use is_fixed_read_only because DeviceConfig may not be available when Build.VERSION_CODES is first accessed
     is_fixed_read_only: true
diff --git a/core/java/android/content/res/TEST_MAPPING b/core/java/android/content/res/TEST_MAPPING
index c2febae..e8893e4 100644
--- a/core/java/android/content/res/TEST_MAPPING
+++ b/core/java/android/content/res/TEST_MAPPING
@@ -5,6 +5,9 @@
     },
     {
       "path": "frameworks/base/core/tests/coretests/src/com/android/internal/content/res"
+    },
+    {
+      "path": "platform_testing/libraries/screenshot"
     }
   ],
   "presubmit": [
diff --git a/core/java/android/view/InsetsSourceControl.java b/core/java/android/view/InsetsSourceControl.java
index 7877352..acbd95bf 100644
--- a/core/java/android/view/InsetsSourceControl.java
+++ b/core/java/android/view/InsetsSourceControl.java
@@ -212,8 +212,7 @@
                 && mInitiallyVisible == that.mInitiallyVisible
                 && mSurfacePosition.equals(that.mSurfacePosition)
                 && mInsetsHint.equals(that.mInsetsHint)
-                && mSkipAnimationOnce == that.mSkipAnimationOnce
-                && Objects.equals(mImeStatsToken, that.mImeStatsToken);
+                && mSkipAnimationOnce == that.mSkipAnimationOnce;
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 5f0eed9..14f8cc7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1632,6 +1632,7 @@
         if (!isShowingAsBubbleBar()) {
             callback = b -> {
                 if (mStackView != null) {
+                    b.setSuppressFlyout(true);
                     mStackView.addBubble(b);
                     mStackView.setSelectedBubble(b);
                 } else {
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 5c5edb1..b0bd5b7 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1708,3 +1708,9 @@
     bug: "365064144"
 }
 
+flag {
+   name: "touchpad_three_finger_tap_customization"
+   namespace: "systemui"
+   description: "Customize touchpad three finger tap"
+   bug: "365063048"
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index 6fc51e4..66a0908 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -53,6 +53,7 @@
 import com.android.systemui.notifications.ui.composable.SnoozeableHeadsUpNotificationSpace
 import com.android.systemui.res.R
 import com.android.systemui.shade.LargeScreenHeaderHelper
+import com.android.systemui.shade.ShadeDisplayAware
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker
@@ -84,7 +85,7 @@
     stackScrollLayout: NotificationStackScrollLayout,
     sharedNotificationContainerBinder: SharedNotificationContainerBinder,
     private val keyguardRootViewModel: KeyguardRootViewModel,
-    private val configurationState: ConfigurationState,
+    @ShadeDisplayAware private val configurationState: ConfigurationState,
     private val iconBindingFailureTracker: StatusBarIconViewBindingFailureTracker,
     private val nicAodViewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
     private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/OnTeardownRuleTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/OnTeardownRuleTest.kt
new file mode 100644
index 0000000..8635bb0
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/OnTeardownRuleTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2024 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
+
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.JUnitCore
+
+@Suppress("JUnitMalformedDeclaration")
+@SmallTest
+class OnTeardownRuleTest : SysuiTestCase() {
+    // None of these inner classes should be run except as part of this utilities-testing test
+    class HasTeardown {
+        @get:Rule val teardownRule = OnTeardownRule()
+
+        @Before
+        fun setUp() {
+            teardownWasRun = false
+            teardownRule.onTeardown { teardownWasRun = true }
+        }
+
+        @Test fun doTest() {}
+
+        companion object {
+            var teardownWasRun = false
+        }
+    }
+
+    @Test
+    fun teardownRuns() {
+        val result = JUnitCore().run(HasTeardown::class.java)
+        assertThat(result.failures).isEmpty()
+        assertThat(HasTeardown.teardownWasRun).isTrue()
+    }
+
+    class FirstTeardownFails {
+        @get:Rule val teardownRule = OnTeardownRule()
+
+        @Before
+        fun setUp() {
+            teardownWasRun = false
+            teardownRule.onTeardown { fail("One fails") }
+            teardownRule.onTeardown { teardownWasRun = true }
+        }
+
+        @Test fun doTest() {}
+
+        companion object {
+            var teardownWasRun = false
+        }
+    }
+
+    @Test
+    fun allTeardownsRun() {
+        val result = JUnitCore().run(FirstTeardownFails::class.java)
+        assertThat(result.failures.map { it.message }).isEqualTo(listOf("One fails"))
+        assertThat(FirstTeardownFails.teardownWasRun).isTrue()
+    }
+
+    class ThreeTeardowns {
+        @get:Rule val teardownRule = OnTeardownRule()
+
+        @Before
+        fun setUp() {
+            messages.clear()
+        }
+
+        @Test
+        fun doTest() {
+            teardownRule.onTeardown { messages.add("A") }
+            teardownRule.onTeardown { messages.add("B") }
+            teardownRule.onTeardown { messages.add("C") }
+        }
+
+        companion object {
+            val messages = mutableListOf<String>()
+        }
+    }
+
+    @Test
+    fun reverseOrder() {
+        val result = JUnitCore().run(ThreeTeardowns::class.java)
+        assertThat(result.failures).isEmpty()
+        assertThat(ThreeTeardowns.messages).isEqualTo(listOf("C", "B", "A"))
+    }
+
+    class TryToDoABadThing {
+        @get:Rule val teardownRule = OnTeardownRule()
+
+        @Test
+        fun doTest() {
+            teardownRule.onTeardown {
+                teardownRule.onTeardown {
+                    // do nothing
+                }
+            }
+        }
+    }
+
+    @Test
+    fun prohibitTeardownDuringTeardown() {
+        val result = JUnitCore().run(TryToDoABadThing::class.java)
+        assertThat(result.failures.map { it.message })
+            .isEqualTo(listOf("Cannot add new teardown routines after test complete."))
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
index 2905a73..646722b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
@@ -116,6 +116,7 @@
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
+import com.android.systemui.statusbar.data.repository.LightBarControllerStore;
 import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.LightBarController;
@@ -208,7 +209,7 @@
     @Mock
     private LightBarController mLightBarController;
     @Mock
-    private LightBarController.Factory mLightBarcontrollerFactory;
+    private LightBarControllerStore mLightBarControllerStore;
     @Mock
     private AutoHideController mAutoHideController;
     @Mock
@@ -257,7 +258,7 @@
     public void setup() throws Exception {
         MockitoAnnotations.initMocks(this);
 
-        when(mLightBarcontrollerFactory.create(any(Context.class))).thenReturn(mLightBarController);
+        when(mLightBarControllerStore.forDisplay(anyInt())).thenReturn(mLightBarController);
         when(mAutoHideControllerFactory.create(any(Context.class))).thenReturn(mAutoHideController);
         when(mNavigationBarView.getHomeButton()).thenReturn(mHomeButton);
         when(mNavigationBarView.getRecentsButton()).thenReturn(mRecentsButton);
@@ -649,8 +650,7 @@
                 mFakeExecutor,
                 mUiEventLogger,
                 mNavBarHelper,
-                mLightBarController,
-                mLightBarcontrollerFactory,
+                mLightBarControllerStore,
                 mAutoHideController,
                 mAutoHideControllerFactory,
                 Optional.of(mTelecomManager),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/LightBarControllerStoreImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/LightBarControllerStoreImplTest.kt
new file mode 100644
index 0000000..18eef33
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/LightBarControllerStoreImplTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 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.data.repository
+
+import android.view.Display.DEFAULT_DISPLAY
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LightBarControllerStoreImplTest : SysuiTestCase() {
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+    private val testScope = kosmos.testScope
+    private val fakeDisplayRepository = kosmos.displayRepository
+
+    private val underTest = kosmos.lightBarControllerStoreImpl
+
+    @Before
+    fun start() {
+        underTest.start()
+    }
+
+    @Before fun addDisplays() = runBlocking { fakeDisplayRepository.addDisplay(DEFAULT_DISPLAY) }
+
+    @Test
+    fun forDisplay_startsInstance() =
+        testScope.runTest {
+            val instance = underTest.forDisplay(DEFAULT_DISPLAY)
+
+            verify(instance).start()
+        }
+
+    @Test
+    fun beforeDisplayRemoved_doesNotStopInstances() =
+        testScope.runTest {
+            val instance = underTest.forDisplay(DEFAULT_DISPLAY)
+
+            verify(instance, never()).stop()
+        }
+
+    @Test
+    fun displayRemoved_stopsInstance() =
+        testScope.runTest {
+            val instance = underTest.forDisplay(DEFAULT_DISPLAY)
+
+            fakeDisplayRepository.removeDisplay(DEFAULT_DISPLAY)
+
+            verify(instance).stop()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
index 88ec18d..12f6825 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
@@ -48,12 +48,10 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.navigationbar.NavigationModeController;
-import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.statusbar.data.model.StatusBarAppearance;
 import com.android.systemui.statusbar.data.model.StatusBarMode;
-import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository;
+import com.android.systemui.statusbar.data.repository.FakeStatusBarModePerDisplayRepository;
 import com.android.systemui.statusbar.policy.BatteryController;
-import com.android.systemui.util.kotlin.JavaAdapter;
 
 import kotlinx.coroutines.test.TestScope;
 
@@ -81,8 +79,8 @@
     private SysuiDarkIconDispatcher mStatusBarIconController;
     private LightBarController mLightBarController;
     private final TestScope mTestScope = TestScopeProvider.getTestScope();
-    private final FakeStatusBarModeRepository mStatusBarModeRepository =
-            new FakeStatusBarModeRepository();
+    private final FakeStatusBarModePerDisplayRepository mStatusBarModeRepository =
+            new FakeStatusBarModePerDisplayRepository();
 
     @Before
     public void setup() {
@@ -92,15 +90,15 @@
         mLightBarTransitionsController = mock(LightBarTransitionsController.class);
         when(mStatusBarIconController.getTransitionsController()).thenReturn(
                 mLightBarTransitionsController);
-        mLightBarController = new LightBarController(
-                mContext,
-                new JavaAdapter(mTestScope),
+        mLightBarController = new LightBarControllerImpl(
+                mContext.getDisplayId(),
+                mTestScope,
                 mStatusBarIconController,
                 mock(BatteryController.class),
                 mock(NavigationModeController.class),
                 mStatusBarModeRepository,
                 mock(DumpManager.class),
-                new FakeDisplayTracker(mContext));
+                mTestScope.getCoroutineContext());
         mLightBarController.start();
     }
 
@@ -121,7 +119,7 @@
                 new AppearanceRegion(APPEARANCE_LIGHT_STATUS_BARS, secondBounds)
         );
 
-        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -142,7 +140,7 @@
                 new AppearanceRegion(0 /* appearance */, secondBounds)
         );
 
-        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -165,7 +163,7 @@
                 new AppearanceRegion(APPEARANCE_LIGHT_STATUS_BARS, secondBounds)
         );
 
-        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -190,7 +188,7 @@
                 new AppearanceRegion(APPEARANCE_LIGHT_STATUS_BARS, thirdBounds)
         );
 
-        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -214,7 +212,7 @@
                 new AppearanceRegion(0 /* appearance */, secondBounds)
         );
 
-        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -231,7 +229,7 @@
                 new AppearanceRegion(APPEARANCE_LIGHT_STATUS_BARS, new Rect(0, 0, 1, 1))
         );
 
-        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -249,7 +247,7 @@
                 new AppearanceRegion(0, new Rect(0, 0, 1, 1))
         );
 
-        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -266,7 +264,7 @@
                 new AppearanceRegion(APPEARANCE_LIGHT_STATUS_BARS, new Rect(0, 0, 1, 1))
         );
 
-        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -276,7 +274,7 @@
         reset(mStatusBarIconController);
 
         // WHEN the same appearance regions but different status bar mode is sent
-        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.LIGHTS_OUT_TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -298,7 +296,7 @@
                 /* start= */ new Rect(0, 0, 10, 10),
                 /* end= */ new Rect(0, 0, 20, 20));
 
-        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         startingBounds,
@@ -311,7 +309,7 @@
         BoundsPair newBounds = new BoundsPair(
                 /* start= */ new Rect(0, 0, 30, 30),
                 /* end= */ new Rect(0, 0, 40, 40));
-        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         newBounds,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt
index e0d9fac..110dec6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt
@@ -36,7 +36,6 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
@@ -58,7 +57,6 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
-import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -144,6 +142,7 @@
         controller.start()
         controller.addCallback(mockOngoingCallListener)
         controller.setChipView(chipView)
+        onTeardown { controller.tearDownChipView() }
 
         val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java)
         verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture())
@@ -153,11 +152,6 @@
             .thenReturn(PROC_STATE_INVISIBLE)
     }
 
-    @After
-    fun tearDown() {
-        controller.tearDownChipView()
-    }
-
     @Test
     fun onEntryUpdated_isOngoingCallNotif_listenerAndRepoNotified() {
         val notification = NotificationEntryBuilder(createOngoingCallNotifEntry())
@@ -224,7 +218,7 @@
         notifCollectionListener.onEntryUpdated(notification.build())
         chipView.measure(
             View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
         )
 
         assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
@@ -241,7 +235,7 @@
         notifCollectionListener.onEntryUpdated(notification.build())
         chipView.measure(
             View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
         )
 
         assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
@@ -257,7 +251,7 @@
         notifCollectionListener.onEntryUpdated(notification.build())
         chipView.measure(
             View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
         )
 
         assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
@@ -668,7 +662,7 @@
 
     private fun createCallNotifEntry(
         callStyle: Notification.CallStyle,
-        nullContentIntent: Boolean = false
+        nullContentIntent: Boolean = false,
     ): NotificationEntry {
         val notificationEntryBuilder = NotificationEntryBuilder()
         notificationEntryBuilder.modifyNotification(context).style = callStyle
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt
index fc1ea22..19d5a16 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt
@@ -66,6 +66,7 @@
             logBuffer = FakeLogBuffer.Factory.create(),
             verboseLogBuffer = FakeLogBuffer.Factory.create(),
             systemClock,
+            context.resources,
         )
     private val demoDataSource =
         mock<DemoDeviceBasedSatelliteDataSource>().also {
@@ -80,7 +81,11 @@
                 )
         }
     private val demoImpl =
-        DemoDeviceBasedSatelliteRepository(demoDataSource, testScope.backgroundScope)
+        DemoDeviceBasedSatelliteRepository(
+            demoDataSource,
+            testScope.backgroundScope,
+            context.resources,
+        )
 
     private val underTest =
         DeviceBasedSatelliteRepositorySwitcher(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepositoryTest.kt
index 8769389..a70881a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepositoryTest.kt
@@ -58,7 +58,12 @@
                 whenever(it.satelliteEvents).thenReturn(fakeSatelliteEvents)
             }
 
-        underTest = DemoDeviceBasedSatelliteRepository(dataSource, testScope.backgroundScope)
+        underTest =
+            DemoDeviceBasedSatelliteRepository(
+                dataSource,
+                testScope.backgroundScope,
+                context.resources,
+            )
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt
index 55460bd..41fa9e7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt
@@ -28,4 +28,6 @@
     override val signalStrength = MutableStateFlow(0)
 
     override val isSatelliteAllowedForCurrentLocation = MutableStateFlow(false)
+
+    override var isOpportunisticSatelliteIconEnabled: Boolean = true
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
index e7e4969..509aa7a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
@@ -172,6 +172,26 @@
         }
 
     @Test
+    fun icon_null_allOosAndConfigIsFalse() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.icon)
+
+            // GIVEN config for opportunistic icon is false
+            repo.isOpportunisticSatelliteIconEnabled = false
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+            i1.isEmergencyOnly.value = false
+
+            // GIVEN apm is disabled
+            airplaneModeRepository.setIsAirplaneMode(false)
+
+            // THEN icon is null because it is not allowed
+            assertThat(latest).isNull()
+        }
+
+    @Test
     fun icon_null_isEmergencyOnly() =
         testScope.runTest {
             val latest by collectLastValue(underTest.icon)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index 53e033e..e7ca1dd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -51,6 +51,8 @@
 import android.os.Handler;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.provider.Settings;
 
 import androidx.annotation.VisibleForTesting;
@@ -561,6 +563,113 @@
     }
 
     @Test
+    @EnableFlags(com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI)
+    public void onWallpaperColorsChanged_homeWallpaper_shouldUpdateTheme() {
+        // Should ask for a new theme when the colors of the last applied wallpaper change
+        WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
+                Color.valueOf(Color.BLUE), null);
+
+        String jsonString =
+                "{\"android.theme.customization.color_source\":\"home_wallpaper\","
+                        + "\"android.theme.customization.system_palette\":\"A16B00\","
+                        + "\"android.theme.customization.accent_color\":\"A16B00\","
+                        + "\"android.theme.customization.color_index\":\"2\"}";
+
+        when(mSecureSettings.getStringForUser(
+                eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
+                .thenReturn(jsonString);
+        when(mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_SYSTEM, USER_SYSTEM))
+                .thenReturn(1);
+        // Set LOCK wallpaper as the last applied one to verify that theme is no longer based on
+        // latest wallpaper
+        when(mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_LOCK, USER_SYSTEM))
+                .thenReturn(2);
+
+        mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM,
+                USER_SYSTEM);
+
+        ArgumentCaptor<String> updatedSetting = ArgumentCaptor.forClass(String.class);
+        verify(mSecureSettings).putStringForUser(
+                eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), updatedSetting.capture(),
+                anyInt());
+
+        verify(mThemeOverlayApplier)
+                .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
+    }
+
+
+
+    @Test
+    @EnableFlags(com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI)
+    public void onWallpaperColorsChanged_homeWallpaperWithSameColor_shouldKeepThemeAndReapply() {
+        // Shouldn't ask for a new theme when the colors of the last applied wallpaper change
+        // with the same specified system palette one.
+        WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
+                Color.valueOf(0xffa16b00), null);
+
+        String jsonString =
+                "{\"android.theme.customization.color_source\":\"home_wallpaper\","
+                        + "\"android.theme.customization.system_palette\":\"A16B00\","
+                        + "\"android.theme.customization.accent_color\":\"A16B00\","
+                        + "\"android.theme.customization.color_index\":\"2\"}";
+
+        when(mSecureSettings.getStringForUser(
+                eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
+                .thenReturn(jsonString);
+        when(mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_SYSTEM, USER_SYSTEM))
+                .thenReturn(1);
+        // Set LOCK wallpaper as the last applied one to verify that theme is no longer based on
+        // latest wallpaper
+        when(mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_LOCK, USER_SYSTEM))
+                .thenReturn(2);
+
+        mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM,
+                USER_SYSTEM);
+
+        ArgumentCaptor<String> updatedSetting = ArgumentCaptor.forClass(String.class);
+        verify(mSecureSettings, never()).putString(
+                eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), updatedSetting.capture());
+
+        // Apply overlay by existing theme from secure setting
+        verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
+    }
+
+    @Test
+    @EnableFlags(com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI)
+    public void onWallpaperColorsChanged_lockWallpaper_shouldKeepTheme() {
+        // Should ask for a new theme when the colors of the last applied wallpaper change
+        WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
+                Color.valueOf(Color.BLUE), null);
+
+        String jsonString =
+                "{\"android.theme.customization.color_source\":\"home_wallpaper\","
+                        + "\"android.theme.customization.system_palette\":\"A16B00\","
+                        + "\"android.theme.customization.accent_color\":\"A16B00\","
+                        + "\"android.theme.customization.color_index\":\"2\"}";
+
+        when(mSecureSettings.getStringForUser(
+                eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
+                .thenReturn(jsonString);
+        when(mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_SYSTEM, USER_SYSTEM))
+                .thenReturn(1);
+        // Set LOCK wallpaper as the last applied one to verify that theme is no longer based on
+        // latest wallpaper
+        when(mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_LOCK, USER_SYSTEM))
+                .thenReturn(2);
+
+        mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_LOCK,
+                USER_SYSTEM);
+
+        ArgumentCaptor<String> updatedSetting = ArgumentCaptor.forClass(String.class);
+        verify(mSecureSettings, never()).putString(
+                eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), updatedSetting.capture());
+
+        verify(mThemeOverlayApplier, never())
+                .applyCurrentUserOverlays(any(), any(), anyInt(), any(), any());
+    }
+
+    @Test
+    @DisableFlags(com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     public void onWallpaperColorsChanged_resetThemeWhenFromLatestWallpaper() {
         // Should ask for a new theme when the colors of the last applied wallpaper change
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
@@ -594,6 +703,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     public void onWallpaperColorsChanged_keepThemeWhenFromLatestWallpaperAndSpecifiedColor() {
         // Shouldn't ask for a new theme when the colors of the last applied wallpaper change
         // with the same specified system palette one.
@@ -627,6 +737,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     public void onWallpaperColorsChanged_keepThemeIfNotLatestWallpaper() {
         // Shouldn't ask for a new theme when the colors of the wallpaper that is not the last
         // applied one change
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index d4a52c3..c8ef093 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -383,6 +383,10 @@
     <!-- Whether to show activity indicators in the status bar -->
     <bool name="config_showActivity">false</bool>
 
+    <!-- Whether to show the opportunistic satellite icon. When true, an icon will show to indicate
+         satellite capabilities when all other connections are out of service. -->
+    <bool name="config_showOpportunisticSatelliteIcon">true</bool>
+
     <!-- Whether or not to show the notification shelf that houses the icons of notifications that
      have been scrolled off-screen. -->
     <bool name="config_showNotificationShelf">true</bool>
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt
index d1c728c..b0f5e5e 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt
@@ -26,11 +26,6 @@
 @Module
 abstract class CommonDataLayerModule {
     @Binds
-    abstract fun bindConfigurationRepository(
-        impl: ConfigurationRepositoryImpl
-    ): ConfigurationRepository
-
-    @Binds
     abstract fun bindPackageChangeRepository(
         impl: PackageChangeRepositoryImpl
     ): PackageChangeRepository
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationStateModule.kt b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationStateModule.kt
index b36da3b..e0756fc 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationStateModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationStateModule.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.common.ui
 
 import android.content.Context
+import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryModule
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -28,7 +29,7 @@
 /**
  * Annotates elements that provide information from the global configuration.
  *
- * The global configuration is the one associted with the main display. Secondary displays will
+ * The global configuration is the one associated with the main display. Secondary displays will
  * apply override to the global configuration. Elements annotated with this shouldn't be used for
  * secondary displays.
  */
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
index 2052c70..31a7fa4 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
@@ -23,13 +23,17 @@
 import androidx.annotation.DimenRes
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.common.ui.GlobalConfig
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.wrapper.DisplayUtilsWrapper
 import dagger.Binds
 import dagger.Module
-import javax.inject.Inject
+import dagger.Provides
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
@@ -57,66 +61,62 @@
     fun getDimensionPixelSize(id: Int): Int
 }
 
-@SysUISingleton
 class ConfigurationRepositoryImpl
-@Inject
+@AssistedInject
 constructor(
-    private val configurationController: ConfigurationController,
-    private val context: Context,
+    @Assisted private val configurationController: ConfigurationController,
+    @Assisted private val context: Context,
     @Application private val scope: CoroutineScope,
     private val displayUtils: DisplayUtilsWrapper,
 ) : ConfigurationRepository {
     private val displayInfo = MutableStateFlow(DisplayInfo())
 
-    override val onAnyConfigurationChange: Flow<Unit> =
-        conflatedCallbackFlow {
-            val callback =
-                object : ConfigurationController.ConfigurationListener {
-                    override fun onUiModeChanged() {
-                        sendUpdate("ConfigurationRepository#onUiModeChanged")
-                    }
-
-                    override fun onThemeChanged() {
-                        sendUpdate("ConfigurationRepository#onThemeChanged")
-                    }
-
-                    override fun onConfigChanged(newConfig: Configuration) {
-                        sendUpdate("ConfigurationRepository#onConfigChanged")
-                    }
-
-                    fun sendUpdate(reason: String) {
-                        trySendWithFailureLogging(Unit, reason)
-                    }
+    override val onAnyConfigurationChange: Flow<Unit> = conflatedCallbackFlow {
+        val callback =
+            object : ConfigurationController.ConfigurationListener {
+                override fun onUiModeChanged() {
+                    sendUpdate("ConfigurationRepository#onUiModeChanged")
                 }
-            configurationController.addCallback(callback)
-            awaitClose { configurationController.removeCallback(callback) }
-        }
 
-    override val onConfigurationChange: Flow<Unit> =
-        conflatedCallbackFlow {
-            val callback =
-                object : ConfigurationController.ConfigurationListener {
-                    override fun onConfigChanged(newConfig: Configuration) {
-                        trySendWithFailureLogging(Unit, "ConfigurationRepository#onConfigChanged")
-                    }
+                override fun onThemeChanged() {
+                    sendUpdate("ConfigurationRepository#onThemeChanged")
                 }
-            configurationController.addCallback(callback)
-            awaitClose { configurationController.removeCallback(callback) }
-        }
 
-    override val configurationValues: Flow<Configuration> =
-            conflatedCallbackFlow {
-                val callback =
-                        object : ConfigurationController.ConfigurationListener {
-                            override fun onConfigChanged(newConfig: Configuration) {
-                                trySend(newConfig)
-                            }
-                        }
+                override fun onConfigChanged(newConfig: Configuration) {
+                    sendUpdate("ConfigurationRepository#onConfigChanged")
+                }
 
-                trySend(context.resources.configuration)
-                configurationController.addCallback(callback)
-                awaitClose { configurationController.removeCallback(callback) }
+                fun sendUpdate(reason: String) {
+                    trySendWithFailureLogging(Unit, reason)
+                }
             }
+        configurationController.addCallback(callback)
+        awaitClose { configurationController.removeCallback(callback) }
+    }
+
+    override val onConfigurationChange: Flow<Unit> = conflatedCallbackFlow {
+        val callback =
+            object : ConfigurationController.ConfigurationListener {
+                override fun onConfigChanged(newConfig: Configuration) {
+                    trySendWithFailureLogging(Unit, "ConfigurationRepository#onConfigChanged")
+                }
+            }
+        configurationController.addCallback(callback)
+        awaitClose { configurationController.removeCallback(callback) }
+    }
+
+    override val configurationValues: Flow<Configuration> = conflatedCallbackFlow {
+        val callback =
+            object : ConfigurationController.ConfigurationListener {
+                override fun onConfigChanged(newConfig: Configuration) {
+                    trySend(newConfig)
+                }
+            }
+
+        trySend(context.resources.configuration)
+        configurationController.addCallback(callback)
+        awaitClose { configurationController.removeCallback(callback) }
+    }
 
     override val scaleForResolution: StateFlow<Float> =
         onConfigurationChange
@@ -134,8 +134,7 @@
                     maxDisplayMode.physicalWidth,
                     maxDisplayMode.physicalHeight,
                     displayInfo.value.naturalWidth,
-                    displayInfo.value.naturalHeight
-                )
+                    displayInfo.value.naturalHeight)
             return if (scaleFactor == Float.POSITIVE_INFINITY) 1f else scaleFactor
         }
         return 1f
@@ -144,9 +143,40 @@
     override fun getDimensionPixelSize(@DimenRes id: Int): Int {
         return context.resources.getDimensionPixelSize(id)
     }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            context: Context,
+            configurationController: ConfigurationController
+        ): ConfigurationRepositoryImpl
+    }
 }
 
 @Module
-interface ConfigurationRepositoryModule {
-    @Binds fun bindImpl(impl: ConfigurationRepositoryImpl): ConfigurationRepository
+abstract class ConfigurationRepositoryModule {
+
+    /**
+     * For compatibility reasons. Ideally, only the an annotated [ConfigurationRepository] should be
+     * injected.
+     */
+    @Binds
+    @Deprecated("Use the ConfigurationRepository annotated with @GlobalConfig instead.")
+    @SysUISingleton
+    abstract fun provideDefaultConfigRepository(
+        @GlobalConfig configurationRepository: ConfigurationRepository
+    ): ConfigurationRepository
+
+    companion object {
+        @Provides
+        @GlobalConfig
+        @SysUISingleton
+        fun provideGlobalConfigRepository(
+            context: Context,
+            @GlobalConfig configurationController: ConfigurationController,
+            factory: ConfigurationRepositoryImpl.Factory
+        ): ConfigurationRepository {
+            return factory.create(context, configurationController)
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt b/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt
index adb1ee2..eb423d6 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt
@@ -32,6 +32,7 @@
 import kotlinx.coroutines.flow.onStart
 
 /** Business logic related to configuration changes. */
+// TODO: b/374267505 - Create a @ShadeDisplayWindow annotated version of this.
 @SysUISingleton
 class ConfigurationInteractor @Inject constructor(private val repository: ConfigurationRepository) {
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index cb649f2..9138243 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -49,6 +49,7 @@
 import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule;
 import com.android.systemui.common.data.CommonDataLayerModule;
 import com.android.systemui.common.ui.ConfigurationStateModule;
+import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryModule;
 import com.android.systemui.common.usagestats.data.CommonUsageStatsDataLayerModule;
 import com.android.systemui.communal.dagger.CommunalModule;
 import com.android.systemui.complication.dagger.ComplicationComponent;
@@ -212,6 +213,7 @@
         CommunalModule.class,
         CommonDataLayerModule.class,
         ConfigurationStateModule.class,
+        ConfigurationRepositoryModule.class,
         CommonUsageStatsDataLayerModule.class,
         ConfigurationControllerModule.class,
         ConnectivityModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
index 96c0cac..40613c0 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
@@ -149,6 +149,7 @@
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.data.repository.LightBarControllerStore;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -258,8 +259,7 @@
     private boolean mTransientShownFromGestureOnSystemBar;
     private int mNavBarMode = NAV_BAR_MODE_3BUTTON;
     private LightBarController mLightBarController;
-    private final LightBarController mMainLightBarController;
-    private final LightBarController.Factory mLightBarControllerFactory;
+    private final LightBarControllerStore mLightBarControllerStore;
     private AutoHideController mAutoHideController;
     private final AutoHideController mMainAutoHideController;
     private final AutoHideController.Factory mAutoHideControllerFactory;
@@ -580,8 +580,7 @@
             @Background Executor bgExecutor,
             UiEventLogger uiEventLogger,
             NavBarHelper navBarHelper,
-            LightBarController mainLightBarController,
-            LightBarController.Factory lightBarControllerFactory,
+            LightBarControllerStore lightBarControllerStore,
             AutoHideController mainAutoHideController,
             AutoHideController.Factory autoHideControllerFactory,
             Optional<TelecomManager> telecomManagerOptional,
@@ -628,8 +627,7 @@
         mUiEventLogger = uiEventLogger;
         mNavBarHelper = navBarHelper;
         mNotificationShadeDepthController = notificationShadeDepthController;
-        mMainLightBarController = mainLightBarController;
-        mLightBarControllerFactory = lightBarControllerFactory;
+        mLightBarControllerStore = lightBarControllerStore;
         mMainAutoHideController = mainAutoHideController;
         mAutoHideControllerFactory = autoHideControllerFactory;
         mTelecomManagerOptional = telecomManagerOptional;
@@ -842,8 +840,7 @@
         // Unfortunately, we still need it because status bar needs LightBarController
         // before notifications creation. We cannot directly use getLightBarController()
         // from NavigationBarFragment directly.
-        LightBarController lightBarController = mIsOnDefaultDisplay
-                ? mMainLightBarController : mLightBarControllerFactory.create(mContext);
+        LightBarController lightBarController = mLightBarControllerStore.forDisplay(mDisplayId);
         setLightBarController(lightBarController);
 
         // TODO(b/118592525): to support multi-display, we start to add something which is
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 0e82bf8..c15c8f9 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -2053,6 +2053,9 @@
         }
         if (mQsController.getExpanded()) {
             mQsController.flingQs(0, FLING_COLLAPSE);
+        } else if (mBarState == KEYGUARD) {
+            mLockscreenShadeTransitionController.goToLockedShade(
+                    /* expandedView= */null, /* needsQSAnimation= */false);
         } else {
             expand(true /* animate */);
         }
@@ -3109,7 +3112,7 @@
         if (isTracking()) {
             onTrackingStopped(true);
         }
-        if (isExpanded() && !mQsController.getExpanded()) {
+        if (isExpanded() && mBarState != KEYGUARD && !mQsController.getExpanded()) {
             mShadeLog.d("Status Bar was long pressed. Expanding to QS.");
             expandToQs();
         } else {
@@ -5091,13 +5094,6 @@
             }
             boolean handled = mHeadsUpTouchHelper.onTouchEvent(event);
 
-            if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && mQsController.handleTouch(
-                    event, isFullyCollapsed(), isShadeOrQsHeightAnimationRunning())) {
-                if (event.getActionMasked() != MotionEvent.ACTION_MOVE) {
-                    mShadeLog.logMotionEvent(event, "onTouch: handleQsTouch handled event");
-                }
-                return true;
-            }
             // This touch session has already resulted in shade expansion. Ignore everything else.
             if (ShadeExpandsOnStatusBarLongPress.isEnabled()
                     && event.getActionMasked() != MotionEvent.ACTION_DOWN
@@ -5105,6 +5101,13 @@
                 mShadeLog.d("Touch has same down time as Status Bar long press. Ignoring.");
                 return false;
             }
+            if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && mQsController.handleTouch(
+                    event, isFullyCollapsed(), isShadeOrQsHeightAnimationRunning())) {
+                if (event.getActionMasked() != MotionEvent.ACTION_MOVE) {
+                    mShadeLog.logMotionEvent(event, "onTouch: handleQsTouch handled event");
+                }
+                return true;
+            }
             if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) {
                 mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
                 handled = true;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
index a8026cd..32255c6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
@@ -20,7 +20,11 @@
 import android.content.res.Resources
 import android.view.LayoutInflater
 import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.common.ui.ConfigurationStateImpl
 import com.android.systemui.common.ui.GlobalConfig
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.res.R
 import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
@@ -97,4 +101,36 @@
         ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()
         return shadeConfigurationController
     }
+
+    @SysUISingleton
+    @Provides
+    @ShadeDisplayAware
+    fun provideShadeDisplayAwareConfigurationState(
+        factory: ConfigurationStateImpl.Factory,
+        @ShadeDisplayAware configurationController: ConfigurationController,
+        @ShadeDisplayAware context: Context,
+        @GlobalConfig configurationState: ConfigurationState,
+    ): ConfigurationState {
+        return if (ShadeWindowGoesAround.isEnabled) {
+            factory.create(context, configurationController)
+        } else {
+            configurationState
+        }
+    }
+
+    @SysUISingleton
+    @Provides
+    @ShadeDisplayAware
+    fun provideShadeDisplayAwareConfigurationRepository(
+        factory: ConfigurationRepositoryImpl.Factory,
+        @ShadeDisplayAware configurationController: ConfigurationController,
+        @ShadeDisplayAware context: Context,
+        @GlobalConfig globalConfigurationRepository: ConfigurationRepository
+    ): ConfigurationRepository {
+        return if (ShadeWindowGoesAround.isEnabled) {
+            factory.create(context, configurationController)
+        } else {
+            globalConfigurationRepository
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LongPressGestureDetector.kt b/packages/SystemUI/src/com/android/systemui/shade/StatusBarLongPressGestureDetector.kt
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/shade/LongPressGestureDetector.kt
rename to packages/SystemUI/src/com/android/systemui/shade/StatusBarLongPressGestureDetector.kt
index 6fb3ca5..ae36e81 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/LongPressGestureDetector.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/StatusBarLongPressGestureDetector.kt
@@ -25,7 +25,7 @@
 
 /** Accepts touch events, detects long press, and calls ShadeViewController#onStatusBarLongPress. */
 @SysUISingleton
-class LongPressGestureDetector
+class StatusBarLongPressGestureDetector
 @Inject
 constructor(context: Context, val shadeViewController: ShadeViewController) {
     val gestureDetector =
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
index e5d08a0..a8d616c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
@@ -51,7 +51,7 @@
     @Application private val applicationScope: CoroutineScope,
     @ShadeDisplayAware private val context: Context,
     @ShadeTouchLog private val touchLog: LogBuffer,
-    private val configurationRepository: ConfigurationRepository,
+    @ShadeDisplayAware private val configurationRepository: ConfigurationRepository,
     private val shadeRepository: ShadeRepository,
     private val splitShadeStateController: SplitShadeStateController,
     private val scrimShadeTransitionController: ScrimShadeTransitionController,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index f65ae67..4341200 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.log.LogBufferFactory
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.data.StatusBarDataLayerModule
+import com.android.systemui.statusbar.data.repository.LightBarControllerStore
 import com.android.systemui.statusbar.phone.LightBarController
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProviderImpl
@@ -55,26 +56,26 @@
  *   [com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule], etc.).
  */
 @Module(includes = [StatusBarDataLayerModule::class, SystemBarUtilsProxyImpl.Module::class])
-abstract class StatusBarModule {
+interface StatusBarModule {
 
     @Binds
     @IntoMap
     @ClassKey(OngoingCallController::class)
-    abstract fun bindOngoingCallController(impl: OngoingCallController): CoreStartable
+    fun bindOngoingCallController(impl: OngoingCallController): CoreStartable
 
     @Binds
     @IntoMap
     @ClassKey(LightBarController::class)
-    abstract fun bindLightBarController(impl: LightBarController): CoreStartable
+    fun lightBarControllerAsCoreStartable(controller: LightBarController): CoreStartable
 
     @Binds
     @IntoMap
     @ClassKey(StatusBarSignalPolicy::class)
-    abstract fun bindStatusBarSignalPolicy(impl: StatusBarSignalPolicy): CoreStartable
+    fun bindStatusBarSignalPolicy(impl: StatusBarSignalPolicy): CoreStartable
 
     @Binds
     @SysUISingleton
-    abstract fun statusBarWindowControllerFactory(
+    fun statusBarWindowControllerFactory(
         implFactory: StatusBarWindowControllerImpl.Factory
     ): StatusBarWindowController.Factory
 
@@ -82,6 +83,12 @@
 
         @Provides
         @SysUISingleton
+        fun lightBarController(store: LightBarControllerStore): LightBarController {
+            return store.defaultDisplay
+        }
+
+        @Provides
+        @SysUISingleton
         fun windowControllerStore(
             multiDisplayImplLazy: Lazy<MultiDisplayStatusBarWindowControllerStore>,
             singleDisplayImplLazy: Lazy<SingleDisplayStatusBarWindowControllerStore>,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt
index f2d926f..39de28e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.statusbar.data
 
 import com.android.systemui.statusbar.data.repository.KeyguardStatusBarRepositoryModule
+import com.android.systemui.statusbar.data.repository.LightBarControllerStoreModule
 import com.android.systemui.statusbar.data.repository.RemoteInputRepositoryModule
 import com.android.systemui.statusbar.data.repository.StatusBarConfigurationControllerModule
 import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStoreModule
@@ -28,6 +29,7 @@
     includes =
         [
             KeyguardStatusBarRepositoryModule::class,
+            LightBarControllerStoreModule::class,
             RemoteInputRepositoryModule::class,
             StatusBarConfigurationControllerModule::class,
             StatusBarContentInsetsProviderStoreModule::class,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/LightBarControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/LightBarControllerStore.kt
new file mode 100644
index 0000000..ff50e31
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/LightBarControllerStore.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 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.data.repository
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.display.data.repository.DisplayRepository
+import com.android.systemui.display.data.repository.DisplayScopeRepository
+import com.android.systemui.display.data.repository.PerDisplayStore
+import com.android.systemui.display.data.repository.PerDisplayStoreImpl
+import com.android.systemui.statusbar.phone.LightBarController
+import com.android.systemui.statusbar.phone.LightBarControllerImpl
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+
+/** Provides per display instances of [LightBarController]. */
+interface LightBarControllerStore : PerDisplayStore<LightBarController>
+
+@SysUISingleton
+class LightBarControllerStoreImpl
+@Inject
+constructor(
+    @Background backgroundApplicationScope: CoroutineScope,
+    displayRepository: DisplayRepository,
+    private val factory: LightBarControllerImpl.Factory,
+    private val displayScopeRepository: DisplayScopeRepository,
+    private val statusBarModeRepositoryStore: StatusBarModeRepositoryStore,
+) :
+    LightBarControllerStore,
+    PerDisplayStoreImpl<LightBarController>(backgroundApplicationScope, displayRepository) {
+
+    override fun createInstanceForDisplay(displayId: Int): LightBarController {
+        return factory
+            .create(
+                displayId,
+                displayScopeRepository.scopeForDisplay(displayId),
+                statusBarModeRepositoryStore.forDisplay(displayId),
+            )
+            .also { it.start() }
+    }
+
+    override suspend fun onDisplayRemovalAction(instance: LightBarController) {
+        instance.stop()
+    }
+
+    override val instanceClass = LightBarController::class.java
+}
+
+@Module
+interface LightBarControllerStoreModule {
+
+    @Binds fun store(impl: LightBarControllerStoreImpl): LightBarControllerStore
+
+    @Binds
+    @IntoMap
+    @ClassKey(LightBarControllerStore::class)
+    fun storeAsCoreStartable(impl: LightBarControllerStoreImpl): CoreStartable
+}
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 99efba4..dceee22 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -171,12 +171,14 @@
 import com.android.systemui.shade.NotificationShadeWindowViewController;
 import com.android.systemui.shade.QuickSettingsController;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeExpandsOnStatusBarLongPress;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.shade.ShadeExpansionListener;
 import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.shade.ShadeLogger;
 import com.android.systemui.shade.ShadeSurface;
 import com.android.systemui.shade.ShadeViewController;
+import com.android.systemui.shade.StatusBarLongPressGestureDetector;
 import com.android.systemui.shared.recents.utilities.Utilities;
 import com.android.systemui.shared.statusbar.phone.BarTransitions;
 import com.android.systemui.statusbar.AutoHideUiElement;
@@ -366,6 +368,7 @@
 
     private PhoneStatusBarViewController mPhoneStatusBarViewController;
     private PhoneStatusBarTransitions mStatusBarTransitions;
+    private final Provider<StatusBarLongPressGestureDetector> mStatusBarLongPressGestureDetector;
     private final AuthRippleController mAuthRippleController;
     @WindowVisibleState private int mStatusBarWindowState = WINDOW_STATE_SHOWING;
     private final NotificationShadeWindowController mNotificationShadeWindowController;
@@ -671,6 +674,7 @@
             ShadeController shadeController,
             WindowRootViewVisibilityInteractor windowRootViewVisibilityInteractor,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
+            Provider<StatusBarLongPressGestureDetector> statusBarLongPressGestureDetector,
             ViewMediatorCallback viewMediatorCallback,
             InitController initController,
             @Named(TIME_TICK_HANDLER_NAME) Handler timeTickHandler,
@@ -778,6 +782,7 @@
         mShadeController = shadeController;
         mWindowRootViewVisibilityInteractor = windowRootViewVisibilityInteractor;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+        mStatusBarLongPressGestureDetector = statusBarLongPressGestureDetector;
         mKeyguardViewMediatorCallback = viewMediatorCallback;
         mInitController = initController;
         mPluginDependencyProvider = pluginDependencyProvider;
@@ -1527,6 +1532,11 @@
                 // to touch outside the customizer to close it, such as on the status or nav bar.
                 mShadeController.onStatusBarTouch(event);
             }
+            if (ShadeExpandsOnStatusBarLongPress.isEnabled()
+                    && mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
+                mStatusBarLongPressGestureDetector.get().handleTouch(event);
+            }
+
             return getNotificationShadeWindowView().onTouchEvent(event);
         };
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.kt
new file mode 100644
index 0000000..b5b3162
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2024 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.view.WindowInsetsController
+import com.android.internal.colorextraction.ColorExtractor
+import com.android.internal.view.AppearanceRegion
+import com.android.systemui.CoreStartable
+
+/** Controls how light status bar flag applies to the icons. */
+interface LightBarController : CoreStartable {
+
+    fun stop()
+
+    fun setNavigationBar(navigationBar: LightBarTransitionsController)
+
+    fun setBiometricUnlockController(biometricUnlockController: BiometricUnlockController)
+
+    fun onNavigationBarAppearanceChanged(
+        @WindowInsetsController.Appearance appearance: Int,
+        nbModeChanged: Boolean,
+        navigationBarMode: Int,
+        navbarColorManagedByIme: Boolean,
+    )
+
+    fun onNavigationBarModeChanged(newBarMode: Int)
+
+    fun setQsCustomizing(customizing: Boolean)
+
+    /** Set if Quick Settings is fully expanded, which affects notification scrim visibility. */
+    fun setQsExpanded(expanded: Boolean)
+
+    /** Set if Global Actions dialog is visible, which requires dark mode (light buttons). */
+    fun setGlobalActionsVisible(visible: Boolean)
+
+    /**
+     * Controls the light status bar temporarily for back navigation.
+     *
+     * @param appearance the customized appearance.
+     */
+    fun customizeStatusBarAppearance(appearance: AppearanceRegion)
+
+    /**
+     * Sets whether the direct-reply is in use or not.
+     *
+     * @param directReplying `true` when the direct-reply is in-use.
+     */
+    fun setDirectReplying(directReplying: Boolean)
+
+    fun setScrimState(
+        scrimState: ScrimState,
+        scrimBehindAlpha: Float,
+        scrimInFrontColor: ColorExtractor.GradientColors,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarControllerImpl.java
similarity index 82%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarControllerImpl.java
index a33996b..6ff9f4c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarControllerImpl.java
@@ -11,7 +11,7 @@
  * 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
+ * limitations under the License.
  */
 
 package com.android.systemui.statusbar.phone;
@@ -22,9 +22,9 @@
 import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
 import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
 
-import android.content.Context;
 import android.graphics.Rect;
 import android.util.Log;
+import android.view.Display;
 import android.view.InsetsFlags;
 import android.view.ViewDebug;
 import android.view.WindowInsetsController.Appearance;
@@ -34,30 +34,32 @@
 
 import com.android.internal.colorextraction.ColorExtractor.GradientColors;
 import com.android.internal.view.AppearanceRegion;
-import com.android.systemui.CoreStartable;
-import com.android.systemui.Dumpable;
-import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.DarkIconDispatcher;
-import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.statusbar.data.model.StatusBarAppearance;
-import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore;
+import com.android.systemui.statusbar.data.repository.StatusBarModePerDisplayRepository;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.util.Compile;
-import com.android.systemui.util.kotlin.JavaAdapter;
+import com.android.systemui.util.kotlin.JavaAdapterKt;
+
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
+
+import kotlin.coroutines.CoroutineContext;
+
+import kotlinx.coroutines.CoroutineScope;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
 
-import javax.inject.Inject;
-
 /**
  * Controls how light status bar flag applies to the icons.
  */
-@SysUISingleton
-public class LightBarController implements
-        BatteryController.BatteryStateChangeCallback, Dumpable, CoreStartable {
+public class LightBarControllerImpl implements
+        BatteryController.BatteryStateChangeCallback, LightBarController {
 
     private static final String TAG = "LightBarController";
     private static final boolean DEBUG_NAVBAR = Compile.IS_DEBUG;
@@ -65,10 +67,13 @@
 
     private static final float NAV_BAR_INVERSION_SCRIM_ALPHA_THRESHOLD = 0.1f;
 
-    private final JavaAdapter mJavaAdapter;
+    private final CoroutineScope mCoroutineScope;
     private final SysuiDarkIconDispatcher mStatusBarIconController;
     private final BatteryController mBatteryController;
-    private final StatusBarModeRepositoryStore mStatusBarModeRepository;
+    private final NavigationModeController mNavModeController;
+    private final DumpManager mDumpManager;
+    private final StatusBarModePerDisplayRepository mStatusBarModeRepository;
+    private final CoroutineContext mMainContext;
     private BiometricUnlockController mBiometricUnlockController;
 
     private LightBarTransitionsController mNavigationBarController;
@@ -119,42 +124,59 @@
     private String mLastNavigationBarAppearanceChangedLog;
     private StringBuilder mLogStringBuilder = null;
 
-    @Inject
-    public LightBarController(
-            Context ctx,
-            JavaAdapter javaAdapter,
+    private final String mDumpableName;
+
+    private final NavigationModeController.ModeChangedListener mNavigationModeListener =
+            (mode) -> mNavigationMode = mode;
+
+    @AssistedInject
+    public LightBarControllerImpl(
+            @Assisted int displayId,
+            @Assisted CoroutineScope coroutineScope,
             DarkIconDispatcher darkIconDispatcher,
             BatteryController batteryController,
             NavigationModeController navModeController,
-            StatusBarModeRepositoryStore statusBarModeRepository,
+            @Assisted StatusBarModePerDisplayRepository statusBarModeRepository,
             DumpManager dumpManager,
-            DisplayTracker displayTracker) {
-        mJavaAdapter = javaAdapter;
+            @Main CoroutineContext mainContext) {
+        mCoroutineScope = coroutineScope;
         mStatusBarIconController = (SysuiDarkIconDispatcher) darkIconDispatcher;
         mBatteryController = batteryController;
-        mBatteryController.addCallback(this);
+        mNavModeController = navModeController;
+        mDumpManager = dumpManager;
         mStatusBarModeRepository = statusBarModeRepository;
-        mNavigationMode = navModeController.addListener((mode) -> {
-            mNavigationMode = mode;
-        });
-
-        if (ctx.getDisplayId() == displayTracker.getDefaultDisplayId()) {
-            dumpManager.registerDumpable(getClass().getSimpleName(), this);
-        }
+        mMainContext = mainContext;
+        String dumpableNameSuffix =
+                displayId == Display.DEFAULT_DISPLAY ? "" : String.valueOf(displayId);
+        mDumpableName = getClass().getSimpleName() + dumpableNameSuffix;
     }
 
     @Override
     public void start() {
-        mJavaAdapter.alwaysCollectFlow(
-                mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance(),
+        mDumpManager.registerCriticalDumpable(mDumpableName, this);
+        mBatteryController.addCallback(this);
+        mNavigationMode = mNavModeController.addListener(mNavigationModeListener);
+        JavaAdapterKt.collectFlow(
+                mCoroutineScope,
+                mMainContext,
+                mStatusBarModeRepository.getStatusBarAppearance(),
                 this::onStatusBarAppearanceChanged);
     }
 
+    @Override
+    public void stop() {
+        mDumpManager.unregisterDumpable(mDumpableName);
+        mBatteryController.removeCallback(this);
+        mNavModeController.removeListener(mNavigationModeListener);
+    }
+
+    @Override
     public void setNavigationBar(LightBarTransitionsController navigationBar) {
         mNavigationBarController = navigationBar;
         updateNavigation();
     }
 
+    @Override
     public void setBiometricUnlockController(
             BiometricUnlockController biometricUnlockController) {
         mBiometricUnlockController = biometricUnlockController;
@@ -202,6 +224,7 @@
         mNavbarColorManagedByIme = navbarColorManagedByIme;
     }
 
+    @Override
     public void onNavigationBarAppearanceChanged(@Appearance int appearance, boolean nbModeChanged,
             int navigationBarMode, boolean navbarColorManagedByIme) {
         int diff = appearance ^ mAppearance;
@@ -244,6 +267,7 @@
         mNavbarColorManagedByIme = navbarColorManagedByIme;
     }
 
+    @Override
     public void onNavigationBarModeChanged(int newBarMode) {
         mHasLightNavigationBar = isLight(mAppearance, newBarMode, APPEARANCE_LIGHT_NAVIGATION_BARS);
     }
@@ -258,30 +282,28 @@
                 mNavigationBarMode, mNavbarColorManagedByIme);
     }
 
+    @Override
     public void setQsCustomizing(boolean customizing) {
         if (mQsCustomizing == customizing) return;
         mQsCustomizing = customizing;
         reevaluate();
     }
 
-    /** Set if Quick Settings is fully expanded, which affects notification scrim visibility */
+    @Override
     public void setQsExpanded(boolean expanded) {
         if (mQsExpanded == expanded) return;
         mQsExpanded = expanded;
         reevaluate();
     }
 
-    /** Set if Global Actions dialog is visible, which requires dark mode (light buttons) */
+    @Override
     public void setGlobalActionsVisible(boolean visible) {
         if (mGlobalActionsVisible == visible) return;
         mGlobalActionsVisible = visible;
         reevaluate();
     }
 
-    /**
-     * Controls the light status bar temporarily for back navigation.
-     * @param appearance the custmoized appearance.
-     */
+    @Override
     public void customizeStatusBarAppearance(AppearanceRegion appearance) {
         if (appearance != null) {
             final ArrayList<AppearanceRegion> appearancesList = new ArrayList<>();
@@ -303,16 +325,14 @@
         }
     }
 
-    /**
-     * Sets whether the direct-reply is in use or not.
-     * @param directReplying {@code true} when the direct-reply is in-use.
-     */
+    @Override
     public void setDirectReplying(boolean directReplying) {
         if (mDirectReplying == directReplying) return;
         mDirectReplying = directReplying;
         reevaluate();
     }
 
+    @Override
     public void setScrimState(ScrimState scrimState, float scrimBehindAlpha,
             GradientColors scrimInFrontColor) {
         boolean bouncerVisibleLast = mBouncerVisible;
@@ -387,20 +407,17 @@
             }
         }
 
-        // If no one is light, all icons become white.
         if (lightBarBounds.isEmpty()) {
-            mStatusBarIconController.getTransitionsController().setIconsDark(
-                    false, animateChange());
-        }
-
-        // If all stacks are light, all icons get dark.
-        else if (lightBarBounds.size() == numStacks) {
+            // If no one is light, all icons become white.
+            mStatusBarIconController
+                    .getTransitionsController()
+                    .setIconsDark(false, animateChange());
+        } else if (lightBarBounds.size() == numStacks) {
+            // If all stacks are light, all icons get dark.
             mStatusBarIconController.setIconsDarkArea(null);
             mStatusBarIconController.getTransitionsController().setIconsDark(true, animateChange());
-        }
-
-        // Not the same for every stack, magic!
-        else {
+        } else {
+            // Not the same for every stack, magic!
             mStatusBarIconController.setIconsDarkArea(lightBarBounds);
             mStatusBarIconController.getTransitionsController().setIconsDark(true, animateChange());
         }
@@ -468,47 +485,15 @@
         }
     }
 
-    /**
-     * Injectable factory for creating a {@link LightBarController}.
-     */
-    public static class Factory {
-        private final JavaAdapter mJavaAdapter;
-        private final DarkIconDispatcher mDarkIconDispatcher;
-        private final BatteryController mBatteryController;
-        private final NavigationModeController mNavModeController;
-        private final StatusBarModeRepositoryStore mStatusBarModeRepository;
-        private final DumpManager mDumpManager;
-        private final DisplayTracker mDisplayTracker;
+    /** Injectable factory for creating a {@link LightBarControllerImpl}. */
+    @AssistedFactory
+    @FunctionalInterface
+    public interface Factory {
 
-        @Inject
-        public Factory(
-                JavaAdapter javaAdapter,
-                DarkIconDispatcher darkIconDispatcher,
-                BatteryController batteryController,
-                NavigationModeController navModeController,
-                StatusBarModeRepositoryStore statusBarModeRepository,
-                DumpManager dumpManager,
-                DisplayTracker displayTracker) {
-            mJavaAdapter = javaAdapter;
-            mDarkIconDispatcher = darkIconDispatcher;
-            mBatteryController = batteryController;
-            mNavModeController = navModeController;
-            mStatusBarModeRepository = statusBarModeRepository;
-            mDumpManager = dumpManager;
-            mDisplayTracker = displayTracker;
-        }
-
-        /** Create an {@link LightBarController} */
-        public LightBarController create(Context context) {
-            return new LightBarController(
-                    context,
-                    mJavaAdapter,
-                    mDarkIconDispatcher,
-                    mBatteryController,
-                    mNavModeController,
-                    mStatusBarModeRepository,
-                    mDumpManager,
-                    mDisplayTracker);
-        }
+        /** Creates a {@link LightBarControllerImpl}. */
+        LightBarControllerImpl create(
+                int displayId,
+                CoroutineScope coroutineScope,
+                StatusBarModePerDisplayRepository statusBarModePerDisplayRepository);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
index 91c43dd..176dd8d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -39,8 +39,8 @@
 import com.android.systemui.Flags;
 import com.android.systemui.Gefingerpoken;
 import com.android.systemui.res.R;
-import com.android.systemui.shade.LongPressGestureDetector;
 import com.android.systemui.shade.ShadeExpandsOnStatusBarLongPress;
+import com.android.systemui.shade.StatusBarLongPressGestureDetector;
 import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
 import com.android.systemui.statusbar.window.StatusBarWindowControllerStore;
 import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder;
@@ -69,7 +69,7 @@
     private InsetsFetcher mInsetsFetcher;
     private int mDensity;
     private float mFontScale;
-    private LongPressGestureDetector mLongPressGestureDetector;
+    private StatusBarLongPressGestureDetector mStatusBarLongPressGestureDetector;
 
     /**
      * Draw this many pixels into the left/right side of the cutout to optimally use the space
@@ -81,9 +81,10 @@
         mStatusBarWindowControllerStore = Dependency.get(StatusBarWindowControllerStore.class);
     }
 
-    void setLongPressGestureDetector(LongPressGestureDetector longPressGestureDetector) {
+    void setLongPressGestureDetector(
+            StatusBarLongPressGestureDetector statusBarLongPressGestureDetector) {
         if (ShadeExpandsOnStatusBarLongPress.isEnabled()) {
-            mLongPressGestureDetector = longPressGestureDetector;
+            mStatusBarLongPressGestureDetector = statusBarLongPressGestureDetector;
         }
     }
 
@@ -207,8 +208,9 @@
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
-        if (ShadeExpandsOnStatusBarLongPress.isEnabled() && mLongPressGestureDetector != null) {
-            mLongPressGestureDetector.handleTouch(event);
+        if (ShadeExpandsOnStatusBarLongPress.isEnabled()
+                && mStatusBarLongPressGestureDetector != null) {
+            mStatusBarLongPressGestureDetector.handleTouch(event);
         }
         if (mTouchEventHandler == null) {
             Log.w(
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 c24f432..4245494 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -33,11 +33,11 @@
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.ui.view.WindowRootView
-import com.android.systemui.shade.LongPressGestureDetector
 import com.android.systemui.shade.ShadeController
 import com.android.systemui.shade.ShadeExpandsOnStatusBarLongPress
 import com.android.systemui.shade.ShadeLogger
 import com.android.systemui.shade.ShadeViewController
+import com.android.systemui.shade.StatusBarLongPressGestureDetector
 import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
 import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator
 import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
@@ -68,7 +68,7 @@
     private val shadeController: ShadeController,
     private val shadeViewController: ShadeViewController,
     private val panelExpansionInteractor: PanelExpansionInteractor,
-    private val longPressGestureDetector: Provider<LongPressGestureDetector>,
+    private val statusBarLongPressGestureDetector: Provider<StatusBarLongPressGestureDetector>,
     private val windowRootView: Provider<WindowRootView>,
     private val shadeLogger: ShadeLogger,
     private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?,
@@ -118,7 +118,7 @@
         addCursorSupportToIconContainers()
 
         if (ShadeExpandsOnStatusBarLongPress.isEnabled) {
-            mView.setLongPressGestureDetector(longPressGestureDetector.get())
+            mView.setLongPressGestureDetector(statusBarLongPressGestureDetector.get())
         }
 
         progressProvider?.setReadyToHandleTransition(true)
@@ -335,7 +335,7 @@
         private val shadeController: ShadeController,
         private val shadeViewController: ShadeViewController,
         private val panelExpansionInteractor: PanelExpansionInteractor,
-        private val longPressGestureDetector: Provider<LongPressGestureDetector>,
+        private val statusBarLongPressGestureDetector: Provider<StatusBarLongPressGestureDetector>,
         private val windowRootView: Provider<WindowRootView>,
         private val shadeLogger: ShadeLogger,
         private val viewUtil: ViewUtil,
@@ -360,7 +360,7 @@
                 shadeController,
                 shadeViewController,
                 panelExpansionInteractor,
-                longPressGestureDetector,
+                statusBarLongPressGestureDetector,
                 windowRootView,
                 shadeLogger,
                 statusBarMoveFromCenterAnimationController,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt
index 1d08f2b..98eed84 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt
@@ -37,6 +37,9 @@
 
     /** Clients must observe this property, as device-based satellite is location-dependent */
     val isSatelliteAllowedForCurrentLocation: StateFlow<Boolean>
+
+    /** When enabled, a satellite icon will display when all other connections are OOS */
+    val isOpportunisticSatelliteIconEnabled: Boolean
 }
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcher.kt
index 58c30e0..de42b92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcher.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcher.kt
@@ -97,6 +97,9 @@
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl)
 
+    override val isOpportunisticSatelliteIconEnabled: Boolean
+        get() = activeRepo.value.isOpportunisticSatelliteIconEnabled
+
     override val isSatelliteProvisioned: StateFlow<Boolean> =
         activeRepo
             .flatMapLatest { it.isSatelliteProvisioned }
@@ -118,6 +121,6 @@
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                realImpl.isSatelliteAllowedForCurrentLocation.value
+                realImpl.isSatelliteAllowedForCurrentLocation.value,
             )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepository.kt
index d557bbf..755899f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepository.kt
@@ -16,15 +16,18 @@
 
 package com.android.systemui.statusbar.pipeline.satellite.data.demo
 
+import android.content.res.Resources
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
 import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.MutableStateFlow
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /** A satellite repository that represents the latest satellite values sent via demo mode. */
 @SysUISingleton
@@ -33,9 +36,13 @@
 constructor(
     private val dataSource: DemoDeviceBasedSatelliteDataSource,
     @Application private val scope: CoroutineScope,
+    @Main resources: Resources,
 ) : DeviceBasedSatelliteRepository {
     private var demoCommandJob: Job? = null
 
+    override val isOpportunisticSatelliteIconEnabled =
+        resources.getBoolean(R.bool.config_showOpportunisticSatelliteIcon)
+
     override val isSatelliteProvisioned = MutableStateFlow(true)
     override val connectionState = MutableStateFlow(SatelliteConnectionState.Unknown)
     override val signalStrength = MutableStateFlow(0)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index 7686338..a36ef56 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.satellite.data.prod
 
+import android.content.res.Resources
 import android.os.OutcomeReceiver
 import android.telephony.TelephonyCallback
 import android.telephony.TelephonyManager
@@ -27,14 +28,17 @@
 import android.telephony.satellite.SatelliteProvisionStateCallback
 import android.telephony.satellite.SatelliteSupportedStateCallback
 import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
+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.core.MessageInitializer
 import com.android.systemui.log.core.MessagePrinter
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteInputLog
 import com.android.systemui.statusbar.pipeline.dagger.VerboseDeviceBasedSatelliteInputLog
 import com.android.systemui.statusbar.pipeline.satellite.data.RealDeviceBasedSatelliteRepository
@@ -66,7 +70,6 @@
 import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
-import com.android.app.tracing.coroutines.launchTraced as launch
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 
@@ -146,10 +149,14 @@
     @DeviceBasedSatelliteInputLog private val logBuffer: LogBuffer,
     @VerboseDeviceBasedSatelliteInputLog private val verboseLogBuffer: LogBuffer,
     private val systemClock: SystemClock,
+    @Main resources: Resources,
 ) : RealDeviceBasedSatelliteRepository {
 
     private val satelliteManager: SatelliteManager?
 
+    override val isOpportunisticSatelliteIconEnabled: Boolean =
+        resources.getBoolean(R.bool.config_showOpportunisticSatelliteIcon)
+
     // Some calls into satellite manager will throw exceptions if it is not supported.
     // This is never expected to change after boot, but may need to be retried in some cases
     @get:VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
index f1a444f..08a98c3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
@@ -53,6 +53,9 @@
     @DeviceBasedSatelliteInputLog private val logBuffer: LogBuffer,
     @DeviceBasedSatelliteTableLog private val tableLog: TableLogBuffer,
 ) {
+    /** Whether or not we should show the satellite icon when all connections are OOS */
+    val isOpportunisticSatelliteIconEnabled = repo.isOpportunisticSatelliteIconEnabled
+
     /** Must be observed by any UI showing Satellite iconography */
     val isSatelliteAllowed =
         if (Flags.oemEnabledSatelliteFlag()) {
@@ -93,12 +96,7 @@
                 flowOf(0)
             }
             .distinctUntilChanged()
-            .logDiffsForTable(
-                tableLog,
-                columnPrefix = "",
-                columnName = COL_LEVEL,
-                initialValue = 0,
-            )
+            .logDiffsForTable(tableLog, columnPrefix = "", columnName = COL_LEVEL, initialValue = 0)
             .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
 
     val isSatelliteProvisioned = repo.isSatelliteProvisioned
@@ -132,10 +130,9 @@
     /** When all connections are considered OOS, satellite connectivity is potentially valid */
     val areAllConnectionsOutOfService =
         if (Flags.oemEnabledSatelliteFlag()) {
-                combine(
-                    allConnectionsOos,
-                    iconsInteractor.isDeviceInEmergencyCallsOnlyMode,
-                ) { connectionsOos, deviceEmergencyOnly ->
+                combine(allConnectionsOos, iconsInteractor.isDeviceInEmergencyCallsOnlyMode) {
+                    connectionsOos,
+                    deviceEmergencyOnly ->
                     logBuffer.log(
                         TAG,
                         LogLevel.INFO,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
index 37f2f19..13ac321 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
@@ -77,35 +77,38 @@
 
     // This adds a 10 seconds delay before showing the icon
     private val shouldShowIconForOosAfterHysteresis: StateFlow<Boolean> =
-        interactor.areAllConnectionsOutOfService
-            .flatMapLatest { shouldShow ->
-                if (shouldShow) {
-                    logBuffer.log(
-                        TAG,
-                        LogLevel.INFO,
-                        { long1 = DELAY_DURATION.inWholeSeconds },
-                        { "Waiting $long1 seconds before showing the satellite icon" }
+        if (interactor.isOpportunisticSatelliteIconEnabled) {
+                interactor.areAllConnectionsOutOfService
+                    .flatMapLatest { shouldShow ->
+                        if (shouldShow) {
+                            logBuffer.log(
+                                TAG,
+                                LogLevel.INFO,
+                                { long1 = DELAY_DURATION.inWholeSeconds },
+                                { "Waiting $long1 seconds before showing the satellite icon" },
+                            )
+                            delay(DELAY_DURATION)
+                            flowOf(true)
+                        } else {
+                            flowOf(false)
+                        }
+                    }
+                    .distinctUntilChanged()
+                    .logDiffsForTable(
+                        tableLog,
+                        columnPrefix = "vm",
+                        columnName = COL_VISIBLE_FOR_OOS,
+                        initialValue = false,
                     )
-                    delay(DELAY_DURATION)
-                    flowOf(true)
-                } else {
-                    flowOf(false)
-                }
+            } else {
+                flowOf(false)
             }
-            .distinctUntilChanged()
-            .logDiffsForTable(
-                tableLog,
-                columnPrefix = "vm",
-                columnName = COL_VISIBLE_FOR_OOS,
-                initialValue = false,
-            )
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
     private val canShowIcon =
-        combine(
-            interactor.isSatelliteAllowed,
-            interactor.isSatelliteProvisioned,
-        ) { allowed, provisioned ->
+        combine(interactor.isSatelliteAllowed, interactor.isSatelliteProvisioned) {
+            allowed,
+            provisioned ->
             allowed && provisioned
         }
 
@@ -141,11 +144,10 @@
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
     override val icon: StateFlow<Icon?> =
-        combine(
-                showIcon,
-                interactor.connectionState,
-                interactor.signalStrength,
-            ) { shouldShow, state, signalStrength ->
+        combine(showIcon, interactor.connectionState, interactor.signalStrength) {
+                shouldShow,
+                state,
+                signalStrength ->
                 if (shouldShow) {
                     SatelliteIconModel.fromConnectionState(state, signalStrength)
                 } else {
@@ -155,10 +157,7 @@
             .stateIn(scope, SharingStarted.WhileSubscribed(), null)
 
     override val carrierText: StateFlow<String?> =
-        combine(
-                showIcon,
-                interactor.connectionState,
-            ) { shouldShow, connectionState ->
+        combine(showIcon, interactor.connectionState) { shouldShow, connectionState ->
                 logBuffer.log(
                     TAG,
                     LogLevel.INFO,
@@ -166,7 +165,7 @@
                         bool1 = shouldShow
                         str1 = connectionState.name
                     },
-                    { "Updating carrier text. shouldShow=$bool1 connectionState=$str1" }
+                    { "Updating carrier text. shouldShow=$bool1 connectionState=$str1" },
                 )
                 if (shouldShow) {
                     when (connectionState) {
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 9c8ef04..1c3fece 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -242,10 +242,16 @@
         }
     };
 
-    private int getLatestWallpaperType(int userId) {
-        return mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_LOCK, userId)
-                > mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_SYSTEM, userId)
-                ? WallpaperManager.FLAG_LOCK : WallpaperManager.FLAG_SYSTEM;
+    private int getDefaultWallpaperColorsSource(int userId) {
+        if (com.android.systemui.shared.Flags.newCustomizationPickerUi()) {
+            // The wallpaper colors source is always the home wallpaper.
+            return WallpaperManager.FLAG_SYSTEM;
+        } else {
+            // The wallpaper colors source is based on the last set wallpaper.
+            return mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_LOCK, userId)
+                    > mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_SYSTEM, userId)
+                    ? WallpaperManager.FLAG_LOCK : WallpaperManager.FLAG_SYSTEM;
+        }
     }
 
     private boolean isSeedColorSet(JSONObject jsonObject, WallpaperColors newWallpaperColors) {
@@ -279,9 +285,9 @@
     private void handleWallpaperColors(WallpaperColors wallpaperColors, int flags, int userId) {
         final int currentUser = mUserTracker.getUserId();
         final boolean hadWallpaperColors = mCurrentColors.get(userId) != null;
-        int latestWallpaperType = getLatestWallpaperType(userId);
-        boolean eventForLatestWallpaper = (flags & latestWallpaperType) != 0;
-        if (eventForLatestWallpaper) {
+        int wallpaperColorsSource = getDefaultWallpaperColorsSource(userId);
+        boolean wallpaperColorsNeedUpdate = (flags & wallpaperColorsSource) != 0;
+        if (wallpaperColorsNeedUpdate) {
             mCurrentColors.put(userId, wallpaperColors);
             if (DEBUG) Log.d(TAG, "got new colors: " + wallpaperColors + " where: " + flags);
         }
@@ -328,7 +334,7 @@
             boolean userChoseLockScreenColor = COLOR_SOURCE_LOCK.equals(wallpaperPickerColorSource);
             boolean preserveLockScreenColor = isDestinationHomeOnly && userChoseLockScreenColor;
 
-            if (!userChosePresetColor && !preserveLockScreenColor && eventForLatestWallpaper
+            if (!userChosePresetColor && !preserveLockScreenColor && wallpaperColorsNeedUpdate
                     && !isSeedColorSet(jsonObject, wallpaperColors)) {
                 mSkipSettingChange = true;
                 if (jsonObject.has(OVERLAY_CATEGORY_ACCENT_COLOR) || jsonObject.has(
@@ -494,7 +500,7 @@
         // Upon boot, make sure we have the most up to date colors
         Runnable updateColors = () -> {
             WallpaperColors systemColor = mWallpaperManager.getWallpaperColors(
-                    getLatestWallpaperType(mUserTracker.getUserId()));
+                    getDefaultWallpaperColorsSource(mUserTracker.getUserId()));
             Runnable applyColors = () -> {
                 if (DEBUG) Log.d(TAG, "Boot colors: " + systemColor);
                 mCurrentColors.put(mUserTracker.getUserId(), systemColor);
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
index 3159124..63a5b3f 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
@@ -20,6 +20,7 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.coroutineScope
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.lifecycle.repeatWhenAttached
@@ -35,7 +36,7 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.stateIn
-import com.android.app.tracing.coroutines.launchTraced as launch
+import kotlinx.coroutines.plus
 
 /** A class allowing Java classes to collect on Kotlin flows. */
 @SysUISingleton
@@ -102,6 +103,22 @@
     }
 }
 
+/**
+ * Collect information for the given [flow], calling [consumer] for each emitted event on the
+ * specified [collectContext].
+ *
+ * Collection will continue until the given [scope] is cancelled.
+ */
+@JvmOverloads
+fun <T> collectFlow(
+    scope: CoroutineScope,
+    collectContext: CoroutineContext = scope.coroutineContext,
+    flow: Flow<T>,
+    consumer: Consumer<T>,
+): Job {
+    return scope.plus(collectContext).launch { flow.collect { consumer.accept(it) } }
+}
+
 fun <A, B, R> combineFlows(flow1: Flow<A>, flow2: Flow<B>, bifunction: (A, B) -> R): Flow<R> {
     return combine(flow1, flow2, bifunction)
 }
diff --git a/packages/SystemUI/tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java b/packages/SystemUI/tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java
index a95735e..82cfab6 100644
--- a/packages/SystemUI/tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java
+++ b/packages/SystemUI/tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java
@@ -56,7 +56,6 @@
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
 public class AAAPlusPlusVerifySysuiRequiredTestPropertiesTest extends SysuiTestCase {
-
     private static final String TAG = "AAA++VerifyTest";
 
     private static final Class[] BASE_CLS_TO_INCLUDE = {
@@ -149,6 +148,9 @@
      */
     private boolean isTestClass(Class<?> loadedClass) {
         try {
+            if (loadedClass.getAnnotation(SkipSysuiVerification.class) != null) {
+                return false;
+            }
             if (Modifier.isAbstract(loadedClass.getModifiers())) {
                 logDebug(String.format("Skipping abstract class %s: not a test",
                         loadedClass.getName()));
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 f472fd1..11b5603 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
@@ -155,6 +155,7 @@
 import com.android.systemui.shade.ShadeControllerImpl;
 import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.shade.ShadeLogger;
+import com.android.systemui.shade.StatusBarLongPressGestureDetector;
 import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyboardShortcutListSearch;
@@ -174,7 +175,6 @@
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays;
 import com.android.systemui.statusbar.core.StatusBarInitializerImpl;
-import com.android.systemui.statusbar.core.StatusBarOrchestrator;
 import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
@@ -371,7 +371,7 @@
     @Mock private EmergencyGestureIntentFactory mEmergencyGestureIntentFactory;
     @Mock private NotificationSettingsInteractor mNotificationSettingsInteractor;
     @Mock private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager;
-    @Mock private StatusBarOrchestrator mStatusBarOrchestrator;
+    @Mock private StatusBarLongPressGestureDetector mStatusBarLongPressGestureDetector;
     private ShadeController mShadeController;
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
     private final FakeGlobalSettings mFakeGlobalSettings = new FakeGlobalSettings();
@@ -602,6 +602,7 @@
                 mShadeController,
                 mWindowRootViewVisibilityInteractor,
                 mStatusBarKeyguardViewManager,
+                () -> mStatusBarLongPressGestureDetector,
                 mViewMediatorCallback,
                 mInitController,
                 new Handler(TestableLooper.get(this).getLooper()),
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 638f195..69efa87 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
@@ -40,14 +40,13 @@
 import com.android.systemui.plugins.fakeDarkIconDispatcher
 import com.android.systemui.res.R
 import com.android.systemui.scene.ui.view.WindowRootView
-import com.android.systemui.shade.LongPressGestureDetector
 import com.android.systemui.shade.ShadeControllerImpl
 import com.android.systemui.shade.ShadeLogger
 import com.android.systemui.shade.ShadeViewController
+import com.android.systemui.shade.StatusBarLongPressGestureDetector
 import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.data.repository.fakeStatusBarContentInsetsProviderStore
-import com.android.systemui.statusbar.data.repository.statusBarContentInsetsProviderStore
 import com.android.systemui.statusbar.policy.Clock
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
@@ -98,7 +97,7 @@
     @Mock private lateinit var windowRootView: Provider<WindowRootView>
     @Mock private lateinit var shadeLogger: ShadeLogger
     @Mock private lateinit var viewUtil: ViewUtil
-    @Mock private lateinit var longPressGestureDetector: LongPressGestureDetector
+    @Mock private lateinit var mStatusBarLongPressGestureDetector: StatusBarLongPressGestureDetector
     private lateinit var statusBarWindowStateController: StatusBarWindowStateController
 
     private lateinit var view: PhoneStatusBarView
@@ -395,7 +394,7 @@
                 shadeControllerImpl,
                 shadeViewController,
                 panelExpansionInteractor,
-                { longPressGestureDetector },
+                { mStatusBarLongPressGestureDetector },
                 windowRootView,
                 shadeLogger,
                 viewUtil,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
index 4d293b9..6326e73 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -102,6 +102,7 @@
                     logBuffer = FakeLogBuffer.Factory.create(),
                     verboseLogBuffer = FakeLogBuffer.Factory.create(),
                     systemClock,
+                    context.resources,
                 )
 
             val connectionState by collectLastValue(underTest.connectionState)
@@ -267,11 +268,7 @@
     fun satelliteProvisioned_notSupported_defaultFalse() =
         testScope.runTest {
             // GIVEN satellite is not supported
-            setUpRepo(
-                uptime = MIN_UPTIME,
-                satMan = satelliteManager,
-                satelliteSupported = false,
-            )
+            setUpRepo(uptime = MIN_UPTIME, satMan = satelliteManager, satelliteSupported = false)
 
             assertThat(underTest.isSatelliteProvisioned.value).isFalse()
         }
@@ -280,11 +277,7 @@
     fun satelliteProvisioned_supported_defaultFalse() =
         testScope.runTest {
             // GIVEN satellite is supported
-            setUpRepo(
-                uptime = MIN_UPTIME,
-                satMan = satelliteManager,
-                satelliteSupported = true,
-            )
+            setUpRepo(uptime = MIN_UPTIME, satMan = satelliteManager, satelliteSupported = true)
 
             // THEN default provisioned state is false
             assertThat(underTest.isSatelliteProvisioned.value).isFalse()
@@ -323,6 +316,7 @@
                     logBuffer = FakeLogBuffer.Factory.create(),
                     verboseLogBuffer = FakeLogBuffer.Factory.create(),
                     systemClock,
+                    context.resources,
                 )
 
             // WHEN we try to check for provisioned status
@@ -361,6 +355,7 @@
                     logBuffer = FakeLogBuffer.Factory.create(),
                     verboseLogBuffer = FakeLogBuffer.Factory.create(),
                     systemClock,
+                    context.resources,
                 )
 
             // WHEN we try to check for provisioned status
@@ -445,11 +440,7 @@
     fun satelliteProvisioned_supported_tracksCallback_reRegistersOnCrash() =
         testScope.runTest {
             // GIVEN satellite is supported
-            setUpRepo(
-                uptime = MIN_UPTIME,
-                satMan = satelliteManager,
-                satelliteSupported = true,
-            )
+            setUpRepo(uptime = MIN_UPTIME, satMan = satelliteManager, satelliteSupported = true)
 
             val provisioned by collectLastValue(underTest.isSatelliteProvisioned)
 
@@ -487,11 +478,7 @@
     fun satelliteNotSupported_listenersAreNotRegistered() =
         testScope.runTest {
             // GIVEN satellite is not supported
-            setUpRepo(
-                uptime = MIN_UPTIME,
-                satMan = satelliteManager,
-                satelliteSupported = false,
-            )
+            setUpRepo(uptime = MIN_UPTIME, satMan = satelliteManager, satelliteSupported = false)
 
             // WHEN data is requested from the repo
             val connectionState by collectLastValue(underTest.connectionState)
@@ -517,11 +504,7 @@
     fun satelliteNotSupported_registersCallbackForStateChanges() =
         testScope.runTest {
             // GIVEN satellite is not supported
-            setUpRepo(
-                uptime = MIN_UPTIME,
-                satMan = satelliteManager,
-                satelliteSupported = false,
-            )
+            setUpRepo(uptime = MIN_UPTIME, satMan = satelliteManager, satelliteSupported = false)
 
             runCurrent()
             // THEN the repo registers for state changes of satellite support
@@ -577,11 +560,7 @@
     fun satelliteNotSupported_supportShowsUp_registersListeners() =
         testScope.runTest {
             // GIVEN satellite is not supported
-            setUpRepo(
-                uptime = MIN_UPTIME,
-                satMan = satelliteManager,
-                satelliteSupported = false,
-            )
+            setUpRepo(uptime = MIN_UPTIME, satMan = satelliteManager, satelliteSupported = false)
             runCurrent()
 
             val callback =
@@ -610,11 +589,7 @@
     fun repoDoesNotCheckForSupportUntilMinUptime() =
         testScope.runTest {
             // GIVEN we init 100ms after sysui starts up
-            setUpRepo(
-                uptime = 100,
-                satMan = satelliteManager,
-                satelliteSupported = true,
-            )
+            setUpRepo(uptime = 100, satMan = satelliteManager, satelliteSupported = true)
 
             // WHEN data is requested
             val connectionState by collectLastValue(underTest.connectionState)
@@ -726,6 +701,7 @@
                 logBuffer = FakeLogBuffer.Factory.create(),
                 verboseLogBuffer = FakeLogBuffer.Factory.create(),
                 systemClock,
+                context.resources,
             )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/OnTeardownRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/OnTeardownRule.kt
new file mode 100644
index 0000000..778614b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/OnTeardownRule.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 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
+
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import org.junit.runners.model.MultipleFailureException
+
+/**
+ * Rule that allows teardown steps to be added right next to the places where it becomes clear they
+ * are needed. This can avoid the need for complicated or conditional logic in a single teardown
+ * method. Examples:
+ * ```
+ * @get:Rule teardownRule = OnTeardownRule()
+ *
+ * // setup and teardown right next to each other
+ * @Before
+ * fun setUp() {
+ *   val oldTimeout = getGlobalTimeout()
+ *   teardownRule.onTeardown { setGlobalTimeout(oldTimeout) }
+ *   overrideGlobalTimeout(5000)
+ * }
+ *
+ * // add teardown logic for fixtures that aren't used in every test
+ * fun addCustomer() {
+ *   val id = globalDatabase.addCustomer(TEST_NAME, TEST_ADDRESS, ...)
+ *   teardownRule.onTeardown { globalDatabase.deleteCustomer(id) }
+ * }
+ * ```
+ */
+class OnTeardownRule : TestWatcher() {
+    private var canAdd = true
+    private val teardowns = mutableListOf<() -> Unit>()
+
+    fun onTeardown(teardownRunnable: () -> Unit) {
+        if (!canAdd) {
+            throw IllegalStateException("Cannot add new teardown routines after test complete.")
+        }
+        teardowns.add(teardownRunnable)
+    }
+
+    fun onTeardown(teardownRunnable: Runnable) {
+        if (!canAdd) {
+            throw IllegalStateException("Cannot add new teardown routines after test complete.")
+        }
+        teardowns.add { teardownRunnable.run() }
+    }
+
+    override fun finished(description: Description?) {
+        canAdd = false
+        val errors = mutableListOf<Throwable>()
+        teardowns.reversed().forEach {
+            try {
+                it()
+            } catch (e: Throwable) {
+                errors.add(e)
+            }
+        }
+        MultipleFailureException.assertEmpty(errors)
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
index 27a2cab..153a8be 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
@@ -56,6 +56,8 @@
 
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Future;
@@ -69,6 +71,17 @@
 // background on Ravenwood is available at go/ravenwood-docs
 @DisabledOnRavenwood
 public abstract class SysuiTestCase {
+    /**
+     * Especially when self-testing test utilities, we may have classes that look like test
+     * classes, but we don't expect to ever actually run as a top-level test.
+     * For example, {@link com.android.systemui.TryToDoABadThing}.
+     * Verifying properties on these as a part of structural tests like
+     * AAAPlusPlusVerifySysuiRequiredTestPropertiesTest is a waste of our time, and makes things
+     * look more confusing, so this lets us skip when appropriate.
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface SkipSysuiVerification {
+    }
 
     private static final String TAG = "SysuiTestCase";
 
@@ -172,6 +185,15 @@
     public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
             new DexmakerShareClassLoaderRule();
 
+    @Rule public final OnTeardownRule mTearDownRule = new OnTeardownRule();
+
+    /**
+     * Schedule a cleanup routine to happen when the test state is torn down.
+     */
+    protected void onTeardown(Runnable tearDownRunnable) {
+        mTearDownRule.onTeardown(tearDownRunnable);
+    }
+
     // set the highest order so it's the innermost rule
     @Rule(order = Integer.MAX_VALUE)
     public TestWithLooperRule mlooperRule = new TestWithLooperRule();
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/LightBarControllerStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/LightBarControllerStoreKosmos.kt
new file mode 100644
index 0000000..5f33732
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/LightBarControllerStoreKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 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.data.repository
+
+import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.display.data.repository.displayScopeRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import org.mockito.kotlin.mock
+
+val Kosmos.lightBarControllerStoreImpl by
+    Kosmos.Fixture {
+        LightBarControllerStoreImpl(
+            backgroundApplicationScope = applicationCoroutineScope,
+            displayRepository = displayRepository,
+            factory = { _, _, _ -> mock() },
+            displayScopeRepository = displayScopeRepository,
+            statusBarModeRepositoryStore = statusBarModeRepository,
+        )
+    }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 985155d..0cf55bb 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -493,7 +493,7 @@
     private static final int MSG_INIT_ADI_DEVICE_STATES = 103;
 
     private static final int MSG_INIT_INPUT_GAINS = 104;
-    private static final int MSG_SET_INPUT_GAIN_INDEX = 105;
+    private static final int MSG_APPLY_INPUT_GAIN_INDEX = 105;
     private static final int MSG_PERSIST_INPUT_GAIN_INDEX = 106;
 
     // end of messages handled under wakelock
@@ -1626,7 +1626,6 @@
                 new InputDeviceVolumeHelper(
                         mSettings,
                         mContentResolver,
-                        mSettingsLock,
                         System.INPUT_GAIN_INDEX_SETTINGS);
     }
 
@@ -5804,7 +5803,7 @@
             // to persist).
             sendMsg(
                     mAudioHandler,
-                    MSG_SET_INPUT_GAIN_INDEX,
+                    MSG_APPLY_INPUT_GAIN_INDEX,
                     SENDMSG_QUEUE,
                     /*arg1*/ index,
                     /*arg2*/ 0,
@@ -5813,22 +5812,22 @@
         }
     }
 
-    private void setInputGainIndexInt(@NonNull AudioDeviceAttributes ada, int index) {
+    private void onApplyInputGainIndex(@NonNull AudioDeviceAttributes ada, int index) {
         // TODO(b/364923030): call AudioSystem to apply input gain in native layer.
 
         // Post a persist input gain msg.
         sendMsg(
                 mAudioHandler,
                 MSG_PERSIST_INPUT_GAIN_INDEX,
-                SENDMSG_QUEUE,
-                /*arg1*/ index,
+                SENDMSG_REPLACE,
+                /*arg1*/ 0,
                 /*arg2*/ 0,
                 /*obj*/ ada,
                 PERSIST_DELAY);
     }
 
-    private void persistInputGainIndex(@NonNull AudioDeviceAttributes ada, int index) {
-        mInputDeviceVolumeHelper.persistInputGainIndex(ada, index);
+    private void onPersistInputGainIndex(@NonNull AudioDeviceAttributes ada) {
+        mInputDeviceVolumeHelper.persistInputGainIndex(ada);
     }
 
     /**
@@ -10213,12 +10212,12 @@
                     vgs.persistVolumeGroup(msg.arg1);
                     break;
 
-                case MSG_SET_INPUT_GAIN_INDEX:
-                    setInputGainIndexInt((AudioDeviceAttributes) msg.obj, msg.arg1);
+                case MSG_APPLY_INPUT_GAIN_INDEX:
+                    onApplyInputGainIndex((AudioDeviceAttributes) msg.obj, msg.arg1);
                     break;
 
                 case MSG_PERSIST_INPUT_GAIN_INDEX:
-                    persistInputGainIndex((AudioDeviceAttributes) msg.obj, msg.arg1);
+                    onPersistInputGainIndex((AudioDeviceAttributes) msg.obj);
                     break;
 
                 case MSG_PERSIST_RINGER_MODE:
diff --git a/services/core/java/com/android/server/audio/InputDeviceVolumeHelper.java b/services/core/java/com/android/server/audio/InputDeviceVolumeHelper.java
index d83dca6..d094629 100644
--- a/services/core/java/com/android/server/audio/InputDeviceVolumeHelper.java
+++ b/services/core/java/com/android/server/audio/InputDeviceVolumeHelper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -43,7 +43,6 @@
 
     private final SettingsAdapter mSettings;
     private final ContentResolver mContentResolver;
-    private final Object mSettingsLock;
     private final String mInputGainIndexSettingsName;
 
     // A map between device internal type (e.g. AudioSystem.DEVICE_IN_BUILTIN_MIC) to its input gain
@@ -54,11 +53,9 @@
     InputDeviceVolumeHelper(
             SettingsAdapter settings,
             ContentResolver contentResolver,
-            Object settingsLock,
             String settingsName) {
         mSettings = settings;
         mContentResolver = contentResolver;
-        mSettingsLock = settingsLock;
         mInputGainIndexSettingsName = settingsName;
 
         IntArray internalDeviceTypes = new IntArray();
@@ -82,34 +79,27 @@
         readSettings();
     }
 
-    public void readSettings() {
+    private void readSettings() {
         synchronized (InputDeviceVolumeHelper.class) {
             for (int inputDeviceType : mSupportedDeviceTypes) {
                 // Retrieve current input gain for device. If no input gain stored for current
                 // device, use default input gain.
-                int index;
-                if (!hasValidSettingsName()) {
-                    index = INDEX_DEFAULT;
-                } else {
-                    String name = getSettingNameForDevice(inputDeviceType);
-                    index =
-                            mSettings.getSystemIntForUser(
-                                    mContentResolver, name, INDEX_DEFAULT, UserHandle.USER_CURRENT);
-                }
+                String name = getSettingNameForDevice(inputDeviceType);
+                int index = name == null
+                        ? INDEX_DEFAULT
+                        : mSettings.getSystemIntForUser(
+                                mContentResolver, name, INDEX_DEFAULT, UserHandle.USER_CURRENT);
 
                 mInputGainIndexMap.put(inputDeviceType, getValidIndex(index));
             }
         }
     }
 
-    public boolean hasValidSettingsName() {
-        return mInputGainIndexSettingsName != null && !mInputGainIndexSettingsName.isEmpty();
-    }
-
-    public @Nullable String getSettingNameForDevice(int inputDeviceType) {
-        if (!hasValidSettingsName()) {
+    private @Nullable String getSettingNameForDevice(int inputDeviceType) {
+        if (mInputGainIndexSettingsName == null || mInputGainIndexSettingsName.isEmpty()) {
             return null;
         }
+
         final String suffix = AudioSystem.getInputDeviceName(inputDeviceType);
         if (suffix.isEmpty()) {
             return mInputGainIndexSettingsName;
@@ -158,29 +148,27 @@
         ensureValidInputDeviceType(inputDeviceType);
 
         int oldIndex;
-        synchronized (mSettingsLock) {
-            synchronized (InputDeviceVolumeHelper.class) {
-                oldIndex = getInputGainIndex(ada);
-                index = getValidIndex(index);
+        synchronized (InputDeviceVolumeHelper.class) {
+            oldIndex = getInputGainIndex(ada);
+            index = getValidIndex(index);
 
-                if (oldIndex == index) {
-                    return false;
-                }
-
-                mInputGainIndexMap.put(inputDeviceType, index);
-                return true;
+            if (oldIndex == index) {
+                return false;
             }
+
+            mInputGainIndexMap.put(inputDeviceType, index);
+            return true;
         }
     }
 
-    public void persistInputGainIndex(@NonNull AudioDeviceAttributes ada, int index) {
+    public void persistInputGainIndex(@NonNull AudioDeviceAttributes ada) {
         int inputDeviceType = AudioDeviceInfo.convertDeviceTypeToInternalInputDevice(ada.getType());
-        ensureValidInputDeviceType(inputDeviceType);
-
-        if (hasValidSettingsName()) {
+        String name = getSettingNameForDevice(inputDeviceType);
+        if (name != null) {
+            int index = getInputGainIndex(ada);
             mSettings.putSystemIntForUser(
                     mContentResolver,
-                    getSettingNameForDevice(inputDeviceType),
+                    name,
                     index,
                     UserHandle.USER_CURRENT);
         }
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 8627a47..ee07d2e 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1735,9 +1735,9 @@
         }
 
         // Show IME over the keyguard if the target allows it.
-        final boolean showImeOverKeyguard = imeTarget != null && imeTarget.isVisible()
-                && win.mIsImWindow && (imeTarget.canShowWhenLocked()
-                        || !imeTarget.canBeHiddenByKeyguard());
+        final boolean showImeOverKeyguard =
+                imeTarget != null && imeTarget.isOnScreen() && win.mIsImWindow && (
+                        imeTarget.canShowWhenLocked() || !imeTarget.canBeHiddenByKeyguard());
         if (showImeOverKeyguard) {
             return false;
         }
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index e9c6e93..71adb80 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -100,13 +100,13 @@
             // isLeashReadyForDispatching (used to dispatch the leash of the control) is
             // depending on mGivenInsetsReady. Therefore, triggering notifyControlChanged here
             // again, so that the control with leash can be eventually dispatched
-            if (!mGivenInsetsReady && mServerVisible && !givenInsetsPending) {
+            if (!mGivenInsetsReady && isServerVisible() && !givenInsetsPending) {
                 mGivenInsetsReady = true;
                 ImeTracker.forLogging().onProgress(mStatsToken,
                         ImeTracker.PHASE_WM_POST_LAYOUT_NOTIFY_CONTROLS_CHANGED);
                 mStateController.notifyControlChanged(mControlTarget, this);
                 setImeShowing(true);
-            } else if (wasServerVisible && mServerVisible && mGivenInsetsReady
+            } else if (wasServerVisible && isServerVisible() && mGivenInsetsReady
                     && givenInsetsPending) {
                 // If the server visibility didn't change (still visible), and mGivenInsetsReady
                 // is set, we won't call into notifyControlChanged. Therefore, we can reset the
@@ -114,7 +114,7 @@
                 ImeTracker.forLogging().onCancelled(mStatsToken,
                         ImeTracker.PHASE_WM_POST_LAYOUT_NOTIFY_CONTROLS_CHANGED);
                 mStatsToken = null;
-            } else if (wasServerVisible && !mServerVisible) {
+            } else if (wasServerVisible && !isServerVisible()) {
                 setImeShowing(false);
             }
         }
@@ -134,11 +134,15 @@
     @Override
     protected boolean isLeashReadyForDispatching() {
         if (android.view.inputmethod.Flags.refactorInsetsController()) {
+            // We should only dispatch the leash, if the following conditions are fulfilled:
+            // 1. parent isLeashReadyForDispatching, 2. mGivenInsetsReady (means there are no
+            // givenInsetsPending), 3. the IME surface is drawn, 4. either the IME is
+            // serverVisible (the unfrozen state)
             final WindowState ws =
                     mWindowContainer != null ? mWindowContainer.asWindowState() : null;
             final boolean isDrawn = ws != null && ws.isDrawn();
             return super.isLeashReadyForDispatching()
-                    && mServerVisible && isDrawn && mGivenInsetsReady;
+                    && isServerVisible() && isDrawn && mGivenInsetsReady;
         } else {
             return super.isLeashReadyForDispatching();
         }
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index 1d4d6eb..7276007 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -730,6 +730,10 @@
         return mFakeControlTarget;
     }
 
+    boolean isServerVisible() {
+        return mServerVisible;
+    }
+
     boolean isClientVisible() {
         return mClientVisible;
     }