diff options
16 files changed, 596 insertions, 25 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayInstanceRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayInstanceRepositoryImplTest.kt new file mode 100644 index 000000000000..299105e2dabd --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayInstanceRepositoryImplTest.kt @@ -0,0 +1,110 @@ +/* + * 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.display.data.repository + +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.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class PerDisplayInstanceRepositoryImplTest : SysuiTestCase() { + + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val testScope = kosmos.testScope + private val fakeDisplayRepository = kosmos.displayRepository + private val fakePerDisplayInstanceProviderWithTeardown = + kosmos.fakePerDisplayInstanceProviderWithTeardown + + private val underTest: PerDisplayInstanceRepositoryImpl<TestPerDisplayInstance> = + kosmos.fakePerDisplayInstanceRepository + + @Before + fun addDisplays() = runBlocking { + fakeDisplayRepository += createDisplay(DEFAULT_DISPLAY_ID) + fakeDisplayRepository += createDisplay(NON_DEFAULT_DISPLAY_ID) + } + + @Test + fun forDisplay_defaultDisplay_multipleCalls_returnsSameInstance() = + testScope.runTest { + val instance = underTest[DEFAULT_DISPLAY_ID] + + assertThat(underTest[DEFAULT_DISPLAY_ID]).isSameInstanceAs(instance) + } + + @Test + fun forDisplay_nonDefaultDisplay_multipleCalls_returnsSameInstance() = + testScope.runTest { + val instance = underTest[NON_DEFAULT_DISPLAY_ID] + + assertThat(underTest[NON_DEFAULT_DISPLAY_ID]).isSameInstanceAs(instance) + } + + @Test + fun forDisplay_nonDefaultDisplay_afterDisplayRemoved_returnsNewInstance() = + testScope.runTest { + val instance = underTest[NON_DEFAULT_DISPLAY_ID] + + fakeDisplayRepository -= NON_DEFAULT_DISPLAY_ID + fakeDisplayRepository += createDisplay(NON_DEFAULT_DISPLAY_ID) + + assertThat(underTest[NON_DEFAULT_DISPLAY_ID]).isNotSameInstanceAs(instance) + } + + @Test + fun forDisplay_nonExistingDisplayId_returnsNull() = + testScope.runTest { assertThat(underTest[NON_EXISTING_DISPLAY_ID]).isNull() } + + @Test + fun forDisplay_afterDisplayRemoved_destroyInstanceInvoked() = + testScope.runTest { + val instance = underTest[NON_DEFAULT_DISPLAY_ID] + + fakeDisplayRepository -= NON_DEFAULT_DISPLAY_ID + + assertThat(fakePerDisplayInstanceProviderWithTeardown.destroyed) + .containsExactly(instance) + } + + @Test + fun forDisplay_withoutDisplayRemoval_destroyInstanceIsNotInvoked() = + testScope.runTest { + underTest[NON_DEFAULT_DISPLAY_ID] + + assertThat(fakePerDisplayInstanceProviderWithTeardown.destroyed).isEmpty() + } + + private fun createDisplay(displayId: Int): Display = + display(type = Display.TYPE_INTERNAL, id = displayId) + + companion object { + private const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY + private const val NON_DEFAULT_DISPLAY_ID = DEFAULT_DISPLAY_ID + 1 + private const val NON_EXISTING_DISPLAY_ID = DEFAULT_DISPLAY_ID + 2 + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SceneContainerPluginTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SceneContainerPluginTest.kt new file mode 100644 index 000000000000..b2e29cf60c27 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SceneContainerPluginTest.kt @@ -0,0 +1,93 @@ +/* + * 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.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +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.flags.EnableSceneContainer +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.shared.model.fakeSceneDataSource +import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableSceneContainer +class SceneContainerPluginTest : SysuiTestCase() { + private val kosmos = testKosmos() + + private val shadeDisplayRepository = kosmos.fakeShadeDisplaysRepository + private val sceneDataSource = kosmos.fakeSceneDataSource + + private val underTest = kosmos.sceneContainerPlugin + + @Test + @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) + fun flagValueOverride_differentDisplayId_alwaysFalse() { + sceneDataSource.changeScene(Scenes.Shade) + + shadeDisplayRepository.setDisplayId(1) + + assertThat( + underTest.flagValueOverride( + flag = SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE, + displayId = 2, + ) + ) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) + fun flagValueOverride_sameDisplayId_returnsTrue() { + sceneDataSource.changeScene(Scenes.Shade) + + shadeDisplayRepository.setDisplayId(1) + + assertThat( + underTest.flagValueOverride( + flag = SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE, + displayId = 1, + ) + ) + .isTrue() + } + + @Test + @DisableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) + fun flagValueOverride_differentDisplayId_shadeGoesAroundFlagOff_returnsTrue() { + sceneDataSource.changeScene(Scenes.Shade) + + shadeDisplayRepository.setDisplayId(1) + + assertThat( + underTest.flagValueOverride( + flag = SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE, + displayId = 2, + ) + ) + .isTrue() + } +} 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 eb4277a66be8..779e10a9682a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateTest.java @@ -19,6 +19,9 @@ package com.android.systemui.model; import static android.view.Display.DEFAULT_DISPLAY; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -29,8 +32,8 @@ 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.kosmos.KosmosJavaAdapter; -import com.android.systemui.settings.DisplayTracker; import org.junit.Before; import org.junit.Test; @@ -48,11 +51,11 @@ public class SysUiStateTest extends SysuiTestCase { private KosmosJavaAdapter mKosmos; private SysUiState.SysUiStateCallback mCallback; private SysUiState mFlagsContainer; - private DisplayTracker mDisplayTracker; private SceneContainerPlugin mSceneContainerPlugin; + private DumpManager mDumpManager; private SysUiState createInstance(int displayId) { - var sysuiState = new SysUiStateImpl(displayId, mSceneContainerPlugin); + var sysuiState = new SysUiStateImpl(displayId, mSceneContainerPlugin, mDumpManager); sysuiState.addCallback(mCallback); return sysuiState; } @@ -60,10 +63,10 @@ public class SysUiStateTest extends SysuiTestCase { @Before public void setup() { mKosmos = new KosmosJavaAdapter(this); - mDisplayTracker = mKosmos.getDisplayTracker(); mFlagsContainer = mKosmos.getSysuiState(); mSceneContainerPlugin = mKosmos.getSceneContainerPlugin(); mCallback = mock(SysUiState.SysUiStateCallback.class); + mDumpManager = mock(DumpManager.class); mFlagsContainer = createInstance(DEFAULT_DISPLAY); } @@ -139,6 +142,18 @@ public class SysUiStateTest extends SysuiTestCase { verify(mCallback, never()).onSystemUiStateChanged(FLAG_1); } + @Test + public void init_registersWithDumpManager() { + verify(mDumpManager).registerNormalDumpable(any(), eq(mFlagsContainer)); + } + + @Test + public void destroy_unregistersWithDumpManager() { + mFlagsContainer.destroy(); + + verify(mDumpManager).unregisterDumpable(anyString()); + } + private void setFlags(int... flags) { setFlags(mFlagsContainer, flags); } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ILauncherProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ILauncherProxy.aidl index 10b930381c44..ade63b1dc9c9 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ILauncherProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ILauncherProxy.aidl @@ -66,7 +66,7 @@ oneway interface ILauncherProxy { /** * Sent when some system ui state changes. */ - void onSystemUiStateChanged(long stateFlags) = 16; + void onSystemUiStateChanged(long stateFlags, int displayId) = 16; /** * Sent when suggested rotation button could be shown diff --git a/packages/SystemUI/src/com/android/systemui/dagger/PerDisplayRepositoriesModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/PerDisplayRepositoriesModule.kt new file mode 100644 index 000000000000..39708a743c23 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dagger/PerDisplayRepositoriesModule.kt @@ -0,0 +1,45 @@ +/* + * 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.dagger + +import com.android.systemui.display.data.repository.DefaultDisplayOnlyInstanceRepositoryImpl +import com.android.systemui.display.data.repository.PerDisplayInstanceRepositoryImpl +import com.android.systemui.display.data.repository.PerDisplayRepository +import com.android.systemui.model.SysUIStateInstanceProvider +import com.android.systemui.model.SysUiState +import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround +import dagger.Module +import dagger.Provides + +/** This module is meant to contain all the code to create the various [PerDisplayRepository<>]. */ +@Module +class PerDisplayRepositoriesModule { + + @SysUISingleton + @Provides + fun provideSysUiStateRepository( + repositoryFactory: PerDisplayInstanceRepositoryImpl.Factory<SysUiState>, + instanceProvider: SysUIStateInstanceProvider, + ): PerDisplayRepository<SysUiState> { + val debugName = "SysUiStatePerDisplayRepo" + return if (ShadeWindowGoesAround.isEnabled) { + repositoryFactory.create(debugName, instanceProvider) + } else { + DefaultDisplayOnlyInstanceRepositoryImpl(debugName, instanceProvider) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index bbc470c77ee4..f08126af0a7a 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -65,9 +65,9 @@ import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.demomode.dagger.DemoModeModule; import com.android.systemui.deviceentry.DeviceEntryModule; import com.android.systemui.display.DisplayModule; +import com.android.systemui.display.data.repository.PerDisplayRepository; import com.android.systemui.doze.dagger.DozeComponent; import com.android.systemui.dreams.dagger.DreamModule; -import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.FlagDependenciesModule; import com.android.systemui.flags.FlagsModule; @@ -88,7 +88,6 @@ import com.android.systemui.mediaprojection.appselector.MediaProjectionActivitie import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherModule; import com.android.systemui.mediarouter.MediaRouterModule; import com.android.systemui.model.SysUiState; -import com.android.systemui.model.SysUiStateImpl; import com.android.systemui.motiontool.MotionToolModule; import com.android.systemui.navigationbar.NavigationBarComponent; import com.android.systemui.navigationbar.gestural.dagger.GestureModule; @@ -289,7 +288,8 @@ import javax.inject.Named; UtilModule.class, NoteTaskModule.class, WalletModule.class, - LowLightModule.class + LowLightModule.class, + PerDisplayRepositoriesModule.class }, subcomponents = { ComplicationComponent.class, @@ -326,11 +326,8 @@ public abstract class SystemUIModule { @SysUISingleton @Provides static SysUiState provideSysUiState( - DumpManager dumpManager, - SysUiStateImpl.Factory sysUiStateFactory) { - final SysUiState state = sysUiStateFactory.create(Display.DEFAULT_DISPLAY); - dumpManager.registerDumpable(state); - return state; + PerDisplayRepository<SysUiState> repository) { + return repository.get(Display.DEFAULT_DISPLAY); } /** 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 new file mode 100644 index 000000000000..63c46bb30a07 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt @@ -0,0 +1,191 @@ +/* + * 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.display.data.repository + +import android.util.Log +import android.view.Display +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.app.tracing.traceSection +import com.android.systemui.Dumpable +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dump.DumpManager +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import java.io.PrintWriter +import java.util.concurrent.ConcurrentHashMap +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest + +/** + * Used to create instances of type `T` for a specific display. + * + * This is useful for resources or objects that need to be managed independently for each connected + * display (e.g., UI state, rendering contexts, or display-specific configurations). + * + * Note that in most cases this can be implemented by a simple `@AssistedFactory` with `displayId` + * parameter + * + * ```kotlin + * class SomeType @AssistedInject constructor(@Assisted displayId: Int,..) + * @AssistedFactory + * interface Factory { + * fun create(displayId: Int): SomeType + * } + * } + * ``` + * + * Then it can be used to create a [PerDisplayRepository] as follows: + * ```kotlin + * // Injected: + * val repositoryFactory: PerDisplayRepositoryImpl.Factory + * val instanceFactory: PerDisplayRepositoryImpl.Factory + * // repository creation: + * repositoryFactory.create(instanceFactory::create) + * ``` + * + * @see PerDisplayRepository For how to retrieve and manage instances created by this factory. + */ +fun interface PerDisplayInstanceProvider<T> { + /** Creates an instance for a display. */ + fun createInstance(displayId: Int): T? +} + +/** + * Extends [PerDisplayInstanceProvider], adding support for destroying the instance. + * + * This is useful for releasing resources associated with a display when it is disconnected or when + * the per-display instance is no longer needed. + */ +interface PerDisplayInstanceProviderWithTeardown<T> : PerDisplayInstanceProvider<T> { + /** Destroys a previously created instance of `T` forever. */ + fun destroyInstance(instance: T) +} + +/** + * Provides access to per-display instances of type `T`. + * + * Acts as a repository, managing the caching and retrieval of instances created by a + * [PerDisplayInstanceProvider]. It ensures that only one instance of `T` exists per display ID. + */ +interface PerDisplayRepository<T> { + /** Gets the cached instance or create a new one for a given display. */ + operator fun get(displayId: Int): T? + + /** Debug name for this repository, mainly for tracing and logging. */ + val debugName: String +} + +/** + * Default implementation of [PerDisplayRepository]. + * + * This class manages a cache of per-display instances of type `T`, creating them using a provided + * [PerDisplayInstanceProvider] and optionally tearing them down using a + * [PerDisplayInstanceProviderWithTeardown] when displays are disconnected. + * + * It listens to the [DisplayRepository] to detect when displays are added or removed, and + * automatically manages the lifecycle of the per-display instances. + * + * Note that this is a [PerDisplayStoreImpl] 2.0 that doesn't require [CoreStartable] bindings, + * providing all args in the constructor. + */ +class PerDisplayInstanceRepositoryImpl<T> +@AssistedInject +constructor( + @Assisted override val debugName: String, + @Assisted private val instanceProvider: PerDisplayInstanceProvider<T>, + @Background private val backgroundApplicationScope: CoroutineScope, + private val displayRepository: DisplayRepository, + private val dumpManager: DumpManager, +) : PerDisplayRepository<T>, Dumpable { + + private val perDisplayInstances = ConcurrentHashMap<Int, T?>() + + init { + backgroundApplicationScope.launch("$debugName#start") { start() } + } + + private suspend fun start() { + dumpManager.registerDumpable(this) + displayRepository.displayIds.collectLatest { displayIds -> + val toRemove = perDisplayInstances.keys - displayIds + toRemove.forEach { displayId -> + perDisplayInstances.remove(displayId)?.let { instance -> + (instanceProvider as? PerDisplayInstanceProviderWithTeardown)?.destroyInstance( + instance + ) + } + } + } + } + + override fun get(displayId: Int): T? { + if (displayRepository.getDisplay(displayId) == null) { + Log.e(TAG, "<$debugName: Display with id $displayId doesn't exist.") + return null + } + + // If it doesn't exist, create it and put it in the map. + return perDisplayInstances.computeIfAbsent(displayId) { key -> + val instance = + traceSection({ "creating instance of $debugName for displayId=$key" }) { + instanceProvider.createInstance(key) + } + if (instance == null) { + Log.e( + TAG, + "<$debugName> returning null because createInstance($key) returned null.", + ) + } + instance + } + } + + @AssistedFactory + interface Factory<T> { + fun create( + debugName: String, + instanceProvider: PerDisplayInstanceProvider<T>, + ): PerDisplayInstanceRepositoryImpl<T> + } + + companion object { + private const val TAG = "PerDisplayInstanceRepo" + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println(perDisplayInstances) + } +} + +/** + * Provides an instance of a given class **only** for the default display, even if asked for another + * display. + * + * This is useful in case of flag refactors: it can be provided instead of an instance of + * [PerDisplayInstanceRepositoryImpl] when a flag related to multi display refactoring is off. + */ +class DefaultDisplayOnlyInstanceRepositoryImpl<T>( + override val debugName: String, + private val instanceProvider: PerDisplayInstanceProvider<T>, +) : PerDisplayRepository<T> { + private val lazyDefaultDisplayInstance by lazy { + instanceProvider.createInstance(Display.DEFAULT_DISPLAY) + } + + override fun get(displayId: Int): T? = lazyDefaultDisplayInstance +} diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt index 564588c159bd..81aca27b9c28 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt @@ -26,6 +26,7 @@ import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.CoroutineScope /** Provides per display instances of [T]. */ +@Deprecated("Use PerDisplayInstanceProvider<T> instead") interface PerDisplayStore<T> { /** @@ -43,6 +44,7 @@ interface PerDisplayStore<T> { fun forDisplay(displayId: Int): T? } +@Deprecated("Use PerDisplayRepository<T> instead") abstract class PerDisplayStoreImpl<T>( @Background private val backgroundApplicationScope: CoroutineScope, private val displayRepository: DisplayRepository, @@ -106,6 +108,11 @@ abstract class PerDisplayStoreImpl<T>( * Will be called when the display associated with [instance] was removed. It allows to perform * any clean up if needed. */ + @Deprecated( + "Use PerDisplayInstanceProviderWithTeardown instead, and let " + + "PerDisplayInstanceRepositoryImpl decide when to destroy the instance (e.g. on " + + "display removal or other conditions." + ) open suspend fun onDisplayRemovalAction(instance: T) {} override fun dump(pw: PrintWriter, args: Array<out String>) { diff --git a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt index ea515c96d3f0..4559a7aea1a2 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt +++ b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt @@ -25,6 +25,8 @@ 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.data.repository.ShadeDisplaysRepository +import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_COMMUNAL_HUB_SHOWING import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED @@ -35,6 +37,7 @@ import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_B import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags import dagger.Lazy import javax.inject.Inject +import kotlinx.coroutines.flow.StateFlow /** * A plugin for [SysUiState] that provides overrides for certain state flags that must be pulled @@ -46,17 +49,28 @@ class SceneContainerPlugin constructor( private val sceneInteractor: Lazy<SceneInteractor>, private val occlusionInteractor: Lazy<SceneContainerOcclusionInteractor>, + private val shadeDisplaysRepository: Lazy<ShadeDisplaysRepository>, ) { + private val shadeDisplayId: StateFlow<Int> by lazy { shadeDisplaysRepository.get().displayId } + /** * Returns an override value for the given [flag] or `null` if the scene framework isn't enabled * or if the flag value doesn't need to be overridden. */ - fun flagValueOverride(@SystemUiStateFlags flag: Long): Boolean? { + fun flagValueOverride(@SystemUiStateFlags flag: Long, displayId: Int): Boolean? { if (!SceneContainerFlag.isEnabled) { return null } + if (ShadeWindowGoesAround.isEnabled && shadeDisplayId.value != displayId) { + // The shade is in another display. All flags related to the shade container will map to + // false on other displays now. + // + // Note that this assumes there is only one SceneContainer and it is only on the shade + // window display. If there will be more, this will need to be revisited + return false + } val transitionState = sceneInteractor.get().transitionState.value val idleTransitionStateOrNull = transitionState as? ObservableTransitionState.Idle val invisibleDueToOcclusion = occlusionInteractor.get().invisibleDueToOcclusion.value diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt b/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt index 432dadbb2136..9e9a1df76b54 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt +++ b/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt @@ -18,6 +18,9 @@ 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 +import com.android.systemui.dump.DumpManager import com.android.systemui.model.SysUiState.SysUiStateCallback import com.android.systemui.shared.system.QuickStepContract import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags @@ -26,6 +29,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dalvik.annotation.optimization.NeverCompile import java.io.PrintWriter +import javax.inject.Inject /** Contains sysUi state flags and notifies registered listeners whenever changes happen. */ interface SysUiState : Dumpable { @@ -70,6 +74,13 @@ interface SysUiState : Dumpable { ) {} } + /** + * Destroys an instance. It shouldn't be used anymore afterwards. + * + * This is mainly used to clean up instances associated with displays that are removed. + */ + fun destroy() + companion object { const val DEBUG: Boolean = false } @@ -80,7 +91,15 @@ class SysUiStateImpl constructor( @Assisted private val displayId: Int, private val sceneContainerPlugin: SceneContainerPlugin?, + private val dumpManager: DumpManager, ) : SysUiState { + + private val debugName = "SysUiStateImpl-ForDisplay=$displayId" + + init { + dumpManager.registerNormalDumpable(debugName, this) + } + /** Returns the current sysui state flags. */ @get:SystemUiStateFlags @SystemUiStateFlags @@ -113,7 +132,8 @@ constructor( /** Methods to this call can be chained together before calling [.commitUpdate]. */ override fun setFlag(@SystemUiStateFlags flag: Long, enabled: Boolean): SysUiState { var enabled = enabled - val overrideOrNull = sceneContainerPlugin?.flagValueOverride(flag) + val overrideOrNull = + sceneContainerPlugin?.flagValueOverride(flag = flag, displayId = displayId) if (overrideOrNull != null && enabled != overrideOrNull) { if (SysUiState.DEBUG) { Log.d( @@ -187,6 +207,10 @@ constructor( pw.println(QuickStepContract.isAssistantGestureDisabled(flags)) } + override fun destroy() { + dumpManager.unregisterDumpable(debugName) + } + @AssistedFactory interface Factory { /** Creates a new instance of [SysUiStateImpl] for a given [displayId]. */ @@ -197,3 +221,16 @@ constructor( private val TAG: String = SysUiState::class.java.simpleName } } + +/** Creates and destroy instances of [SysUiState] */ +@SysUISingleton +class SysUIStateInstanceProvider @Inject constructor(private val factory: SysUiStateImpl.Factory) : + PerDisplayInstanceProviderWithTeardown<SysUiState> { + override fun createInstance(displayId: Int): SysUiState { + return factory.create(displayId) + } + + override fun destroyInstance(instance: SysUiState) { + instance.destroy() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/LauncherProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/LauncherProxyService.java index 9af4630bf492..8253a071dea2 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/LauncherProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/LauncherProxyService.java @@ -66,6 +66,7 @@ import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; +import android.view.Display; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; @@ -94,6 +95,7 @@ import com.android.systemui.keyguard.KeyguardWmStateRefactor; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager; import com.android.systemui.model.SysUiState; +import com.android.systemui.model.SysUiState.SysUiStateCallback; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.navigationbar.views.NavigationBar; @@ -584,7 +586,8 @@ public class LauncherProxyService implements CallbackController<LauncherProxyLis // Force-update the systemui state flags updateSystemUiStateFlags(); - notifySystemUiStateFlags(mSysUiState.getFlags()); + // TODO b/398011576 - send the state for all displays. + notifySystemUiStateFlags(mSysUiState.getFlags(), Display.DEFAULT_DISPLAY); notifyConnectionChanged(); } @@ -650,6 +653,17 @@ public class LauncherProxyService implements CallbackController<LauncherProxyLis } }; + private final SysUiStateCallback mSysUiStateCallback = + new SysUiStateCallback() { + @Override + public void onSystemUiStateChanged(long sysUiFlags) { + } + + @Override + public void onSystemUiStateChangedForDisplay(long sysUiFlags, int displayId) { + notifySystemUiStateFlags(sysUiFlags, displayId); + } + }; @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Inject public LauncherProxyService(Context context, @@ -708,8 +722,10 @@ public class LauncherProxyService implements CallbackController<LauncherProxyLis com.android.internal.R.string.config_recentsComponentName)); mQuickStepIntent = new Intent(ACTION_QUICKSTEP) .setPackage(mRecentsComponentName.getPackageName()); + // TODO b/398011576 - Here we're still only handling the default display state. We should + // have a callback for any sysuiState change. mSysUiState = sysUiState; - mSysUiState.addCallback(this::notifySystemUiStateFlags); + mSysUiState.addCallback(mSysUiStateCallback); mUiEventLogger = uiEventLogger; mDisplayTracker = displayTracker; mUnfoldTransitionProgressForwarder = unfoldTransitionProgressForwarder; @@ -815,14 +831,14 @@ public class LauncherProxyService implements CallbackController<LauncherProxyLis } } - private void notifySystemUiStateFlags(@SystemUiStateFlags long flags) { + private void notifySystemUiStateFlags(@SystemUiStateFlags long flags, int displayId) { if (SysUiState.DEBUG) { Log.d(TAG_OPS, "Notifying sysui state change to launcher service: proxy=" - + mLauncherProxy + " flags=" + flags); + + mLauncherProxy + " flags=" + flags + " displayId=" + displayId); } try { if (mLauncherProxy != null) { - mLauncherProxy.onSystemUiStateChanged(flags); + mLauncherProxy.onSystemUiStateChanged(flags, displayId); } } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to notify sysui state change", e); diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt index ec37b7592650..0a5efb7bb286 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt @@ -24,6 +24,7 @@ import android.os.PowerManager import android.os.UserManager import android.testing.TestableContext import android.testing.TestableLooper +import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.app.AssistUtils @@ -66,6 +67,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.any import org.mockito.Mockito.anyInt @@ -165,7 +167,8 @@ class LauncherProxyServiceTest : SysuiTestCase() { verify(launcherProxy) .onSystemUiStateChanged( - longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_AWAKE } + longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_AWAKE }, + eq(Display.DEFAULT_DISPLAY), ) } @@ -175,7 +178,8 @@ class LauncherProxyServiceTest : SysuiTestCase() { verify(launcherProxy) .onSystemUiStateChanged( - longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_WAKING } + longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_WAKING }, + eq(Display.DEFAULT_DISPLAY), ) } @@ -185,7 +189,8 @@ class LauncherProxyServiceTest : SysuiTestCase() { verify(launcherProxy) .onSystemUiStateChanged( - longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_ASLEEP } + longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_ASLEEP }, + eq(Display.DEFAULT_DISPLAY), ) } @@ -197,7 +202,8 @@ class LauncherProxyServiceTest : SysuiTestCase() { verify(launcherProxy) .onSystemUiStateChanged( - longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_GOING_TO_SLEEP } + longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_GOING_TO_SLEEP }, + eq(Display.DEFAULT_DISPLAY), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt index d6f0e06e104d..70b22d7f829d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt @@ -62,6 +62,14 @@ class FakeDisplayRepository @Inject constructor() : DisplayRepository { displays.forEach { addDisplay(it) } } + suspend operator fun plusAssign(display: Display) { + addDisplay(display) + } + + suspend operator fun minusAssign(displayId: Int) { + removeDisplay(displayId) + } + suspend fun addDisplay(display: Display) { flow.value += display displayIdFlow.value += display.displayId diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/PerDisplayStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/PerDisplayStoreKosmos.kt index e3797260ed6d..aa23aa30b7bc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/PerDisplayStoreKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/PerDisplayStoreKosmos.kt @@ -16,8 +16,10 @@ package com.android.systemui.display.data.repository +import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope import kotlinx.coroutines.CoroutineScope class FakePerDisplayStore( @@ -47,3 +49,30 @@ val Kosmos.fakePerDisplayStore by displayRepository = displayRepository, ) } + +class FakePerDisplayInstanceProviderWithTeardown : + PerDisplayInstanceProviderWithTeardown<TestPerDisplayInstance> { + val destroyed = mutableListOf<TestPerDisplayInstance>() + + override fun destroyInstance(instance: TestPerDisplayInstance) { + destroyed += instance + } + + override fun createInstance(displayId: Int): TestPerDisplayInstance? { + return TestPerDisplayInstance(displayId) + } +} + +val Kosmos.fakePerDisplayInstanceProviderWithTeardown by + Kosmos.Fixture { FakePerDisplayInstanceProviderWithTeardown() } + +val Kosmos.fakePerDisplayInstanceRepository by + Kosmos.Fixture { + PerDisplayInstanceRepositoryImpl( + debugName = "fakePerDisplayInstanceRepository", + instanceProvider = fakePerDisplayInstanceProviderWithTeardown, + testScope.backgroundScope, + displayRepository, + dumpManager, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/model/SceneContainerPluginKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/model/SceneContainerPluginKosmos.kt index d19dfe8d74fb..79506f9e75a5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/model/SceneContainerPluginKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/model/SceneContainerPluginKosmos.kt @@ -20,10 +20,12 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository val Kosmos.sceneContainerPlugin by Fixture { SceneContainerPlugin( sceneInteractor = { sceneInteractor }, occlusionInteractor = { sceneContainerOcclusionInteractor }, + shadeDisplaysRepository = { fakeShadeDisplaysRepository }, ) } 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 8aecf886b578..6272aafaa688 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 @@ -17,10 +17,11 @@ package com.android.systemui.model import android.view.Display +import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import org.mockito.Mockito.spy val Kosmos.sysUiState by Fixture { - spy(SysUiStateImpl(Display.DEFAULT_DISPLAY, sceneContainerPlugin)) + spy(SysUiStateImpl(Display.DEFAULT_DISPLAY, sceneContainerPlugin, dumpManager)) } |