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
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 7cf562f..c2c5e00 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -190,6 +190,7 @@
         "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 @@
         "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 3f65aa7..8d7f7eb 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 f49d2a1..7ca0b6e 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 ff23837..b0143f5 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 @@
     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 65cd84b..373279c 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 @@
     ): 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 0000000..83337f7
--- /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 20a9e5d..73b7a8a 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.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 @@
     /** 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 @@
     private val virtualDeviceManager: VirtualDeviceManager,
     keyguardRepository: KeyguardRepository,
     displayRepository: DisplayRepository,
+    deviceStateRepository: DeviceStateRepository,
     @Background backgroundCoroutineDispatcher: CoroutineDispatcher,
 ) : ConnectedDisplayInteractor {
 
@@ -128,9 +133,16 @@
             }
         }
 
+    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 d500d1c2..c0a873a 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 @@
     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 @@
         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 19b4d22..10aa703 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 @@
 
 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 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 @@
     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 @@
                     },
                     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 0000000..21b8aca
--- /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 1f18705..42b0f50 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 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 @@
 
     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 @@
             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 bbc63f2..ae84df5 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 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 @@
             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 da6c28a..7deee5a 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 @@
             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 0000000..5f6dc6e
--- /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
+}