summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Nicolò Mazzucato <nicomazz@google.com> 2025-03-04 02:32:57 -0800
committer Android (Google) Code Review <android-gerrit@google.com> 2025-03-04 02:32:57 -0800
commit1bc44b45acbc50ec25cd6435a5970ce3e718cb45 (patch)
treeca7ee1668d05575dad1d45999579d508344fbff5
parent9ae2affb56991ae37efa9e94eb118a3947ba9631 (diff)
parente5db5c65a4b40ef3f23666f6100dff93744af440 (diff)
Merge changes Ie34a4ac6,I78323f20,If95ab474,I3927a1fd into main
* changes: Fix status bar icon size when shade goes around Fix statusbar visibility wrt shade expansion in multiple displays Propagate new shade displayId only after display change succeeded Make SysUiState for external display an override of the default one
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateOverrideTest.kt105
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateTest.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt19
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt16
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt136
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/model/SysUIStateOverride.kt83
-rw-r--r--packages/SystemUI/src/com/android/systemui/model/SysUiState.kt26
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarConfigurationStateStore.kt120
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt73
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/model/SysUiStateKosmos.kt12
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt54
22 files changed, 652 insertions, 92 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateOverrideTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateOverrideTest.kt
new file mode 100644
index 000000000000..24bd8adaa45a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateOverrideTest.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2025 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.model
+
+import android.view.Display
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.never
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SysUIStateOverrideTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ private val defaultState = kosmos.sysUiState
+ private val callbackOnOverride = mock<SysUiState.SysUiStateCallback>()
+ private val dumpManager = kosmos.dumpManager
+
+ private val underTest = kosmos.sysUiStateOverrideFactory.invoke(DISPLAY_1)
+
+ @Before
+ fun setup() {
+ underTest.start()
+ underTest.addCallback(callbackOnOverride)
+ reset(callbackOnOverride)
+ }
+
+ @Test
+ fun setFlag_setOnDefaultState_propagatedToOverride() {
+ defaultState.setFlag(FLAG_1, true).commitUpdate()
+
+ verify(callbackOnOverride).onSystemUiStateChanged(FLAG_1, Display.DEFAULT_DISPLAY)
+ verify(callbackOnOverride).onSystemUiStateChanged(FLAG_1, DISPLAY_1)
+ }
+
+ @Test
+ fun setFlag_onOverride_overridesDefaultOnes() {
+ defaultState.setFlag(FLAG_1, false).setFlag(FLAG_2, true).commitUpdate()
+ underTest.setFlag(FLAG_1, true).setFlag(FLAG_2, false).commitUpdate()
+
+ assertThat(underTest.isFlagEnabled(FLAG_1)).isTrue()
+ assertThat(underTest.isFlagEnabled(FLAG_2)).isFalse()
+
+ assertThat(defaultState.isFlagEnabled(FLAG_1)).isFalse()
+ assertThat(defaultState.isFlagEnabled(FLAG_2)).isTrue()
+ }
+
+ @Test
+ fun destroy_callbacksForDefaultStateNotReceivedAnymore() {
+ defaultState.setFlag(FLAG_1, true).commitUpdate()
+
+ verify(callbackOnOverride).onSystemUiStateChanged(FLAG_1, Display.DEFAULT_DISPLAY)
+
+ reset(callbackOnOverride)
+ underTest.destroy()
+ defaultState.setFlag(FLAG_1, false).commitUpdate()
+
+ verify(callbackOnOverride, never()).onSystemUiStateChanged(FLAG_1, Display.DEFAULT_DISPLAY)
+ }
+
+ @Test
+ fun init_registersWithDumpManager() {
+ verify(dumpManager).registerNormalDumpable(any(), eq(underTest))
+ }
+
+ @Test
+ fun destroy_unregistersWithDumpManager() {
+ underTest.destroy()
+
+ verify(dumpManager).unregisterDumpable(ArgumentMatchers.anyString())
+ }
+
+ private companion object {
+ const val DISPLAY_1 = 1
+ const val FLAG_1 = 1L
+ const val FLAG_2 = 2L
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateTest.java
index f6de6295212b..5bb7f36e4187 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateTest.java
@@ -140,6 +140,8 @@ public class SysUiStateTest extends SysuiTestCase {
@Test
public void init_registersWithDumpManager() {
+ mFlagsContainer.start();
+
verify(mDumpManager).registerNormalDumpable(any(), eq(mFlagsContainer));
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt
index 9498daaf0b07..c635c7ff69a4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt
@@ -67,7 +67,7 @@ class ShadeDisplaysRepositoryTest : SysuiTestCase() {
fun policy_changing_propagatedFromTheLatestPolicy() =
testScope.runTest {
val underTest = createUnderTest()
- val displayIds by collectValues(underTest.displayId)
+ val displayIds by collectValues(underTest.pendingDisplayId)
assertThat(displayIds).containsExactly(0)
@@ -98,7 +98,7 @@ class ShadeDisplaysRepositoryTest : SysuiTestCase() {
DEVELOPMENT_SHADE_DISPLAY_AWARENESS,
FakeShadeDisplayPolicy.name,
)
- val displayId by collectLastValue(underTest.displayId)
+ val displayId by collectLastValue(underTest.pendingDisplayId)
displayRepository.addDisplay(displayId = 1)
@@ -161,7 +161,7 @@ class ShadeDisplaysRepositoryTest : SysuiTestCase() {
FakeShadeDisplayPolicy.name,
)
- val displayId by collectLastValue(underTest.displayId)
+ val displayId by collectLastValue(underTest.pendingDisplayId)
displayRepository.addDisplays(display(id = 2, type = TYPE_EXTERNAL))
FakeShadeDisplayPolicy.setDisplayId(2)
@@ -176,4 +176,17 @@ class ShadeDisplaysRepositoryTest : SysuiTestCase() {
assertThat(displayId).isEqualTo(2)
}
+
+ @Test
+ fun onDisplayChangedSucceeded_displayIdChanges() =
+ testScope.runTest {
+ val underTest = createUnderTest()
+ val displayId by collectLastValue(underTest.displayId)
+
+ assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
+
+ underTest.onDisplayChangedSucceeded(1)
+
+ assertThat(displayId).isEqualTo(1)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt
index fd6bc98b006c..f51d41bbc80b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt
@@ -69,7 +69,7 @@ class ShadePrimaryDisplayCommandTest : SysuiTestCase() {
@Test
fun commandShadeDisplayOverride_resetsDisplayId() =
testScope.runTest {
- val displayId by collectLastValue(shadeDisplaysRepository.displayId)
+ val displayId by collectLastValue(shadeDisplaysRepository.pendingDisplayId)
assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
val newDisplayId = 2
@@ -87,7 +87,7 @@ class ShadePrimaryDisplayCommandTest : SysuiTestCase() {
@Test
fun commandShadeDisplayOverride_anyExternalDisplay_notOnDefaultAnymore() =
testScope.runTest {
- val displayId by collectLastValue(shadeDisplaysRepository.displayId)
+ val displayId by collectLastValue(shadeDisplaysRepository.pendingDisplayId)
assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
val newDisplayId = 2
displayRepository.addDisplay(displayId = newDisplayId)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
index 0ad60b617194..03f546b09faf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
@@ -22,7 +22,6 @@ import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.ui.data.repository.configurationRepository
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
@@ -82,7 +81,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {
fun start_shadeInCorrectPosition_notAddedOrRemoved() =
testScope.runTest {
whenever(display.displayId).thenReturn(0)
- positionRepository.setDisplayId(0)
+ positionRepository.setPendingDisplayId(0)
underTest.start()
@@ -93,18 +92,19 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {
fun start_shadeInWrongPosition_changes() =
testScope.runTest {
whenever(display.displayId).thenReturn(0)
- positionRepository.setDisplayId(1)
+ positionRepository.setPendingDisplayId(1)
underTest.start()
verify(shadeContext).reparentToDisplay(eq(1))
+ assertThat(positionRepository.displayId.value).isEqualTo(1)
}
@Test
fun start_shadeInWrongPosition_logsStartToLatencyTracker() =
testScope.runTest {
whenever(display.displayId).thenReturn(0)
- positionRepository.setDisplayId(1)
+ positionRepository.setPendingDisplayId(1)
underTest.start()
@@ -115,7 +115,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {
fun start_shadeInWrongPosition_someNotificationsVisible_hiddenThenShown() =
testScope.runTest {
whenever(display.displayId).thenReturn(0)
- positionRepository.setDisplayId(1)
+ positionRepository.setPendingDisplayId(1)
activeNotificationRepository.setActiveNotifs(1)
underTest.start()
@@ -129,7 +129,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {
fun start_shadeInWrongPosition_someNotificationsVisible_waitsForInflationsBeforeShowingNssl() =
testScope.runTest {
whenever(display.displayId).thenReturn(0)
- positionRepository.setDisplayId(1)
+ positionRepository.setPendingDisplayId(1)
activeNotificationRepository.setActiveNotifs(1)
val endRebinding = notificationRebindingTracker.trackRebinding("test")
@@ -155,7 +155,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {
fun start_shadeInWrongPosition_noNotifications_nsslNotHidden() =
testScope.runTest {
whenever(display.displayId).thenReturn(0)
- positionRepository.setDisplayId(1)
+ positionRepository.setPendingDisplayId(1)
activeNotificationRepository.setActiveNotifs(0)
underTest.start()
@@ -170,7 +170,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {
fun start_shadeInWrongPosition_waitsUntilMovedToDisplayReceived() =
testScope.runTest {
whenever(display.displayId).thenReturn(0)
- positionRepository.setDisplayId(1)
+ positionRepository.setPendingDisplayId(1)
activeNotificationRepository.setActiveNotifs(1)
underTest.start()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
index a083e59fe263..91b3896332f5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
@@ -66,7 +66,7 @@ class FakeHomeStatusBarViewModel(
override val mediaProjectionStopDialogDueToCallEndedState =
MutableStateFlow(MediaProjectionStopDialogModel.Hidden)
- override val isHomeStatusBarAllowedByScene = MutableStateFlow(false)
+ override val isHomeStatusBarAllowed = MutableStateFlow(false)
override val canShowOngoingActivityChips: Flow<Boolean> = MutableStateFlow(false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
index 27aa4bab3deb..ee5979c6ed9f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
@@ -30,6 +30,7 @@ import android.view.Display.DEFAULT_DISPLAY
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.display.data.repository.fake
@@ -57,6 +58,7 @@ import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.screenrecord.data.model.ScreenRecordModel
import com.android.systemui.screenrecord.data.repository.screenRecordRepository
+import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
@@ -97,7 +99,6 @@ import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.runCurrent
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -502,9 +503,10 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
}
@Test
- fun isHomeStatusBarAllowedByScene_sceneLockscreen_notOccluded_false() =
+ @EnableSceneContainer
+ fun isHomeStatusBarAllowed_sceneLockscreen_notOccluded_false() =
kosmos.runTest {
- val latest by collectLastValue(underTest.isHomeStatusBarAllowedByScene)
+ val latest by collectLastValue(underTest.isHomeStatusBarAllowed)
kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen)
kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(false, taskInfo = null)
@@ -513,9 +515,10 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
}
@Test
- fun isHomeStatusBarAllowedByScene_sceneLockscreen_occluded_true() =
+ @EnableSceneContainer
+ fun isHomeStatusBarAllowed_sceneLockscreen_occluded_true() =
kosmos.runTest {
- val latest by collectLastValue(underTest.isHomeStatusBarAllowedByScene)
+ val latest by collectLastValue(underTest.isHomeStatusBarAllowed)
kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen)
kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, taskInfo = null)
@@ -524,9 +527,10 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
}
@Test
- fun isHomeStatusBarAllowedByScene_overlayBouncer_false() =
+ @EnableSceneContainer
+ fun isHomeStatusBarAllowed_overlayBouncer_false() =
kosmos.runTest {
- val latest by collectLastValue(underTest.isHomeStatusBarAllowedByScene)
+ val latest by collectLastValue(underTest.isHomeStatusBarAllowed)
kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen)
kosmos.sceneContainerRepository.showOverlay(Overlays.Bouncer)
@@ -535,9 +539,10 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
}
@Test
- fun isHomeStatusBarAllowedByScene_sceneCommunal_false() =
+ @EnableSceneContainer
+ fun isHomeStatusBarAllowed_sceneCommunal_false() =
kosmos.runTest {
- val latest by collectLastValue(underTest.isHomeStatusBarAllowedByScene)
+ val latest by collectLastValue(underTest.isHomeStatusBarAllowed)
kosmos.sceneContainerRepository.snapToScene(Scenes.Communal)
@@ -545,9 +550,10 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
}
@Test
- fun isHomeStatusBarAllowedByScene_sceneShade_false() =
+ @EnableSceneContainer
+ fun isHomeStatusBarAllowed_sceneShade_false() =
kosmos.runTest {
- val latest by collectLastValue(underTest.isHomeStatusBarAllowedByScene)
+ val latest by collectLastValue(underTest.isHomeStatusBarAllowed)
kosmos.sceneContainerRepository.snapToScene(Scenes.Shade)
@@ -555,9 +561,10 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
}
@Test
- fun isHomeStatusBarAllowedByScene_sceneGone_true() =
+ @EnableSceneContainer
+ fun isHomeStatusBarAllowed_sceneGone_true() =
kosmos.runTest {
- val latest by collectLastValue(underTest.isHomeStatusBarAllowedByScene)
+ val latest by collectLastValue(underTest.isHomeStatusBarAllowed)
kosmos.sceneContainerRepository.snapToScene(Scenes.Gone)
@@ -565,9 +572,10 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
}
@Test
- fun isHomeStatusBarAllowedByScene_sceneGoneWithNotificationsShadeOverlay_false() =
+ @EnableSceneContainer
+ fun isHomeStatusBarAllowed_sceneGoneWithNotificationsShadeOverlay_false() =
kosmos.runTest {
- val latest by collectLastValue(underTest.isHomeStatusBarAllowedByScene)
+ val latest by collectLastValue(underTest.isHomeStatusBarAllowed)
kosmos.sceneContainerRepository.snapToScene(Scenes.Gone)
kosmos.sceneContainerRepository.showOverlay(Overlays.NotificationsShade)
@@ -577,14 +585,104 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
}
@Test
- fun isHomeStatusBarAllowedByScene_sceneGoneWithQuickSettingsShadeOverlay_false() =
+ @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
+ @EnableSceneContainer
+ fun isHomeStatusBarAllowed_QsVisibleButInExternalDisplay_defaultStatusBarVisible() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.isHomeStatusBarAllowed)
+
+ kosmos.sceneContainerRepository.snapToScene(Scenes.Gone)
+ kosmos.sceneContainerRepository.showOverlay(Overlays.QuickSettingsShade)
+ kosmos.fakeShadeDisplaysRepository.setDisplayId(EXTERNAL_DISPLAY)
+ runCurrent()
+
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
+ @EnableSceneContainer
+ fun isHomeStatusBarAllowed_QsVisibleButInExternalDisplay_withFlagOff_defaultStatusBarInvisible() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.isHomeStatusBarAllowed)
+
+ kosmos.sceneContainerRepository.snapToScene(Scenes.Gone)
+ kosmos.sceneContainerRepository.showOverlay(Overlays.QuickSettingsShade)
+ kosmos.fakeShadeDisplaysRepository.setDisplayId(EXTERNAL_DISPLAY)
+ runCurrent()
+
+ // Shade position is ignored.
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
+ @EnableSceneContainer
+ fun isHomeStatusBarAllowed_qsVisibleInThisDisplay_thisStatusBarInvisible() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.isHomeStatusBarAllowed)
+
+ kosmos.sceneContainerRepository.snapToScene(Scenes.Gone)
+ kosmos.sceneContainerRepository.showOverlay(Overlays.QuickSettingsShade)
+ kosmos.fakeShadeDisplaysRepository.setDisplayId(DEFAULT_DISPLAY)
+ runCurrent()
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun isHomeStatusBarAllowed_qsExpandedOnDefaultDisplay_statusBarInAnotherDisplay_visible() =
kosmos.runTest {
- val latest by collectLastValue(underTest.isHomeStatusBarAllowedByScene)
+ val underTest = homeStatusBarViewModelFactory(EXTERNAL_DISPLAY)
+ val latest by collectLastValue(underTest.isHomeStatusBarAllowed)
kosmos.sceneContainerRepository.snapToScene(Scenes.Gone)
kosmos.sceneContainerRepository.showOverlay(Overlays.QuickSettingsShade)
runCurrent()
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun isHomeStatusBarAllowed_onDefaultDisplayLockscreen_invisible() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.isHomeStatusBarAllowed)
+
+ kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen)
+ runCurrent()
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ @EnableSceneContainer
+ @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
+ fun isHomeStatusBarAllowed_onExternalDispalyWithLocksceren_invisible() =
+ kosmos.runTest {
+ val underTest = homeStatusBarViewModelFactory(EXTERNAL_DISPLAY)
+ val latest by collectLastValue(underTest.isHomeStatusBarAllowed)
+
+ kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen)
+ runCurrent()
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ @DisableSceneContainer
+ fun isHomeStatusBarAllowed_legacy_onDefaultDisplayLockscreen_invisible() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.isHomeStatusBarAllowed)
+
+ kosmos.fakeKeyguardTransitionRepository.transitionTo(
+ KeyguardState.GONE,
+ KeyguardState.LOCKSCREEN,
+ )
+
+ runCurrent()
+
assertThat(latest).isFalse()
}
@@ -1560,4 +1658,8 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
testScope = testScope,
)
}
+
+ private companion object {
+ const val EXTERNAL_DISPLAY = 1
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
index 13f6bba01135..ba7e17bdd7a5 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
@@ -23,6 +23,7 @@ import androidx.annotation.ColorInt
import androidx.annotation.DimenRes
import androidx.annotation.LayoutRes
import com.android.settingslib.Utils
+import com.android.systemui.statusbar.data.repository.StatusBarConfigurationState
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.onDensityOrFontScaleChanged
import com.android.systemui.statusbar.policy.onThemeChanged
@@ -78,7 +79,7 @@ class ConfigurationStateImpl
constructor(
@Assisted private val configurationController: ConfigurationController,
@Assisted private val context: Context,
-) : ConfigurationState {
+) : ConfigurationState, StatusBarConfigurationState {
private val layoutInflater = LayoutInflater.from(context)
@@ -147,7 +148,7 @@ constructor(
*/
fun create(
context: Context,
- configurationController: ConfigurationController
+ configurationController: ConfigurationController,
): ConfigurationStateImpl
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt
index 36d3eb51283a..3458c9549665 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt
@@ -130,6 +130,7 @@ constructor(
displayRepository.displayIds.collectLatest { displayIds ->
val toRemove = perDisplayInstances.keys - displayIds
toRemove.forEach { displayId ->
+ Log.d(TAG, "<$debugName> destroying instance for displayId=$displayId.")
perDisplayInstances.remove(displayId)?.let { instance ->
(instanceProvider as? PerDisplayInstanceProviderWithTeardown)?.destroyInstance(
instance
@@ -147,6 +148,7 @@ constructor(
// If it doesn't exist, create it and put it in the map.
return perDisplayInstances.computeIfAbsent(displayId) { key ->
+ Log.d(TAG, "<$debugName> creating instance for displayId=$key, as it wasn't available.")
val instance =
traceSection({ "creating instance of $debugName for displayId=$key" }) {
instanceProvider.createInstance(key)
diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUIStateOverride.kt b/packages/SystemUI/src/com/android/systemui/model/SysUIStateOverride.kt
new file mode 100644
index 000000000000..89e06e226997
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/model/SysUIStateOverride.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2025 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.model
+
+import android.view.Display
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/**
+ * This class is used to provide per-display overrides for only certain flags.
+ *
+ * While some of the [SystemUiStateFlags] are per display (e.g. shade expansion, dialog visible),
+ * some of them are device specific (e.g. whether it's awake or not). A [SysUIStateOverride] is
+ * created for each display that is not [Display.DEFAULT_DISPLAY], and if some flags are set on it,
+ * they will override whatever the default display state had in those.
+ */
+class SysUIStateOverride
+@AssistedInject
+constructor(
+ @Assisted override val displayId: Int,
+ private val sceneContainerPlugin: SceneContainerPlugin?,
+ dumpManager: DumpManager,
+ private val defaultDisplayState: SysUiState,
+ private val stateDispatcher: SysUIStateDispatcher,
+) : SysUiStateImpl(displayId, sceneContainerPlugin, dumpManager, stateDispatcher) {
+
+ private val override = StateChange()
+ private var lastSentFlags = defaultDisplayState.flags
+
+ private val defaultFlagsChangedCallback = { _: Long, otherDisplayId: Int ->
+ if (otherDisplayId == Display.DEFAULT_DISPLAY) {
+ commitUpdate()
+ }
+ }
+
+ override fun start() {
+ super.start()
+ stateDispatcher.registerListener(defaultFlagsChangedCallback)
+ }
+
+ override fun destroy() {
+ super.destroy()
+ stateDispatcher.unregisterListener(defaultFlagsChangedCallback)
+ }
+
+ override fun commitUpdate() {
+ if (flags != lastSentFlags) {
+ stateDispatcher.dispatchSysUIStateChange(flags, displayId)
+ lastSentFlags = flags
+ }
+ }
+
+ override val flags: Long
+ get() = override.applyTo(defaultDisplayState.flags)
+
+ override fun setFlag(@SystemUiStateFlags flag: Long, enabled: Boolean): SysUiState {
+ val toSet = flagWithOptionalOverrides(flag, enabled, displayId, sceneContainerPlugin)
+ override.setFlag(flag, toSet)
+ return this
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(displayId: Int): SysUIStateOverride
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt b/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt
index 663355941613..e99ee7ddb919 100644
--- a/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt
+++ b/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt
@@ -16,6 +16,7 @@
package com.android.systemui.model
import android.util.Log
+import android.view.Display
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.display.data.repository.PerDisplayInstanceProviderWithTeardown
@@ -74,6 +75,9 @@ interface SysUiState : Dumpable {
*/
fun destroy()
+ /** Initializes the state after construction. */
+ fun start()
+
/** The display ID this instances is associated with */
val displayId: Int
@@ -84,7 +88,7 @@ interface SysUiState : Dumpable {
private const val TAG = "SysUIState"
-class SysUiStateImpl
+open class SysUiStateImpl
@AssistedInject
constructor(
@Assisted override val displayId: Int,
@@ -93,9 +97,10 @@ constructor(
private val stateDispatcher: SysUIStateDispatcher,
) : SysUiState {
- private val debugName = "SysUiStateImpl-ForDisplay=$displayId"
+ private val debugName
+ get() = "SysUiStateImpl-ForDisplay=$displayId"
- init {
+ override fun start() {
dumpManager.registerNormalDumpable(debugName, this)
}
@@ -222,10 +227,19 @@ fun flagWithOptionalOverrides(
/** Creates and destroy instances of [SysUiState] */
@SysUISingleton
-class SysUIStateInstanceProvider @Inject constructor(private val factory: SysUiStateImpl.Factory) :
- PerDisplayInstanceProviderWithTeardown<SysUiState> {
+class SysUIStateInstanceProvider
+@Inject
+constructor(
+ private val factory: SysUiStateImpl.Factory,
+ private val overrideFactory: SysUIStateOverride.Factory,
+) : PerDisplayInstanceProviderWithTeardown<SysUiState> {
override fun createInstance(displayId: Int): SysUiState {
- return factory.create(displayId)
+ return if (displayId == Display.DEFAULT_DISPLAY) {
+ factory.create(displayId)
+ } else {
+ overrideFactory.create(displayId)
+ }
+ .apply { start() }
}
override fun destroyInstance(instance: SysUiState) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
index 96b224fbd4f3..cd224735cc62 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
@@ -36,6 +36,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.res.R
import com.android.systemui.scene.ui.view.WindowRootView
+import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository
import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
import com.android.systemui.shade.data.repository.ShadeDisplaysRepositoryImpl
import com.android.systemui.shade.display.ShadeDisplayPolicyModule
@@ -205,7 +206,18 @@ object ShadeDisplayAwareModule {
@SysUISingleton
@Provides
- fun provideShadePositionRepository(impl: ShadeDisplaysRepositoryImpl): ShadeDisplaysRepository {
+ fun provideShadePositionRepository(
+ impl: MutableShadeDisplaysRepository
+ ): ShadeDisplaysRepository {
+ ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()
+ return impl
+ }
+
+ @SysUISingleton
+ @Provides
+ fun provideMutableShadePositionRepository(
+ impl: ShadeDisplaysRepositoryImpl
+ ): MutableShadeDisplaysRepository {
ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()
return impl
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt
index 3513334f2a5c..e18ed83a1e8e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt
@@ -22,16 +22,28 @@ import com.android.systemui.shade.display.ShadeDisplayPolicy
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
-class FakeShadeDisplayRepository : ShadeDisplaysRepository {
+class FakeShadeDisplayRepository : MutableShadeDisplaysRepository {
private val _displayId = MutableStateFlow(Display.DEFAULT_DISPLAY)
+ private val _pendingDisplayId = MutableStateFlow(Display.DEFAULT_DISPLAY)
fun setDisplayId(displayId: Int) {
_displayId.value = displayId
}
+ fun setPendingDisplayId(displayId: Int) {
+ _pendingDisplayId.value = displayId
+ }
+
+ override fun onDisplayChangedSucceeded(displayId: Int) {
+ setDisplayId(displayId)
+ }
+
override val displayId: StateFlow<Int>
get() = _displayId
+ override val pendingDisplayId: StateFlow<Int>
+ get() = _pendingDisplayId
+
override val currentPolicy: ShadeDisplayPolicy
get() = FakeShadeDisplayPolicy
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
index 2a14ca44386d..7117a23bfb77 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
@@ -29,6 +29,7 @@ import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
@@ -40,10 +41,28 @@ import kotlinx.coroutines.flow.stateIn
/** Source of truth for the display currently holding the shade. */
interface ShadeDisplaysRepository {
- /** ID of the display which currently hosts the shade */
+ /** ID of the display which currently hosts the shade. */
val displayId: StateFlow<Int>
/** The current policy set. */
val currentPolicy: ShadeDisplayPolicy
+
+ /**
+ * Id of the display that should host the shade.
+ *
+ * If this differs from [displayId], it means there is a shade movement in progress. Classes
+ * that rely on the shade being already moved (and its context/resources updated) should rely on
+ * [displayId]. Classes that need to do work associated with the shade move, should listen at
+ * this.
+ */
+ val pendingDisplayId: StateFlow<Int>
+}
+
+/** Provides a way to set whether the display changed succeeded. */
+interface MutableShadeDisplaysRepository : ShadeDisplaysRepository {
+ /**
+ * To be called when the shade changed window, and its resources have been completely updated.
+ */
+ fun onDisplayChangedSucceeded(displayId: Int)
}
/**
@@ -63,7 +82,7 @@ constructor(
@ShadeOnDefaultDisplayWhenLocked private val shadeOnDefaultDisplayWhenLocked: Boolean,
keyguardRepository: KeyguardRepository,
displayRepository: DisplayRepository,
-) : ShadeDisplaysRepository {
+) : MutableShadeDisplaysRepository {
private val policy: StateFlow<ShadeDisplayPolicy> =
globalSettings
@@ -105,10 +124,16 @@ constructor(
override val currentPolicy: ShadeDisplayPolicy
get() = policy.value
- override val displayId: StateFlow<Int> =
+ override val pendingDisplayId: StateFlow<Int> =
keyguardAwareDisplayPolicy.stateIn(
bgScope,
SharingStarted.WhileSubscribed(),
Display.DEFAULT_DISPLAY,
)
+ private val _committedDisplayId = MutableStateFlow(Display.DEFAULT_DISPLAY)
+ override val displayId: StateFlow<Int> = _committedDisplayId
+
+ override fun onDisplayChangedSucceeded(displayId: Int) {
+ _committedDisplayId.value = displayId
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
index 930b1cb1905c..0e0f58dc8d0e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
@@ -32,6 +32,7 @@ import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker
import com.android.systemui.shade.ShadeTraceLogger.logMoveShadeWindowTo
import com.android.systemui.shade.ShadeTraceLogger.t
import com.android.systemui.shade.ShadeTraceLogger.traceReparenting
+import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository
import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
import com.android.systemui.shade.display.ShadeExpansionIntent
import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
@@ -44,6 +45,7 @@ import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
@@ -55,7 +57,7 @@ import kotlinx.coroutines.withTimeoutOrNull
class ShadeDisplaysInteractor
@Inject
constructor(
- private val shadePositionRepository: ShadeDisplaysRepository,
+ private val shadePositionRepository: MutableShadeDisplaysRepository,
@ShadeDisplayAware private val shadeContext: WindowContext,
@ShadeDisplayAware private val configurationRepository: ConfigurationRepository,
@Background private val bgScope: CoroutineScope,
@@ -72,11 +74,14 @@ constructor(
private val hasActiveNotifications: Boolean
get() = activeNotificationsInteractor.areAnyNotificationsPresentValue
+ /** Current display id of the shade window. */
+ val displayId: StateFlow<Int> = shadePositionRepository.displayId
+
override fun start() {
ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()
listenForWindowContextConfigChanges()
bgScope.launchTraced(TAG) {
- shadePositionRepository.displayId.collectLatest { displayId ->
+ shadePositionRepository.pendingDisplayId.collectLatest { displayId ->
moveShadeWindowTo(displayId)
}
}
@@ -119,6 +124,7 @@ constructor(
reparentToDisplayId(id = destinationId)
}
checkContextDisplayMatchesExpected(destinationId)
+ shadePositionRepository.onDisplayChangedSucceeded(destinationId)
}
}
} catch (e: IllegalStateException) {
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 27d815190d85..5e7e1793ab35 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt
@@ -20,6 +20,7 @@ import com.android.systemui.statusbar.data.repository.KeyguardStatusBarRepositor
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.StatusBarConfigurationStateModule
import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStoreModule
import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryModule
import com.android.systemui.statusbar.data.repository.SystemEventChipAnimationControllerStoreModule
@@ -34,6 +35,7 @@ import dagger.Module
LightBarControllerStoreModule::class,
RemoteInputRepositoryModule::class,
StatusBarConfigurationControllerModule::class,
+ StatusBarConfigurationStateModule::class,
StatusBarContentInsetsProviderStoreModule::class,
StatusBarModeRepositoryModule::class,
StatusBarPhoneDataLayerModule::class,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarConfigurationStateStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarConfigurationStateStore.kt
new file mode 100644
index 000000000000..4e6c5ddc3066
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarConfigurationStateStore.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2025 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.WindowManager.LayoutParams.TYPE_STATUS_BAR
+import com.android.systemui.CoreStartable
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.common.ui.ConfigurationStateImpl
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.display.data.repository.DisplayRepository
+import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository
+import com.android.systemui.display.data.repository.PerDisplayStore
+import com.android.systemui.display.data.repository.PerDisplayStoreImpl
+import com.android.systemui.display.data.repository.SingleDisplayStore
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import dagger.Lazy
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+
+/** Status bar specific interface to disambiguate from the global [ConfigurationState]. */
+interface StatusBarConfigurationState : ConfigurationState
+
+/** Provides per display instances of [ConfigurationState], specifically for the Status Bar. */
+interface StatusBarConfigurationStateStore : PerDisplayStore<StatusBarConfigurationState>
+
+@SysUISingleton
+class MultiDisplayStatusBarConfigurationStateStore
+@Inject
+constructor(
+ @Background backgroundApplicationScope: CoroutineScope,
+ displayRepository: DisplayRepository,
+ private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository,
+ private val statusBarConfigurationControllerStore: StatusBarConfigurationControllerStore,
+ private val factory: ConfigurationStateImpl.Factory,
+) :
+ StatusBarConfigurationStateStore,
+ PerDisplayStoreImpl<StatusBarConfigurationState>(
+ backgroundApplicationScope,
+ displayRepository,
+ ) {
+
+ init {
+ StatusBarConnectedDisplays.unsafeAssertInNewMode()
+ }
+
+ override fun createInstanceForDisplay(displayId: Int): StatusBarConfigurationState? {
+ val displayWindowProperties =
+ displayWindowPropertiesRepository.get(displayId, TYPE_STATUS_BAR) ?: return null
+ val configController =
+ statusBarConfigurationControllerStore.forDisplay(displayId) ?: return null
+ return factory.create(displayWindowProperties.context, configController)
+ }
+
+ override val instanceClass = StatusBarConfigurationState::class.java
+}
+
+@SysUISingleton
+class SingleDisplayStatusBarConfigurationStateStore
+@Inject
+constructor(@Main globalConfigState: ConfigurationState) :
+ StatusBarConfigurationStateStore,
+ PerDisplayStore<StatusBarConfigurationState> by SingleDisplayStore(
+ globalConfigState as StatusBarConfigurationState
+ ) {
+
+ init {
+ StatusBarConnectedDisplays.assertInLegacyMode()
+ }
+}
+
+@Module
+object StatusBarConfigurationStateModule {
+
+ @Provides
+ @SysUISingleton
+ fun store(
+ singleDisplayLazy: Lazy<SingleDisplayStatusBarConfigurationStateStore>,
+ multiDisplayLazy: Lazy<MultiDisplayStatusBarConfigurationStateStore>,
+ ): StatusBarConfigurationStateStore {
+ return if (StatusBarConnectedDisplays.isEnabled) {
+ multiDisplayLazy.get()
+ } else {
+ singleDisplayLazy.get()
+ }
+ }
+
+ @Provides
+ @SysUISingleton
+ @IntoMap
+ @ClassKey(StatusBarConfigurationStateStore::class)
+ fun storeAsCoreStartable(
+ multiDisplayLazy: Lazy<MultiDisplayStatusBarConfigurationStateStore>
+ ): CoreStartable {
+ return if (StatusBarConnectedDisplays.isEnabled) {
+ multiDisplayLazy.get()
+ } else {
+ CoreStartable.NOP
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt
index aa81ebf22ac6..30aec8dabcaa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt
@@ -18,11 +18,10 @@ package com.android.systemui.statusbar.notification.icon.ui.viewbinder
import android.view.Display
import androidx.lifecycle.lifecycleScope
-import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.app.tracing.traceSection
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.shade.ShadeDisplayAware
+import com.android.systemui.statusbar.data.repository.StatusBarConfigurationStateStore
import com.android.systemui.statusbar.notification.collection.NotifCollection
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
@@ -37,7 +36,8 @@ class NotificationIconContainerStatusBarViewBinder
@Inject
constructor(
private val viewModel: NotificationIconContainerStatusBarViewModel,
- @ShadeDisplayAware private val configuration: ConfigurationState,
+ private val configurationStore: StatusBarConfigurationStateStore,
+ private val defaultConfigurationState: ConfigurationState,
private val systemBarUtilsState: SystemBarUtilsState,
private val failureTracker: StatusBarIconViewBindingFailureTracker,
private val defaultDisplayViewStore: StatusBarNotificationIconViewStore,
@@ -56,12 +56,14 @@ constructor(
lifecycleScope.launch { it.activate() }
}
}
+ val configurationState: ConfigurationState =
+ configurationStore.forDisplay(displayId) ?: defaultConfigurationState
lifecycleScope.launch {
NotificationIconContainerViewBinder.bind(
displayId = displayId,
view = view,
viewModel = viewModel,
- configuration = configuration,
+ configuration = configurationState,
systemBarUtilsState = systemBarUtilsState,
failureTracker = failureTracker,
viewStore = viewStore,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
index cdd02865bbee..cfd50973924d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
@@ -258,7 +258,7 @@ constructor(
if (SceneContainerFlag.isEnabled) {
listener?.let { listener ->
launch {
- viewModel.isHomeStatusBarAllowedByScene.collect {
+ viewModel.isHomeStatusBarAllowed.collect {
listener.onIsHomeStatusBarAllowedBySceneChanged(it)
}
}
@@ -503,7 +503,7 @@ interface StatusBarVisibilityChangeListener {
/**
* Called when the scene state has changed such that the home status bar is newly allowed or no
- * longer allowed. See [HomeStatusBarViewModel.isHomeStatusBarAllowedByScene].
+ * longer allowed. See [HomeStatusBarViewModel.isHomeStatusBarAllowed].
*/
fun onIsHomeStatusBarAllowedBySceneChanged(isHomeStatusBarAllowedByScene: Boolean)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
index 9975aff938d6..3bae91a0ebe3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel
import android.annotation.ColorInt
import android.graphics.Rect
+import android.view.Display
import android.view.View
import androidx.compose.runtime.getValue
import com.android.app.tracing.coroutines.launchTraced as launch
@@ -41,7 +42,9 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.domain.interactor.ShadeDisplaysInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import com.android.systemui.statusbar.chips.mediaprojection.domain.model.MediaProjectionStopDialogModel
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel
@@ -73,6 +76,7 @@ import com.android.systemui.statusbar.pipeline.shared.ui.model.VisibilityModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import javax.inject.Provider
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.awaitCancellation
@@ -142,13 +146,12 @@ interface HomeStatusBarViewModel : Activatable {
val popupChips: List<PopupChipModel.Shown>
/**
- * True if the current scene can show the home status bar (aka this status bar), and false if
- * the current scene should never show the home status bar.
+ * True if the status bar should be visible.
*
* TODO(b/364360986): Once the is<SomeChildView>Visible flows are fully enabled, we shouldn't
* need this flow anymore.
*/
- val isHomeStatusBarAllowedByScene: StateFlow<Boolean>
+ val isHomeStatusBarAllowed: StateFlow<Boolean>
/** True if the home status bar is showing, and there is no HUN happening */
val canShowOngoingActivityChips: Flow<Boolean>
@@ -222,6 +225,7 @@ constructor(
statusBarContentInsetsViewModelStore: StatusBarContentInsetsViewModelStore,
@Background bgScope: CoroutineScope,
@Background bgDispatcher: CoroutineDispatcher,
+ shadeDisplaysInteractor: Provider<ShadeDisplaysInteractor>,
) : HomeStatusBarViewModel, ExclusiveActivatable() {
private val hydrator = Hydrator(traceName = "HomeStatusBarViewModel.hydrator")
@@ -258,12 +262,44 @@ constructor(
override val popupChips
get() = statusBarPopupChips.shownPopupChips
- override val isHomeStatusBarAllowedByScene: StateFlow<Boolean> =
+ /**
+ * Whether the display of this statusbar has the shade window (that is hosting shade container
+ * and lockscreen, among other things).
+ */
+ private val isShadeWindowOnThisDisplay =
+ if (ShadeWindowGoesAround.isEnabled) {
+ shadeDisplaysInteractor.get().displayId.map { shadeDisplayId ->
+ thisDisplayId == shadeDisplayId
+ }
+ } else {
+ // Shade doesn't move anywhere, it is always on the default display.
+ flowOf(thisDisplayId == Display.DEFAULT_DISPLAY)
+ }
+
+ private val isShadeVisibleOnAnyDisplay =
+ if (SceneContainerFlag.isEnabled) {
+ sceneInteractor.currentOverlays.map { currentOverlays ->
+ (Overlays.NotificationsShade in currentOverlays ||
+ Overlays.QuickSettingsShade in currentOverlays)
+ }
+ } else {
+ shadeInteractor.isAnyFullyExpanded
+ }
+
+ private val isShadeVisibleOnThisDisplay: Flow<Boolean> =
+ combine(isShadeWindowOnThisDisplay, isShadeVisibleOnAnyDisplay) {
+ hasShade,
+ isShadeVisibleOnAnyDisplay ->
+ hasShade && isShadeVisibleOnAnyDisplay
+ }
+
+ private val isHomeStatusBarAllowedByScene: Flow<Boolean> =
combine(
sceneInteractor.currentScene,
- sceneInteractor.currentOverlays,
+ isShadeVisibleOnThisDisplay,
sceneContainerOcclusionInteractor.invisibleDueToOcclusion,
- ) { currentScene, currentOverlays, isOccluded ->
+ ) { currentScene, isShadeVisible, isOccluded ->
+
// All scenes have their own status bars, so we should only show the home status bar
// if we're not in a scene. There are two exceptions:
// 1) The shade (notifications or quick settings) is shown, because it has its own
@@ -271,9 +307,7 @@ constructor(
// 2) If the scene is occluded, then the occluding app needs to show the status bar.
// (Fullscreen apps actually won't show the status bar but that's handled with the
// rest of our fullscreen app logic, which lives elsewhere.)
- (currentScene == Scenes.Gone &&
- Overlays.NotificationsShade !in currentOverlays &&
- Overlays.QuickSettingsShade !in currentOverlays) || isOccluded
+ (currentScene == Scenes.Gone && !isShadeVisible) || isOccluded
}
.distinctUntilChanged()
.logDiffsForTable(
@@ -281,7 +315,6 @@ constructor(
columnName = COL_ALLOWED_BY_SCENE,
initialValue = false,
)
- .stateIn(bgScope, SharingStarted.WhileSubscribed(), initialValue = false)
override val areNotificationsLightsOut: Flow<Boolean> =
if (NotificationsLiveDataStoreRefactor.isUnexpectedlyInLegacyMode()) {
@@ -330,21 +363,29 @@ constructor(
* if we shouldn't be showing any part of the home status bar.
*/
private val isHomeScreenStatusBarAllowedLegacy: Flow<Boolean> =
- combine(
- keyguardTransitionInteractor.currentKeyguardState,
- shadeInteractor.isAnyFullyExpanded,
- ) { currentKeyguardState, isShadeExpanded ->
- (currentKeyguardState == GONE || currentKeyguardState == OCCLUDED) && !isShadeExpanded
+ combine(keyguardTransitionInteractor.currentKeyguardState, isShadeVisibleOnThisDisplay) {
+ currentKeyguardState,
+ isShadeVisibleOnThisDisplay ->
+ (currentKeyguardState == GONE || currentKeyguardState == OCCLUDED) &&
+ !isShadeVisibleOnThisDisplay
// TODO(b/364360986): Add edge cases, like secure camera launch.
}
- private val isHomeStatusBarAllowed: Flow<Boolean> =
+ // "Compat" to cover both legacy and Scene container case in one flow.
+ private val isHomeStatusBarAllowedCompat =
if (SceneContainerFlag.isEnabled) {
isHomeStatusBarAllowedByScene
} else {
isHomeScreenStatusBarAllowedLegacy
}
+ override val isHomeStatusBarAllowed =
+ isHomeStatusBarAllowedCompat.stateIn(
+ bgScope,
+ SharingStarted.WhileSubscribed(),
+ initialValue = false,
+ )
+
private val shouldHomeStatusBarBeVisible =
combine(
isHomeStatusBarAllowed,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/model/SysUiStateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/model/SysUiStateKosmos.kt
index dbd1c9ce56fe..848fed6b0590 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/model/SysUiStateKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/model/SysUiStateKosmos.kt
@@ -48,3 +48,15 @@ val Kosmos.fakeSysUIStatePerDisplayRepository by Fixture { FakePerDisplayReposit
val Kosmos.sysuiStateInteractor by Fixture {
SysUIStateDisplaysInteractor(fakeSysUIStatePerDisplayRepository, displayRepository)
}
+
+val Kosmos.sysUiStateOverrideFactory by Fixture {
+ { displayId: Int ->
+ SysUIStateOverride(
+ displayId,
+ sceneContainerPlugin,
+ dumpManager,
+ sysUiState,
+ sysUIStateDispatcher,
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
index a97c651ba426..5c4deaadffd5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
@@ -25,6 +25,7 @@ import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.log.table.tableLogBufferFactory
import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.shade.domain.interactor.shadeDisplaysInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel
import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel
@@ -40,29 +41,34 @@ import com.android.systemui.statusbar.pipeline.shared.domain.interactor.homeStat
import com.android.systemui.statusbar.pipeline.shared.domain.interactor.homeStatusBarInteractor
var Kosmos.homeStatusBarViewModel: HomeStatusBarViewModel by
+ Kosmos.Fixture { homeStatusBarViewModelFactory.invoke(testableContext.displayId) }
+var Kosmos.homeStatusBarViewModelFactory: (Int) -> HomeStatusBarViewModel by
Kosmos.Fixture {
- HomeStatusBarViewModelImpl(
- testableContext.displayId,
- batteryViewModelFactory,
- tableLogBufferFactory,
- homeStatusBarInteractor,
- homeStatusBarIconBlockListInteractor,
- lightsOutInteractor,
- activeNotificationsInteractor,
- darkIconInteractor,
- headsUpNotificationInteractor,
- keyguardTransitionInteractor,
- keyguardInteractor,
- statusBarOperatorNameViewModel,
- sceneInteractor,
- sceneContainerOcclusionInteractor,
- shadeInteractor,
- shareToAppChipViewModel,
- ongoingActivityChipsViewModel,
- statusBarPopupChipsViewModelFactory,
- systemStatusEventAnimationInteractor,
- multiDisplayStatusBarContentInsetsViewModelStore,
- backgroundScope,
- testDispatcher,
- )
+ { displayId ->
+ HomeStatusBarViewModelImpl(
+ displayId,
+ batteryViewModelFactory,
+ tableLogBufferFactory,
+ homeStatusBarInteractor,
+ homeStatusBarIconBlockListInteractor,
+ lightsOutInteractor,
+ activeNotificationsInteractor,
+ darkIconInteractor,
+ headsUpNotificationInteractor,
+ keyguardTransitionInteractor,
+ keyguardInteractor,
+ statusBarOperatorNameViewModel,
+ sceneInteractor,
+ sceneContainerOcclusionInteractor,
+ shadeInteractor,
+ shareToAppChipViewModel,
+ ongoingActivityChipsViewModel,
+ statusBarPopupChipsViewModelFactory,
+ systemStatusEventAnimationInteractor,
+ multiDisplayStatusBarContentInsetsViewModelStore,
+ backgroundScope,
+ testDispatcher,
+ { shadeDisplaysInteractor },
+ )
+ }
}