diff options
| author | 2023-11-30 16:09:19 +0000 | |
|---|---|---|
| committer | 2023-11-30 18:09:28 +0000 | |
| commit | adc0250c93aa508fc6f41b75e767e764169ef39d (patch) | |
| tree | 8d685386e938dd1cc68c3b3212fa094f8850d95b | |
| parent | 4aa3999f54bc1348952ccc45a9828e44d99e89b7 (diff) | |
Update mirroring dialog to show concurrent displays warning
This introduces config_concurrentDisplayDeviceStates array, that is device specific and is supposed to contain all device states that represent "concurrent displays".
This also creates a DeviceStateRepository that allows interactors to use the state provided by DeviceStateManager easily.
Several other places in sysui are doing something similar (e.g. DevicePostureController and DisplayStateRepository), but with a slighly different logic that doesn't suit this use case (DPC is using an androidx related res that doesn't contain the concurrent state and having some logic to use the base state in certain cases, and DSR is only listening for specific states). Eventually, those other classes should be refactored to use DeviceStateRepository under the wood.
This is only enabled for devices overriding the config_concurrentDisplayDeviceStates array in an overlay.
Flag: ACONFIG enable_dual_display_blocking DISABLED
Test: ConnectedDisplayInteractorTest, DeviceStateRepositoryTest, MirroringConfirmationDialogScerenshotTest
Bug: 296211844
Change-Id: I68c0b1489019471aec0a72fda70f57a7bc1ed29d
16 files changed, 399 insertions, 7 deletions
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index ba1f3924bff3..1229453e5736 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -693,6 +693,16 @@ --> </integer-array> + <!-- The device states (supplied by DeviceStateManager) that should be treated as concurrent + display state. Default is empty. --> + <integer-array name="config_concurrentDisplayDeviceStates"> + <!-- Example: + <item>0</item> + <item>1</item> + <item>2</item> + --> + </integer-array> + <!-- Indicates whether the window manager reacts to half-fold device states by overriding rotation. --> <bool name="config_windowManagerHalfFoldAutoRotateOverride">false</bool> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 7787c5d394e4..93aacdff57df 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4142,6 +4142,7 @@ <java-symbol type="array" name="config_foldedDeviceStates" /> <java-symbol type="array" name="config_halfFoldedDeviceStates" /> <java-symbol type="array" name="config_rearDisplayDeviceStates" /> + <java-symbol type="array" name="config_concurrentDisplayDeviceStates" /> <java-symbol type="bool" name="config_windowManagerHalfFoldAutoRotateOverride" /> <java-symbol type="bool" name="config_windowManagerPauseRotationWhenUnfolding" /> <java-symbol type="integer" name="config_pauseRotationWhenUnfolding_hingeEventTimeout" /> diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 7cf562f48ff3..c2c5e001a5df 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -190,6 +190,7 @@ android_library { "androidx.room_room-runtime", "androidx.room_room-ktx", "com.google.android.material_material", + "device_state_flags_lib", "kotlinx_coroutines_android", "kotlinx_coroutines", "iconloader_base", @@ -302,6 +303,7 @@ android_library { "androidx.exifinterface_exifinterface", "androidx.room_room-runtime", "androidx.room_room-ktx", + "device_state_flags_lib", "kotlinx-coroutines-android", "kotlinx-coroutines-core", "kotlinx_coroutines_test", diff --git a/packages/SystemUI/res/layout/connected_display_dialog.xml b/packages/SystemUI/res/layout/connected_display_dialog.xml index 3f65aa7984b5..8d7f7ebd82dc 100644 --- a/packages/SystemUI/res/layout/connected_display_dialog.xml +++ b/packages/SystemUI/res/layout/connected_display_dialog.xml @@ -45,6 +45,15 @@ android:text="@string/connected_display_dialog_start_mirroring" android:textAppearance="@style/TextAppearance.Dialog.Title" /> + <TextView + android:id="@+id/dual_display_warning" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:visibility="gone" + android:text="@string/connected_display_dialog_dual_display_stop_warning" + android:textAppearance="@style/TextAppearance.Dialog.Body" /> + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index f49d2a19bcd4..7ca0b6ee8d9f 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3245,6 +3245,8 @@ <!--- Title of the dialog appearing when an external display is connected, asking whether to start mirroring [CHAR LIMIT=NONE]--> <string name="connected_display_dialog_start_mirroring">Mirror to external display?</string> + <!--- Body of the mirroring dialog, shown when dual display is enabled. This signals that enabling mirroring will stop concurrent displays on a foldable device. [CHAR LIMIT=NONE]--> + <string name="connected_display_dialog_dual_display_stop_warning">Any dual screen activity currently running will be stopped</string> <!--- Label of the "enable display" button of the dialog appearing when an external display is connected [CHAR LIMIT=NONE]--> <string name="mirror_display">Mirror display</string> <!--- Label of the dismiss button of the dialog appearing when an external display is connected [CHAR LIMIT=NONE]--> diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt index ff23837703b5..b0143f5cdc4a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt @@ -60,6 +60,8 @@ interface DisplayStateRepository { val currentRotation: StateFlow<DisplayRotation> } +// TODO(b/296211844): This class could directly use DeviceStateRepository and DisplayRepository +// instead. @SysUISingleton class DisplayStateRepositoryImpl @Inject diff --git a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt index 65cd84bc4da1..373279cec5d1 100644 --- a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt +++ b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt @@ -16,6 +16,8 @@ package com.android.systemui.display +import com.android.systemui.display.data.repository.DeviceStateRepository +import com.android.systemui.display.data.repository.DeviceStateRepositoryImpl import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.DisplayRepositoryImpl import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor @@ -32,4 +34,9 @@ interface DisplayModule { ): ConnectedDisplayInteractor @Binds fun bindsDisplayRepository(displayRepository: DisplayRepositoryImpl): DisplayRepository + + @Binds + fun bindsDeviceStateRepository( + deviceStateRepository: DeviceStateRepositoryImpl + ): DeviceStateRepository } diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt new file mode 100644 index 000000000000..83337f760c33 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 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.display.data.repository + +import android.content.Context +import android.hardware.devicestate.DeviceStateManager +import com.android.internal.R +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn + +interface DeviceStateRepository { + val state: StateFlow<DeviceState> + + enum class DeviceState { + /** Device state in [R.array.config_foldedDeviceStates] */ + FOLDED, + /** Device state in [R.array.config_halfFoldedDeviceStates] */ + HALF_FOLDED, + /** Device state in [R.array.config_openDeviceStates] */ + UNFOLDED, + /** Device state in [R.array.config_rearDisplayDeviceStates] */ + REAR_DISPLAY, + /** Device state in [R.array.config_concurrentDisplayDeviceStates] */ + CONCURRENT_DISPLAY, + /** Device state in none of the other arrays. */ + UNKNOWN, + } +} + +class DeviceStateRepositoryImpl +@Inject +constructor( + context: Context, + deviceStateManager: DeviceStateManager, + @Background bgScope: CoroutineScope, + @Background executor: Executor +) : DeviceStateRepository { + + override val state: StateFlow<DeviceState> = + conflatedCallbackFlow { + val callback = + DeviceStateManager.DeviceStateCallback { state -> + trySend(deviceStateToPosture(state)) + } + deviceStateManager.registerCallback(executor, callback) + awaitClose { deviceStateManager.unregisterCallback(callback) } + } + .stateIn(bgScope, started = SharingStarted.WhileSubscribed(), DeviceState.UNKNOWN) + + private fun deviceStateToPosture(deviceStateId: Int): DeviceState { + return deviceStateMap.firstOrNull { (ids, _) -> deviceStateId in ids }?.deviceState + ?: DeviceState.UNKNOWN + } + + private val deviceStateMap = + listOf( + R.array.config_foldedDeviceStates to DeviceState.FOLDED, + R.array.config_halfFoldedDeviceStates to DeviceState.HALF_FOLDED, + R.array.config_openDeviceStates to DeviceState.UNFOLDED, + R.array.config_rearDisplayDeviceStates to DeviceState.REAR_DISPLAY, + R.array.config_concurrentDisplayDeviceStates to DeviceState.CONCURRENT_DISPLAY, + ) + .map { IdsPerDeviceState(context.resources.getIntArray(it.first).toSet(), it.second) } + + private data class IdsPerDeviceState(val ids: Set<Int>, val deviceState: DeviceState) +} diff --git a/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt b/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt index 20a9e5d572c9..73b7a8ac7bd3 100644 --- a/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt @@ -21,6 +21,7 @@ import android.companion.virtual.flags.Flags import android.view.Display import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.display.data.repository.DeviceStateRepository import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State @@ -55,6 +56,9 @@ interface ConnectedDisplayInteractor { /** Pending display that can be enabled to be used by the system. */ val pendingDisplay: Flow<PendingDisplay?> + /** Pending display that can be enabled to be used by the system. */ + val concurrentDisplaysInProgress: Flow<Boolean> + /** Possible connected display state. */ enum class State { DISCONNECTED, @@ -84,6 +88,7 @@ constructor( private val virtualDeviceManager: VirtualDeviceManager, keyguardRepository: KeyguardRepository, displayRepository: DisplayRepository, + deviceStateRepository: DeviceStateRepository, @Background backgroundCoroutineDispatcher: CoroutineDispatcher, ) : ConnectedDisplayInteractor { @@ -128,9 +133,16 @@ constructor( } } + override val concurrentDisplaysInProgress: Flow<Boolean> = + deviceStateRepository.state + .map { it == DeviceStateRepository.DeviceState.CONCURRENT_DISPLAY } + .distinctUntilChanged() + .flowOn(backgroundCoroutineDispatcher) + private fun DisplayRepository.PendingDisplay.toInteractorPendingDisplay(): PendingDisplay = object : PendingDisplay { override suspend fun enable() = this@toInteractorPendingDisplay.enable() + override suspend fun ignore() = this@toInteractorPendingDisplay.ignore() } diff --git a/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt b/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt index d500d1c2d238..c0a873ac9a65 100644 --- a/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt @@ -37,11 +37,13 @@ class MirroringConfirmationDialog( private val onCancelMirroring: View.OnClickListener, private val navbarBottomInsetsProvider: () -> Int, configurationController: ConfigurationController? = null, + private val showConcurrentDisplayInfo: Boolean = false, theme: Int = R.style.Theme_SystemUI_Dialog, ) : SystemUIBottomSheetDialog(context, configurationController, theme) { private lateinit var mirrorButton: TextView private lateinit var dismissButton: TextView + private lateinit var dualDisplayWarning: TextView private var enabledPressed = false override fun onCreate(savedInstanceState: Bundle?) { @@ -56,6 +58,11 @@ class MirroringConfirmationDialog( dismissButton = requireViewById<TextView>(R.id.cancel).apply { setOnClickListener(onCancelMirroring) } + dualDisplayWarning = + requireViewById<TextView>(R.id.dual_display_warning).apply { + visibility = if (showConcurrentDisplayInfo) View.VISIBLE else View.GONE + } + setOnDismissListener { if (!enabledPressed) { onCancelMirroring.onClick(null) diff --git a/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt index 19b4d2220558..10aa70391f01 100644 --- a/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.display.ui.viewmodel import android.app.Dialog import android.content.Context +import com.android.server.policy.feature.flags.Flags import com.android.systemui.biometrics.Utils import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -28,8 +29,9 @@ import com.android.systemui.statusbar.policy.ConfigurationController import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch /** @@ -44,25 +46,33 @@ constructor( private val connectedDisplayInteractor: ConnectedDisplayInteractor, @Application private val scope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher, - private val configurationController: ConfigurationController + private val configurationController: ConfigurationController, ) { private var dialog: Dialog? = null /** Starts listening for pending displays. */ fun init() { - connectedDisplayInteractor.pendingDisplay - .onEach { pendingDisplay -> + val pendingDisplayFlow = connectedDisplayInteractor.pendingDisplay + val concurrentDisplaysInProgessFlow = + if (Flags.enableDualDisplayBlocking()) { + connectedDisplayInteractor.concurrentDisplaysInProgress + } else { + flow { emit(false) } + } + pendingDisplayFlow + .combine(concurrentDisplaysInProgessFlow) { pendingDisplay, concurrentDisplaysInProgress + -> if (pendingDisplay == null) { hideDialog() } else { - showDialog(pendingDisplay) + showDialog(pendingDisplay, concurrentDisplaysInProgress) } } .launchIn(scope) } - private fun showDialog(pendingDisplay: PendingDisplay) { + private fun showDialog(pendingDisplay: PendingDisplay, concurrentDisplaysInProgess: Boolean) { hideDialog() dialog = MirroringConfirmationDialog( @@ -77,6 +87,7 @@ constructor( }, navbarBottomInsetsProvider = { Utils.getNavbarInsets(context).bottom }, configurationController, + showConcurrentDisplayInfo = concurrentDisplaysInProgess ) .apply { show() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DeviceStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DeviceStateRepositoryTest.kt new file mode 100644 index 000000000000..21b8aca363ca --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DeviceStateRepositoryTest.kt @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2023 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.display.data.repository + +import android.hardware.devicestate.DeviceStateManager +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.FlowValue +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.runner.RunWith +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class DeviceStateRepositoryTest : SysuiTestCase() { + + private val deviceStateManager = mock<DeviceStateManager>() + private val deviceStateManagerListener = + kotlinArgumentCaptor<DeviceStateManager.DeviceStateCallback>() + + private val testScope = TestScope(UnconfinedTestDispatcher()) + private val fakeExecutor = FakeExecutor(FakeSystemClock()) + + private lateinit var deviceStateRepository: DeviceStateRepositoryImpl + + @Before + fun setup() { + mContext.orCreateTestableResources.apply { + addOverride(R.array.config_foldedDeviceStates, listOf(TEST_FOLDED).toIntArray()) + addOverride(R.array.config_halfFoldedDeviceStates, TEST_HALF_FOLDED.toIntArray()) + addOverride(R.array.config_openDeviceStates, TEST_UNFOLDED.toIntArray()) + addOverride(R.array.config_rearDisplayDeviceStates, TEST_REAR_DISPLAY.toIntArray()) + addOverride( + R.array.config_concurrentDisplayDeviceStates, + TEST_CONCURRENT_DISPLAY.toIntArray() + ) + } + deviceStateRepository = + DeviceStateRepositoryImpl( + mContext, + deviceStateManager, + TestScope(UnconfinedTestDispatcher()), + fakeExecutor + ) + + // It should register only after there are clients collecting the flow + verify(deviceStateManager, never()).registerCallback(any(), any()) + } + + @Test + fun folded_receivesFoldedState() = + testScope.runTest { + val state = displayState() + + deviceStateManagerListener.value.onStateChanged(TEST_FOLDED) + + assertThat(state()).isEqualTo(DeviceState.FOLDED) + } + + @Test + fun halfFolded_receivesHalfFoldedState() = + testScope.runTest { + val state = displayState() + + deviceStateManagerListener.value.onStateChanged(TEST_HALF_FOLDED) + + assertThat(state()).isEqualTo(DeviceState.HALF_FOLDED) + } + + @Test + fun unfolded_receivesUnfoldedState() = + testScope.runTest { + val state = displayState() + + deviceStateManagerListener.value.onStateChanged(TEST_UNFOLDED) + + assertThat(state()).isEqualTo(DeviceState.UNFOLDED) + } + + @Test + fun rearDisplay_receivesRearDisplayState() = + testScope.runTest { + val state = displayState() + + deviceStateManagerListener.value.onStateChanged(TEST_REAR_DISPLAY) + + assertThat(state()).isEqualTo(DeviceState.REAR_DISPLAY) + } + + @Test + fun concurrentDisplay_receivesConcurrentDisplayState() = + testScope.runTest { + val state = displayState() + + deviceStateManagerListener.value.onStateChanged(TEST_CONCURRENT_DISPLAY) + + assertThat(state()).isEqualTo(DeviceState.CONCURRENT_DISPLAY) + } + + @Test + fun unknownState_receivesUnknownState() = + testScope.runTest { + val state = displayState() + + deviceStateManagerListener.value.onStateChanged(123456) + + assertThat(state()).isEqualTo(DeviceState.UNKNOWN) + } + + private fun TestScope.displayState(): FlowValue<DeviceState?> { + val flowValue = collectLastValue(deviceStateRepository.state) + verify(deviceStateManager) + .registerCallback( + any(), + deviceStateManagerListener.capture(), + ) + return flowValue + } + + private fun Int.toIntArray() = listOf(this).toIntArray() + + private companion object { + // Used to fake the ids in the test. Note that there is no guarantees different devices will + // have the same ids (that's why the ones in this test start from 41) + const val TEST_FOLDED = 41 + const val TEST_HALF_FOLDED = 42 + const val TEST_UNFOLDED = 43 + const val TEST_REAR_DISPLAY = 44 + const val TEST_CONCURRENT_DISPLAY = 45 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt index 1f18705edfdb..42b0f5097cad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt @@ -28,6 +28,9 @@ 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.display.data.repository.DeviceStateRepository +import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState.CONCURRENT_DISPLAY +import com.android.systemui.display.data.repository.FakeDeviceStateRepository import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.display.data.repository.createPendingDisplay import com.android.systemui.display.data.repository.display @@ -59,11 +62,13 @@ class ConnectedDisplayInteractorTest : SysuiTestCase() { private val fakeDisplayRepository = FakeDisplayRepository() private val fakeKeyguardRepository = FakeKeyguardRepository() + private val fakeDeviceStateRepository = FakeDeviceStateRepository() private val connectedDisplayStateProvider: ConnectedDisplayInteractor = ConnectedDisplayInteractorImpl( virtualDeviceManager, fakeKeyguardRepository, fakeDisplayRepository, + fakeDeviceStateRepository, UnconfinedTestDispatcher(), ) private val testScope = TestScope(UnconfinedTestDispatcher()) @@ -283,6 +288,44 @@ class ConnectedDisplayInteractorTest : SysuiTestCase() { assertThat(pendingDisplay).isNull() } + @Test + fun concurrentDisplaysInProgress_started_returnsTrue() = + testScope.runTest { + val concurrentDisplaysInProgress = + collectLastValue(connectedDisplayStateProvider.concurrentDisplaysInProgress) + + fakeDeviceStateRepository.emit(CONCURRENT_DISPLAY) + + assertThat(concurrentDisplaysInProgress()).isTrue() + } + + @Test + fun concurrentDisplaysInProgress_stopped_returnsFalse() = + testScope.runTest { + val concurrentDisplaysInProgress = + collectLastValue(connectedDisplayStateProvider.concurrentDisplaysInProgress) + + fakeDeviceStateRepository.emit(CONCURRENT_DISPLAY) + fakeDeviceStateRepository.emit(DeviceStateRepository.DeviceState.UNKNOWN) + + assertThat(concurrentDisplaysInProgress()).isFalse() + } + + @Test + fun concurrentDisplaysInProgress_otherStates_returnsFalse() = + testScope.runTest { + val concurrentDisplaysInProgress = + collectLastValue(connectedDisplayStateProvider.concurrentDisplaysInProgress) + + DeviceStateRepository.DeviceState.entries + .filter { it != CONCURRENT_DISPLAY } + .forEach { deviceState -> + fakeDeviceStateRepository.emit(deviceState) + + assertThat(concurrentDisplaysInProgress()).isFalse() + } + } + private fun TestScope.lastValue(): FlowValue<State?> = collectLastValue(connectedDisplayStateProvider.connectedDisplayState) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventCoordinatorTest.kt index bbc63f2009b9..ae84df55e113 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventCoordinatorTest.kt @@ -21,7 +21,6 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay -import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State.CONNECTED import com.android.systemui.privacy.PrivacyItemController import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.util.mockito.any @@ -107,5 +106,7 @@ class SystemEventCoordinatorTest : SysuiTestCase() { get() = flow override val pendingDisplay: Flow<PendingDisplay?> get() = MutableSharedFlow<PendingDisplay>() + override val concurrentDisplaysInProgress: Flow<Boolean> + get() = TODO("Not yet implemented") } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt index da6c28ad9af4..7deee5a70809 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt @@ -321,5 +321,7 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() { get() = TODO("Not yet implemented") override val pendingDisplay: Flow<PendingDisplay?> get() = TODO("Not yet implemented") + override val concurrentDisplaysInProgress: Flow<Boolean> + get() = TODO("Not yet implemented") } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDeviceStateRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDeviceStateRepository.kt new file mode 100644 index 000000000000..5f6dc6e7d429 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDeviceStateRepository.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 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.display.data.repository + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** Fake [DeviceStateRepository] implementation for testing. */ +class FakeDeviceStateRepository : DeviceStateRepository { + private val flow = MutableStateFlow(DeviceStateRepository.DeviceState.UNKNOWN) + + /** Emits [value] as [displays] flow value. */ + suspend fun emit(value: DeviceStateRepository.DeviceState) = flow.emit(value) + + override val state: StateFlow<DeviceStateRepository.DeviceState> + get() = flow +} |