[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),
+        )
+    }