Loading packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt +48 −21 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.display.data.repository import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED import android.hardware.display.DisplayManager.DisplayListener import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_ADDED import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED Loading Loading @@ -95,7 +96,8 @@ constructor( @Background backgroundCoroutineDispatcher: CoroutineDispatcher ) : DisplayRepository { // Displays are enabled only after receiving them in [onDisplayAdded] private val allDisplayEvents: Flow<DisplayEvent> = conflatedCallbackFlow { private val allDisplayEvents: Flow<DisplayEvent> = conflatedCallbackFlow { val callback = object : DisplayListener { override fun onDisplayAdded(displayId: Int) { Loading @@ -110,13 +112,20 @@ constructor( trySend(DisplayEvent.Changed(displayId)) } } // Triggers an initial event when subscribed. This is needed to avoid getDisplays to // be called when this class is constructed, but only when someone subscribes to // this flow. trySend(DisplayEvent.Changed(Display.DEFAULT_DISPLAY)) displayManager.registerDisplayListener( callback, backgroundHandler, EVENT_FLAG_DISPLAY_ADDED or EVENT_FLAG_DISPLAY_CHANGED or EVENT_FLAG_DISPLAY_REMOVED, EVENT_FLAG_DISPLAY_ADDED or EVENT_FLAG_DISPLAY_CHANGED or EVENT_FLAG_DISPLAY_REMOVED, ) awaitClose { displayManager.unregisterDisplayListener(callback) } } .flowOn(backgroundCoroutineDispatcher) override val displayChangeEvent: Flow<Int> = allDisplayEvents.filter { it is DisplayEvent.Changed }.map { it.displayId } Loading @@ -128,7 +137,9 @@ constructor( .stateIn( applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = getDisplays() // To avoid getting displays on this object construction, they are get after the // first event. allDisplayEvents emits a changed event when we subscribe to it. initialValue = emptySet() ) private fun getDisplays(): Set<Display> = Loading @@ -146,12 +157,23 @@ constructor( private val ignoredDisplayIds = MutableStateFlow<Set<Int>>(emptySet()) private fun getInitialConnectedDisplays(): Set<Int> = displayManager .getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) .map { it.displayId } .toSet() .also { if (DEBUG) { Log.d(TAG, "getInitialConnectedDisplays: $it") } } /* keeps connected displays until they are disconnected. */ private val connectedDisplayIds: StateFlow<Set<Int>> = conflatedCallbackFlow { val connectedIds = getInitialConnectedDisplays().toMutableSet() val callback = object : DisplayConnectionListener { private val connectedIds = mutableSetOf<Int>() override fun onDisplayConnected(id: Int) { if (DEBUG) { Log.d(TAG, "display with id=$id connected.") Loading @@ -170,6 +192,7 @@ constructor( trySend(connectedIds.toSet()) } } trySend(connectedIds.toSet()) displayManager.registerDisplayListener( callback, backgroundHandler, Loading @@ -183,6 +206,10 @@ constructor( .stateIn( applicationScope, started = SharingStarted.WhileSubscribed(), // The initial value is set to empty, but connected displays are gathered as soon as // the flow starts being collected. This is to ensure the call to get displays (an // IPC) happens in the background instead of when this object // is instantiated. initialValue = emptySet() ) Loading packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt +37 −8 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.os.Looper import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.Display import android.view.Display.TYPE_EXTERNAL import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.FlowValue Loading Loading @@ -62,6 +63,7 @@ class DisplayRepositoryTest : SysuiTestCase() { @Before fun setup() { setDisplays(emptyList()) setAllDisplaysIncludingDisabled() displayRepository = DisplayRepositoryImpl( displayManager, Loading @@ -70,6 +72,7 @@ class DisplayRepositoryTest : SysuiTestCase() { UnconfinedTestDispatcher() ) verify(displayManager, never()).registerDisplayListener(any(), any()) verify(displayManager, never()).getDisplays(any()) } @Test Loading Loading @@ -350,6 +353,22 @@ class DisplayRepositoryTest : SysuiTestCase() { assertThat(pendingDisplay).isNotNull() } @Test fun initialState_onePendingDisplayOnBoot_notNull() = testScope.runTest { // 1 is not enabled, but just connected. It should be seen as pending setAllDisplaysIncludingDisabled(0, 1) setDisplays(0) // 0 is enabled. verify(displayManager, never()).getDisplays(any()) val pendingDisplay by collectLastValue(displayRepository.pendingDisplay) verify(displayManager).getDisplays(any()) assertThat(pendingDisplay).isNotNull() assertThat(pendingDisplay!!.id).isEqualTo(1) } @Test fun onPendingDisplay_internalDisplay_ignored() = testScope.runTest { Loading @@ -365,7 +384,7 @@ class DisplayRepositoryTest : SysuiTestCase() { testScope.runTest { val pendingDisplay by lastPendingDisplay() sendOnDisplayConnected(1, Display.TYPE_EXTERNAL) sendOnDisplayConnected(1, TYPE_EXTERNAL) sendOnDisplayConnected(2, Display.TYPE_INTERNAL) assertThat(pendingDisplay!!.id).isEqualTo(1) Loading Loading @@ -416,7 +435,7 @@ class DisplayRepositoryTest : SysuiTestCase() { whenever(displayManager.getDisplay(eq(id))).thenReturn(null) } private fun sendOnDisplayConnected(id: Int, displayType: Int = Display.TYPE_EXTERNAL) { private fun sendOnDisplayConnected(id: Int, displayType: Int = TYPE_EXTERNAL) { val mockDisplay = display(id = id, type = displayType) whenever(displayManager.getDisplay(eq(id))).thenReturn(mockDisplay) connectedDisplayListener.value.onDisplayConnected(id) Loading @@ -424,15 +443,25 @@ class DisplayRepositoryTest : SysuiTestCase() { private fun setDisplays(displays: List<Display>) { whenever(displayManager.displays).thenReturn(displays.toTypedArray()) displays.forEach { display -> whenever(displayManager.getDisplay(eq(display.displayId))).thenReturn(display) } private fun setDisplays(vararg ids: Int) { setDisplays(ids.map { display(it) }) } private fun display(id: Int): Display { return mock<Display>().also { mockDisplay -> whenever(mockDisplay.displayId).thenReturn(id) private fun setAllDisplaysIncludingDisabled(vararg ids: Int) { val displays = ids.map { display(type = TYPE_EXTERNAL, id = it) }.toTypedArray() whenever( displayManager.getDisplays( eq(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) ) ) .thenReturn(displays) displays.forEach { display -> whenever(displayManager.getDisplay(eq(display.displayId))).thenReturn(display) } } private fun setDisplays(vararg ids: Int) { setDisplays(ids.map { display(type = TYPE_EXTERNAL, id = it) }) } } Loading
packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt +48 −21 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.display.data.repository import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED import android.hardware.display.DisplayManager.DisplayListener import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_ADDED import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED Loading Loading @@ -95,7 +96,8 @@ constructor( @Background backgroundCoroutineDispatcher: CoroutineDispatcher ) : DisplayRepository { // Displays are enabled only after receiving them in [onDisplayAdded] private val allDisplayEvents: Flow<DisplayEvent> = conflatedCallbackFlow { private val allDisplayEvents: Flow<DisplayEvent> = conflatedCallbackFlow { val callback = object : DisplayListener { override fun onDisplayAdded(displayId: Int) { Loading @@ -110,13 +112,20 @@ constructor( trySend(DisplayEvent.Changed(displayId)) } } // Triggers an initial event when subscribed. This is needed to avoid getDisplays to // be called when this class is constructed, but only when someone subscribes to // this flow. trySend(DisplayEvent.Changed(Display.DEFAULT_DISPLAY)) displayManager.registerDisplayListener( callback, backgroundHandler, EVENT_FLAG_DISPLAY_ADDED or EVENT_FLAG_DISPLAY_CHANGED or EVENT_FLAG_DISPLAY_REMOVED, EVENT_FLAG_DISPLAY_ADDED or EVENT_FLAG_DISPLAY_CHANGED or EVENT_FLAG_DISPLAY_REMOVED, ) awaitClose { displayManager.unregisterDisplayListener(callback) } } .flowOn(backgroundCoroutineDispatcher) override val displayChangeEvent: Flow<Int> = allDisplayEvents.filter { it is DisplayEvent.Changed }.map { it.displayId } Loading @@ -128,7 +137,9 @@ constructor( .stateIn( applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = getDisplays() // To avoid getting displays on this object construction, they are get after the // first event. allDisplayEvents emits a changed event when we subscribe to it. initialValue = emptySet() ) private fun getDisplays(): Set<Display> = Loading @@ -146,12 +157,23 @@ constructor( private val ignoredDisplayIds = MutableStateFlow<Set<Int>>(emptySet()) private fun getInitialConnectedDisplays(): Set<Int> = displayManager .getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) .map { it.displayId } .toSet() .also { if (DEBUG) { Log.d(TAG, "getInitialConnectedDisplays: $it") } } /* keeps connected displays until they are disconnected. */ private val connectedDisplayIds: StateFlow<Set<Int>> = conflatedCallbackFlow { val connectedIds = getInitialConnectedDisplays().toMutableSet() val callback = object : DisplayConnectionListener { private val connectedIds = mutableSetOf<Int>() override fun onDisplayConnected(id: Int) { if (DEBUG) { Log.d(TAG, "display with id=$id connected.") Loading @@ -170,6 +192,7 @@ constructor( trySend(connectedIds.toSet()) } } trySend(connectedIds.toSet()) displayManager.registerDisplayListener( callback, backgroundHandler, Loading @@ -183,6 +206,10 @@ constructor( .stateIn( applicationScope, started = SharingStarted.WhileSubscribed(), // The initial value is set to empty, but connected displays are gathered as soon as // the flow starts being collected. This is to ensure the call to get displays (an // IPC) happens in the background instead of when this object // is instantiated. initialValue = emptySet() ) Loading
packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt +37 −8 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.os.Looper import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.Display import android.view.Display.TYPE_EXTERNAL import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.FlowValue Loading Loading @@ -62,6 +63,7 @@ class DisplayRepositoryTest : SysuiTestCase() { @Before fun setup() { setDisplays(emptyList()) setAllDisplaysIncludingDisabled() displayRepository = DisplayRepositoryImpl( displayManager, Loading @@ -70,6 +72,7 @@ class DisplayRepositoryTest : SysuiTestCase() { UnconfinedTestDispatcher() ) verify(displayManager, never()).registerDisplayListener(any(), any()) verify(displayManager, never()).getDisplays(any()) } @Test Loading Loading @@ -350,6 +353,22 @@ class DisplayRepositoryTest : SysuiTestCase() { assertThat(pendingDisplay).isNotNull() } @Test fun initialState_onePendingDisplayOnBoot_notNull() = testScope.runTest { // 1 is not enabled, but just connected. It should be seen as pending setAllDisplaysIncludingDisabled(0, 1) setDisplays(0) // 0 is enabled. verify(displayManager, never()).getDisplays(any()) val pendingDisplay by collectLastValue(displayRepository.pendingDisplay) verify(displayManager).getDisplays(any()) assertThat(pendingDisplay).isNotNull() assertThat(pendingDisplay!!.id).isEqualTo(1) } @Test fun onPendingDisplay_internalDisplay_ignored() = testScope.runTest { Loading @@ -365,7 +384,7 @@ class DisplayRepositoryTest : SysuiTestCase() { testScope.runTest { val pendingDisplay by lastPendingDisplay() sendOnDisplayConnected(1, Display.TYPE_EXTERNAL) sendOnDisplayConnected(1, TYPE_EXTERNAL) sendOnDisplayConnected(2, Display.TYPE_INTERNAL) assertThat(pendingDisplay!!.id).isEqualTo(1) Loading Loading @@ -416,7 +435,7 @@ class DisplayRepositoryTest : SysuiTestCase() { whenever(displayManager.getDisplay(eq(id))).thenReturn(null) } private fun sendOnDisplayConnected(id: Int, displayType: Int = Display.TYPE_EXTERNAL) { private fun sendOnDisplayConnected(id: Int, displayType: Int = TYPE_EXTERNAL) { val mockDisplay = display(id = id, type = displayType) whenever(displayManager.getDisplay(eq(id))).thenReturn(mockDisplay) connectedDisplayListener.value.onDisplayConnected(id) Loading @@ -424,15 +443,25 @@ class DisplayRepositoryTest : SysuiTestCase() { private fun setDisplays(displays: List<Display>) { whenever(displayManager.displays).thenReturn(displays.toTypedArray()) displays.forEach { display -> whenever(displayManager.getDisplay(eq(display.displayId))).thenReturn(display) } private fun setDisplays(vararg ids: Int) { setDisplays(ids.map { display(it) }) } private fun display(id: Int): Display { return mock<Display>().also { mockDisplay -> whenever(mockDisplay.displayId).thenReturn(id) private fun setAllDisplaysIncludingDisabled(vararg ids: Int) { val displays = ids.map { display(type = TYPE_EXTERNAL, id = it) }.toTypedArray() whenever( displayManager.getDisplays( eq(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) ) ) .thenReturn(displays) displays.forEach { display -> whenever(displayManager.getDisplay(eq(display.displayId))).thenReturn(display) } } private fun setDisplays(vararg ids: Int) { setDisplays(ids.map { display(type = TYPE_EXTERNAL, id = it) }) } }