[SB] Update StatusBarWindowStateRepository to be for any display.
StatusBarWindowStateRepository is no longer a singleton that injects the
display ID. Instead, it was renamed to
StatusBarWindowStatePerDisplayRepository and must be instantiated with a
specific display ID. StatusBarWindowStateRepositoryStore *is* a
singleton that lets other classes fetch a per-display repo instance for
either the default display or any display.
Bug: 364360986
Flag: EXEMPT code is unused for now
Test: atest StatusBarWindowStatePerDisplayRepositoryTest
StatusBarWindowStateRepositoryStoreTest
Change-Id: I0490cc7e005a8c7eca32fdecfe3ea579af188488
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
index fa9c6b2..1a1a592e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
@@ -32,13 +32,14 @@
* A centralized class maintaining the state of the status bar window.
*
* @deprecated use
- * [com.android.systemui.statusbar.window.data.repository.StatusBarWindowStateRepository] instead.
+ * [com.android.systemui.statusbar.window.data.repository.StatusBarWindowStateRepositoryStore.defaultDisplay]
+ * repo instead.
*
* Classes that want to get updates about the status bar window state should subscribe to this class
* via [addListener] and should NOT add their own callback on [CommandQueue].
*/
@SysUISingleton
-@Deprecated("Use StatusBarWindowRepository instead")
+@Deprecated("Use StatusBarWindowStateRepositoryStore.defaultDisplay instead")
class StatusBarWindowStateController
@Inject
constructor(@DisplayId private val thisDisplayId: Int, commandQueue: CommandQueue) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStatePerDisplayRepository.kt
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepository.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStatePerDisplayRepository.kt
index 678576d..bef8c84 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStatePerDisplayRepository.kt
@@ -22,13 +22,13 @@
import android.app.StatusBarManager.WINDOW_STATE_SHOWING
import android.app.StatusBarManager.WINDOW_STATUS_BAR
import android.app.StatusBarManager.WindowVisibleState
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.DisplayId
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.window.data.model.StatusBarWindowState
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
-import javax.inject.Inject
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.SharingStarted
@@ -40,16 +40,23 @@
*
* Classes that want to get updates about the status bar window state should subscribe to
* [windowState] and should NOT add their own callback on [CommandQueue].
+ *
+ * Each concrete implementation of this class will be for a specific display ID. Use
+ * [StatusBarWindowStateRepositoryStore] to fetch a concrete implementation for a certain display.
*/
-@SysUISingleton
-class StatusBarWindowStateRepository
-@Inject
+interface StatusBarWindowStatePerDisplayRepository {
+ /** Emits the current window state for the status bar on this specific display. */
+ val windowState: StateFlow<StatusBarWindowState>
+}
+
+class StatusBarWindowStatePerDisplayRepositoryImpl
+@AssistedInject
constructor(
+ @Assisted private val thisDisplayId: Int,
private val commandQueue: CommandQueue,
- @DisplayId private val thisDisplayId: Int,
@Application private val scope: CoroutineScope,
-) {
- val windowState: StateFlow<StatusBarWindowState> =
+) : StatusBarWindowStatePerDisplayRepository {
+ override val windowState: StateFlow<StatusBarWindowState> =
conflatedCallbackFlow {
val callback =
object : CommandQueue.Callbacks {
@@ -84,3 +91,8 @@
}
}
}
+
+@AssistedFactory
+interface StatusBarWindowStatePerDisplayRepositoryFactory {
+ fun create(@Assisted displayId: Int): StatusBarWindowStatePerDisplayRepositoryImpl
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStore.kt
new file mode 100644
index 0000000..0e33326
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStore.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.window.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.DisplayId
+import java.lang.ref.WeakReference
+import javax.inject.Inject
+
+/**
+ * Singleton class to create instances of [StatusBarWindowStatePerDisplayRepository] for a specific
+ * display.
+ *
+ * Repository instances for a specific display should be cached so that if multiple classes request
+ * a repository for the same display ID, we only create the repository once.
+ */
+interface StatusBarWindowStateRepositoryStore {
+ val defaultDisplay: StatusBarWindowStatePerDisplayRepository
+
+ fun forDisplay(displayId: Int): StatusBarWindowStatePerDisplayRepository
+}
+
+@SysUISingleton
+class StatusBarWindowStateRepositoryStoreImpl
+@Inject
+constructor(
+ @DisplayId private val displayId: Int,
+ private val factory: StatusBarWindowStatePerDisplayRepositoryFactory,
+) : StatusBarWindowStateRepositoryStore {
+ // Use WeakReferences to store the repositories so that the repositories are kept around so long
+ // as some UI holds a reference to them, but the repositories are cleaned up once no UI is using
+ // them anymore.
+ // See Change-Id Ib490062208506d646add2fe7e5e5d4df5fb3e66e for similar behavior in
+ // MobileConnectionsRepositoryImpl.
+ private val repositoryCache =
+ mutableMapOf<Int, WeakReference<StatusBarWindowStatePerDisplayRepository>>()
+
+ override val defaultDisplay = factory.create(displayId)
+
+ override fun forDisplay(displayId: Int): StatusBarWindowStatePerDisplayRepository {
+ synchronized(repositoryCache) {
+ return repositoryCache[displayId]?.get()
+ ?: factory.create(displayId).also { repositoryCache[displayId] = WeakReference(it) }
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStatePerDisplayRepositoryTest.kt
similarity index 94%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStatePerDisplayRepositoryTest.kt
index 38e04bb..0c27e58 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStatePerDisplayRepositoryTest.kt
@@ -39,12 +39,16 @@
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
-class StatusBarWindowStateRepositoryTest : SysuiTestCase() {
+class StatusBarWindowStatePerDisplayRepositoryTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val commandQueue = kosmos.commandQueue
private val underTest =
- StatusBarWindowStateRepository(commandQueue, DISPLAY_ID, testScope.backgroundScope)
+ StatusBarWindowStatePerDisplayRepositoryImpl(
+ DISPLAY_ID,
+ commandQueue,
+ testScope.backgroundScope,
+ )
private val callback: CommandQueue.Callbacks
get() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreTest.kt
new file mode 100644
index 0000000..b6a3f36
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreTest.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.window.data.repository
+
+import android.app.StatusBarManager.WINDOW_STATE_HIDDEN
+import android.app.StatusBarManager.WINDOW_STATE_SHOWING
+import android.app.StatusBarManager.WINDOW_STATUS_BAR
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.settings.displayTracker
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.commandQueue
+import com.android.systemui.statusbar.window.data.model.StatusBarWindowState
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.reset
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class StatusBarWindowStateRepositoryStoreTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val commandQueue = kosmos.commandQueue
+ private val defaultDisplayId = kosmos.displayTracker.defaultDisplayId
+
+ private val underTest = kosmos.statusBarWindowStateRepositoryStore
+
+ @Test
+ fun defaultDisplay_repoIsForDefaultDisplay() =
+ testScope.runTest {
+ val repo = underTest.defaultDisplay
+ val latest by collectLastValue(repo.windowState)
+
+ testScope.runCurrent()
+ val callbackCaptor = argumentCaptor<CommandQueue.Callbacks>()
+ verify(commandQueue).addCallback(callbackCaptor.capture())
+ val callback = callbackCaptor.firstValue
+
+ // WHEN a default display callback is sent
+ callback.setWindowState(defaultDisplayId, WINDOW_STATUS_BAR, WINDOW_STATE_SHOWING)
+
+ // THEN its value is used
+ assertThat(latest).isEqualTo(StatusBarWindowState.Showing)
+
+ // WHEN a non-default display callback is sent
+ callback.setWindowState(defaultDisplayId + 1, WINDOW_STATUS_BAR, WINDOW_STATE_HIDDEN)
+
+ // THEN its value is NOT used
+ assertThat(latest).isEqualTo(StatusBarWindowState.Showing)
+ }
+
+ @Test
+ fun forDisplay_repoIsForSpecifiedDisplay() =
+ testScope.runTest {
+ // The repository store will always create a repository for the default display, which
+ // will always add a callback to commandQueue. Ignore that callback here.
+ testScope.runCurrent()
+ reset(commandQueue)
+
+ val displayId = defaultDisplayId + 15
+ val repo = underTest.forDisplay(displayId)
+ val latest by collectLastValue(repo.windowState)
+
+ testScope.runCurrent()
+ val callbackCaptor = argumentCaptor<CommandQueue.Callbacks>()
+ verify(commandQueue).addCallback(callbackCaptor.capture())
+ val callback = callbackCaptor.firstValue
+
+ // WHEN a default display callback is sent
+ callback.setWindowState(defaultDisplayId, WINDOW_STATUS_BAR, WINDOW_STATE_SHOWING)
+
+ // THEN its value is NOT used
+ assertThat(latest).isEqualTo(StatusBarWindowState.Hidden)
+
+ // WHEN a callback for this display is sent
+ callback.setWindowState(displayId, WINDOW_STATUS_BAR, WINDOW_STATE_SHOWING)
+
+ // THEN its value is used
+ assertThat(latest).isEqualTo(StatusBarWindowState.Showing)
+ }
+
+ @Test
+ fun forDisplay_reusesRepoForSameDisplayId() =
+ testScope.runTest {
+ // The repository store will always create a repository for the default display, which
+ // will always add a callback to commandQueue. Ignore that callback here.
+ testScope.runCurrent()
+ reset(commandQueue)
+
+ val displayId = defaultDisplayId + 15
+ val firstRepo = underTest.forDisplay(displayId)
+ testScope.runCurrent()
+ val secondRepo = underTest.forDisplay(displayId)
+ testScope.runCurrent()
+
+ assertThat(firstRepo).isEqualTo(secondRepo)
+ // Verify that we only added 1 CommandQueue.Callback because we only created 1 repo.
+ val callbackCaptor = argumentCaptor<CommandQueue.Callbacks>()
+ verify(commandQueue).addCallback(callbackCaptor.capture())
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt
new file mode 100644
index 0000000..e2b7f5f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.window.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.settings.displayTracker
+import com.android.systemui.statusbar.commandQueue
+
+class KosmosStatusBarWindowStatePerDisplayRepositoryFactory(private val kosmos: Kosmos) :
+ StatusBarWindowStatePerDisplayRepositoryFactory {
+ override fun create(displayId: Int): StatusBarWindowStatePerDisplayRepositoryImpl {
+ return StatusBarWindowStatePerDisplayRepositoryImpl(
+ displayId,
+ kosmos.commandQueue,
+ kosmos.applicationCoroutineScope,
+ )
+ }
+}
+
+val Kosmos.statusBarWindowStateRepositoryStore by
+ Kosmos.Fixture {
+ StatusBarWindowStateRepositoryStoreImpl(
+ displayId = displayTracker.defaultDisplayId,
+ factory = KosmosStatusBarWindowStatePerDisplayRepositoryFactory(this),
+ )
+ }