diff options
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 +} |