summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Nicolo' Mazzucato <nicomazz@google.com> 2023-11-30 16:09:19 +0000
committer Nicolo' Mazzucato <nicomazz@google.com> 2023-11-30 18:09:28 +0000
commitadc0250c93aa508fc6f41b75e767e764169ef39d (patch)
tree8d685386e938dd1cc68c3b3212fa094f8850d95b
parent4aa3999f54bc1348952ccc45a9828e44d99e89b7 (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
-rw-r--r--core/res/res/values/config.xml10
-rw-r--r--core/res/res/values/symbols.xml1
-rw-r--r--packages/SystemUI/Android.bp2
-rw-r--r--packages/SystemUI/res/layout/connected_display_dialog.xml9
-rw-r--r--packages/SystemUI/res/values/strings.xml2
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt88
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt23
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DeviceStateRepositoryTest.kt164
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt43
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventCoordinatorTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDeviceStateRepository.kt31
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
+}