diff options
3 files changed, 201 insertions, 3 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt index dfea78458b9d..197b0eea05cb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt @@ -23,15 +23,16 @@ import android.view.Display import android.view.Display.DEFAULT_DISPLAY import android.view.Display.TYPE_EXTERNAL import android.view.Display.TYPE_INTERNAL +import android.view.IWindowManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.FlowValue import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.statusbar.CommandQueue import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever import com.android.systemui.utils.os.FakeHandler import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.TestScope @@ -46,6 +47,7 @@ import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @@ -53,7 +55,11 @@ import org.mockito.kotlin.eq class DisplayRepositoryTest : SysuiTestCase() { private val displayManager = mock<DisplayManager>() + private val commandQueue = mock<CommandQueue>() + private val windowManager = mock<IWindowManager>() + private val displayListener = kotlinArgumentCaptor<DisplayManager.DisplayListener>() + private val commandQueueCallbacks = kotlinArgumentCaptor<CommandQueue.Callbacks>() private val connectedDisplayListener = kotlinArgumentCaptor<DisplayManager.DisplayListener>() private val testHandler = FakeHandler(Looper.getMainLooper()) @@ -67,6 +73,8 @@ class DisplayRepositoryTest : SysuiTestCase() { private val displayRepository: DisplayRepositoryImpl by lazy { DisplayRepositoryImpl( displayManager, + commandQueue, + windowManager, testHandler, TestScope(UnconfinedTestDispatcher()), UnconfinedTestDispatcher(), @@ -513,6 +521,115 @@ class DisplayRepositoryTest : SysuiTestCase() { assertThat(displayRepository.getDisplay(2)).isNull() } + @Test + fun displayIdsWithSystemDecorations_onStart_emitsDisplaysWithSystemDecorations() = + testScope.runTest { + setDisplays(0, 1, 2) + whenever(windowManager.shouldShowSystemDecors(0)).thenReturn(true) + whenever(windowManager.shouldShowSystemDecors(1)).thenReturn(false) + whenever(windowManager.shouldShowSystemDecors(2)).thenReturn(true) + + val displayIdsWithSystemDecorations by latestDisplayIdsWithSystemDecorationsValue() + + assertThat(displayIdsWithSystemDecorations).containsExactly(0, 2) + } + + @Test + fun displayIdsWithSystemDecorations_systemDecorationAdded_emitsIncludingNewDisplayIds() = + testScope.runTest { + setDisplays(0) + whenever(windowManager.shouldShowSystemDecors(0)).thenReturn(true) + val lastDisplayIdsWithSystemDecorations by latestDisplayIdsWithSystemDecorationsValue() + + sendOnDisplayAddSystemDecorations(2) + sendOnDisplayAddSystemDecorations(3) + + assertThat(lastDisplayIdsWithSystemDecorations).containsExactly(0, 2, 3) + } + + @Test + fun displayIdsWithSystemDecorations_systemDecorationAdded_emitsToNewSubscribers() = + testScope.runTest { + setDisplays(0) + whenever(windowManager.shouldShowSystemDecors(0)).thenReturn(true) + + val priorDisplayIdsWithSystemDecorations by latestDisplayIdsWithSystemDecorationsValue() + sendOnDisplayAddSystemDecorations(1) + assertThat(priorDisplayIdsWithSystemDecorations).containsExactly(0, 1) + + val lastDisplayIdsWithSystemDecorations by + collectLastValue(displayRepository.displayIdsWithSystemDecorations) + assertThat(lastDisplayIdsWithSystemDecorations).containsExactly(0, 1) + } + + @Test + fun displayIdsWithSystemDecorations_systemDecorationRemoved_doesNotEmitRemovedDisplayId() = + testScope.runTest { + val lastDisplayIdsWithSystemDecorations by latestDisplayIdsWithSystemDecorationsValue() + + sendOnDisplayAddSystemDecorations(1) + sendOnDisplayAddSystemDecorations(2) + sendOnDisplayRemoveSystemDecorations(2) + + assertThat(lastDisplayIdsWithSystemDecorations).containsExactly(1) + } + + @Test + fun displayIdsWithSystemDecorations_systemDecorationsRemoved_nonExistentDisplay_noEffect() = + testScope.runTest { + val lastDisplayIdsWithSystemDecorations by latestDisplayIdsWithSystemDecorationsValue() + + sendOnDisplayAddSystemDecorations(1) + sendOnDisplayRemoveSystemDecorations(2) + + assertThat(lastDisplayIdsWithSystemDecorations).containsExactly(1) + } + + @Test + fun displayIdsWithSystemDecorations_displayRemoved_doesNotEmitRemovedDisplayId() = + testScope.runTest { + val lastDisplayIdsWithSystemDecorations by latestDisplayIdsWithSystemDecorationsValue() + + sendOnDisplayAddSystemDecorations(1) + sendOnDisplayAddSystemDecorations(2) + sendOnDisplayRemoved(2) + + assertThat(lastDisplayIdsWithSystemDecorations).containsExactly(1) + } + + @Test + fun displayIdsWithSystemDecorations_displayRemoved_nonExistentDisplay_noEffect() = + testScope.runTest { + val lastDisplayIdsWithSystemDecorations by latestDisplayIdsWithSystemDecorationsValue() + + sendOnDisplayAddSystemDecorations(1) + sendOnDisplayRemoved(2) + + assertThat(lastDisplayIdsWithSystemDecorations).containsExactly(1) + } + + @Test + fun displayIdsWithSystemDecorations_onFlowCollection_commandQueueCallbackRegistered() = + testScope.runTest { + val lastDisplayIdsWithSystemDecorations by latestDisplayIdsWithSystemDecorationsValue() + + assertThat(lastDisplayIdsWithSystemDecorations).isEmpty() + + verify(commandQueue, times(1)).addCallback(any()) + } + + @Test + fun displayIdsWithSystemDecorations_afterFlowCollection_commandQueueCallbackUnregistered() { + testScope.runTest { + val lastDisplayIdsWithSystemDecorations by latestDisplayIdsWithSystemDecorationsValue() + + assertThat(lastDisplayIdsWithSystemDecorations).isEmpty() + + verify(commandQueue, times(1)).addCallback(any()) + } + verify(commandQueue, times(1)).removeCallback(any()) + } + private fun Iterable<Display>.ids(): List<Int> = map { it.displayId } private fun Iterable<Set<Display>>.toIdSets(): List<Set<Int>> = map { it.ids().toSet() } @@ -550,6 +667,14 @@ class DisplayRepositoryTest : SysuiTestCase() { return flowValue } + // Wrapper to capture the displayListener and commandQueueCallbacks. + private fun TestScope.latestDisplayIdsWithSystemDecorationsValue(): FlowValue<Set<Int>?> { + val flowValue = collectLastValue(displayRepository.displayIdsWithSystemDecorations) + captureAddedRemovedListener() + captureCommandQueueCallbacks() + return flowValue + } + private fun captureAddedRemovedListener() { verify(displayManager) .registerDisplayListener( @@ -563,6 +688,10 @@ class DisplayRepositoryTest : SysuiTestCase() { ) } + private fun captureCommandQueueCallbacks() { + verify(commandQueue).addCallback(commandQueueCallbacks.capture()) + } + private fun sendOnDisplayAdded(id: Int, displayType: Int) { val mockDisplay = display(id = id, type = displayType) whenever(displayManager.getDisplay(eq(id))).thenReturn(mockDisplay) @@ -592,6 +721,14 @@ class DisplayRepositoryTest : SysuiTestCase() { connectedDisplayListener.value.onDisplayChanged(id) } + private fun sendOnDisplayRemoveSystemDecorations(id: Int) { + commandQueueCallbacks.value.onDisplayRemoveSystemDecorations(id) + } + + private fun sendOnDisplayAddSystemDecorations(id: Int) { + commandQueueCallbacks.value.onDisplayAddSystemDecorations(id) + } + private fun setDisplays(displays: List<Display>) { whenever(displayManager.displays).thenReturn(displays.toTypedArray()) displays.forEach { display -> diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt index d4642006e68d..721d116004f3 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt @@ -26,14 +26,16 @@ import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_REMOVED import android.os.Handler import android.util.Log import android.view.Display +import android.view.IWindowManager import com.android.app.tracing.FlowTracing.traceEach import com.android.app.tracing.traceSection -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.DisplayEvent +import com.android.systemui.statusbar.CommandQueue import com.android.systemui.util.Compile import com.android.systemui.util.kotlin.pairwiseBy +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -43,6 +45,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter @@ -50,12 +53,13 @@ import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.stateIn -/** Provides a [Flow] of [Display] as returned by [DisplayManager]. */ +/** Repository for providing access to display related information and events. */ interface DisplayRepository { /** Display change event indicating a change to the given displayId has occurred. */ val displayChangeEvent: Flow<Int> @@ -66,6 +70,9 @@ interface DisplayRepository { /** Display removal event indicating a display has been removed. */ val displayRemovalEvent: Flow<Int> + /** A [StateFlow] that maintains a set of display IDs that should have system decorations. */ + val displayIdsWithSystemDecorations: StateFlow<Set<Int>> + /** * Provides the current set of displays. * @@ -124,6 +131,8 @@ class DisplayRepositoryImpl @Inject constructor( private val displayManager: DisplayManager, + private val commandQueue: CommandQueue, + private val windowManager: IWindowManager, @Background backgroundHandler: Handler, @Background bgApplicationScope: CoroutineScope, @Background backgroundCoroutineDispatcher: CoroutineDispatcher, @@ -426,6 +435,56 @@ constructor( .map { it.resultSet } } + private val decorationEvents: Flow<Event> = callbackFlow { + val callback = + object : CommandQueue.Callbacks { + override fun onDisplayAddSystemDecorations(displayId: Int) { + trySend(Event.Add(displayId)) + } + + override fun onDisplayRemoveSystemDecorations(displayId: Int) { + trySend(Event.Remove(displayId)) + } + } + commandQueue.addCallback(callback) + awaitClose { commandQueue.removeCallback(callback) } + } + + private val initialDisplayIdsWithDecorations: Set<Int> = + displayIds.value.filter { windowManager.shouldShowSystemDecors(it) }.toSet() + + /** + * A [StateFlow] that maintains a set of display IDs that should have system decorations. + * + * Updates to the set are triggered by: + * - Adding displays via [CommandQueue.Callbacks.onDisplayAddSystemDecorations]. + * - Removing displays via [CommandQueue.Callbacks.onDisplayRemoveSystemDecorations]. + * - Removing displays via [displayRemovalEvent] emissions. + * + * The set is initialized with displays that qualify for system decorations based on + * [WindowManager.shouldShowSystemDecors]. + */ + override val displayIdsWithSystemDecorations: StateFlow<Set<Int>> = + merge(decorationEvents, displayRemovalEvent.map { Event.Remove(it) }) + .scan(initialDisplayIdsWithDecorations) { displayIds: Set<Int>, event: Event -> + when (event) { + is Event.Add -> displayIds + event.displayId + is Event.Remove -> displayIds - event.displayId + } + } + .distinctUntilChanged() + .stateIn( + scope = bgApplicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = initialDisplayIdsWithDecorations, + ) + + private sealed class Event(val displayId: Int) { + class Add(displayId: Int) : Event(displayId) + + class Remove(displayId: Int) : Event(displayId) + } + private companion object { const val TAG = "DisplayRepository" val DEBUG = Log.isLoggable(TAG, Log.DEBUG) || Compile.IS_DEBUG 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 a64fc2413246..d6f0e06e104d 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 @@ -104,6 +104,8 @@ class FakeDisplayRepository @Inject constructor() : DisplayRepository { private val _displayChangeEvent = MutableSharedFlow<Int>(replay = 1) override val displayChangeEvent: Flow<Int> = _displayChangeEvent + override val displayIdsWithSystemDecorations: StateFlow<Set<Int>> = MutableStateFlow(emptySet()) + suspend fun emitDisplayChangeEvent(displayId: Int) = _displayChangeEvent.emit(displayId) fun setDefaultDisplayOff(defaultDisplayOff: Boolean) { |