Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit aeb60b9f authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[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
parent 5c539f7d
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -32,13 +32,14 @@ import javax.inject.Inject
 * 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) {
+21 −9
Original line number Diff line number Diff line
@@ -22,13 +22,13 @@ import android.app.StatusBarManager.WINDOW_STATE_HIDING
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 @@ import kotlinx.coroutines.flow.stateIn
 *
 * 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 @@ constructor(
        }
    }
}

@AssistedFactory
interface StatusBarWindowStatePerDisplayRepositoryFactory {
    fun create(@Assisted displayId: Int): StatusBarWindowStatePerDisplayRepositoryImpl
}
+60 −0
Original line number Diff line number Diff line
/*
 * 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) }
        }
    }
}
+6 −2
Original line number Diff line number Diff line
@@ -39,12 +39,16 @@ import org.mockito.kotlin.argumentCaptor

@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() {
+123 −0
Original line number Diff line number Diff line
/*
 * 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())
        }
}
Loading