Show confirmation dialog to start display mirroring

Adds support for a confirmation dialog that is shown when an external
display is connected.

Test: ConnectedDisplayInteractorTest, DisplayRepositoryTest, MirroringConfirmationDialogTest, MirroringConfirmationDialogScerenshotTest
Bug: 265060064
Change-Id: Ic0e443cd822a3c7784bd56687a244e266f2bd3f0
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 6778d5a..b5b873c 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -349,6 +349,9 @@
 
     <uses-permission android:name="android.permission.MONITOR_KEYBOARD_BACKLIGHT" />
 
+    <!-- Listen to (dis-)connection of external displays and enable / disable them. -->
+    <uses-permission android:name="android.permission.MANAGE_DISPLAYS" />
+
     <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" />
     <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" />
     <protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" />
diff --git a/packages/SystemUI/res/layout/connected_display_dialog.xml b/packages/SystemUI/res/layout/connected_display_dialog.xml
new file mode 100644
index 0000000..569dd4c
--- /dev/null
+++ b/packages/SystemUI/res/layout/connected_display_dialog.xml
@@ -0,0 +1,69 @@
+<!--
+  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.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center_horizontal"
+    android:orientation="vertical"
+    android:paddingHorizontal="@dimen/dialog_side_padding"
+    android:paddingTop="@dimen/dialog_top_padding"
+    android:background="@*android:drawable/bottomsheet_background"
+    android:paddingBottom="@dimen/dialog_bottom_padding">
+
+    <ImageView
+        android:id="@+id/connected_display_dialog_icon"
+        android:layout_width="@dimen/screenrecord_logo_size"
+        android:layout_height="@dimen/screenrecord_logo_size"
+        android:importantForAccessibility="no"
+        android:src="@drawable/stat_sys_connected_display"
+        android:tint="?androidprv:attr/materialColorPrimary" />
+
+    <TextView
+        android:id="@+id/connected_display_dialog_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/screenrecord_title_margin_top"
+        android:gravity="center"
+        android:text="@string/connected_display_dialog_start_mirroring"
+        android:textAppearance="@style/TextAppearance.Dialog.Title" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/screenrecord_buttons_margin_top"
+        android:orientation="horizontal">
+
+        <Button
+            android:id="@+id/cancel"
+            style="@style/Widget.Dialog.Button.BorderButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/cancel" />
+
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1" />
+
+        <Button
+            android:id="@+id/enable_display"
+            style="@style/Widget.Dialog.Button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/enable_display" />
+    </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index b37aeee..6840108 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3184,6 +3184,12 @@
     <!-- Label for a button that, when clicked, sends the user to the app store to install an app. [CHAR LIMIT=64]. -->
     <string name="install_app">Install app</string>
 
+    <!--- 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>
+
+    <!--- Label of the "enable display" button of the dialog appearing when an external display is connected [CHAR LIMIT=NONE]-->
+    <string name="enable_display">Enable display</string>
+
     <!-- Title of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=30] -->
     <string name="privacy_dialog_title">Microphone &amp; Camera</string>
     <!-- Subtitle of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index 6fdb4ca..dcacd09 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -22,6 +22,7 @@
 import com.android.systemui.InitController;
 import com.android.systemui.SystemUIAppComponentFactoryBase;
 import com.android.systemui.dagger.qualifiers.PerUser;
+import com.android.systemui.display.ui.viewmodel.ConnectingDisplayViewModel;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.KeyguardSliceProvider;
 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli;
@@ -140,6 +141,7 @@
         getMediaMuteAwaitConnectionCli();
         getNearbyMediaDevicesManager();
         getUnfoldLatencyTracker().init();
+        getConnectingDisplayViewModel().init();
         getFoldStateLoggingProvider().ifPresent(FoldStateLoggingProvider::init);
         getFoldStateLogger().ifPresent(FoldStateLogger::init);
         getUnfoldTransitionProgressProvider().ifPresent((progressProvider) ->
@@ -229,6 +231,11 @@
     NearbyMediaDevicesManager getNearbyMediaDevicesManager();
 
     /**
+     * Creates a ConnectingDisplayViewModel
+     */
+    ConnectingDisplayViewModel getConnectingDisplayViewModel();
+
+    /**
      * Returns {@link CoreStartable}s that should be started with the application.
      */
     Map<Class<?>, Provider<CoreStartable>> getStartables();
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt
index b18f7bf..0c8e293 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt
@@ -22,6 +22,7 @@
 import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
 import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_REMOVED
 import android.os.Handler
+import android.util.Log
 import android.view.Display
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
@@ -41,6 +42,13 @@
 interface DisplayRepository {
     /** Provides a nullable set of displays. */
     val displays: Flow<Set<Display>>
+
+    /**
+     * Pending display id that can be enabled/disabled.
+     *
+     * When `null`, it means there is no pending display waiting to be enabled.
+     */
+    val pendingDisplay: Flow<Int?>
 }
 
 @SysUISingleton
@@ -85,8 +93,59 @@
                 initialValue = getDisplays()
             )
 
-    fun getDisplays(): Set<Display> =
+    private fun getDisplays(): Set<Display> =
         traceSection("DisplayRepository#getDisplays()") {
             displayManager.displays?.toSet() ?: emptySet()
         }
+
+    override val pendingDisplay: Flow<Int?> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : DisplayConnectionListener {
+                        private val pendingIds = mutableSetOf<Int>()
+                        override fun onDisplayConnected(id: Int) {
+                            pendingIds += id
+                            trySend(id)
+                        }
+
+                        override fun onDisplayDisconnected(id: Int) {
+                            if (id in pendingIds) {
+                                pendingIds -= id
+                                trySend(null)
+                            } else {
+                                Log.e(
+                                    TAG,
+                                    "onDisplayDisconnected received for unknown display. " +
+                                        "id=$id, knownIds=$pendingIds"
+                                )
+                            }
+                        }
+                    }
+                displayManager.registerDisplayListener(
+                    callback,
+                    backgroundHandler,
+                    DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED,
+                )
+                awaitClose { displayManager.unregisterDisplayListener(callback) }
+            }
+            .flowOn(backgroundCoroutineDispatcher)
+            .stateIn(
+                applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = null
+            )
+
+    private companion object {
+        const val TAG = "DisplayRepository"
+    }
+}
+
+/** Used to provide default implementations for all methods. */
+private interface DisplayConnectionListener : DisplayListener {
+
+    override fun onDisplayConnected(id: Int) {}
+    override fun onDisplayDisconnected(id: Int) {}
+    override fun onDisplayAdded(id: Int) {}
+    override fun onDisplayRemoved(id: Int) {}
+    override fun onDisplayChanged(id: Int) {}
 }
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 4b957c7..308b7d7 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
@@ -16,10 +16,13 @@
 
 package com.android.systemui.display.domain.interactor
 
+import android.hardware.display.DisplayManager
 import android.view.Display
 import com.android.systemui.dagger.SysUISingleton
 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
+import com.android.systemui.util.traceSection
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.distinctUntilChanged
@@ -37,18 +40,31 @@
      */
     val connectedDisplayState: Flow<State>
 
+    /** Pending display that can be enabled to be used by the system. */
+    val pendingDisplay: Flow<PendingDisplay?>
+
     /** Possible connected display state. */
     enum class State {
         DISCONNECTED,
         CONNECTED,
         CONNECTED_SECURE,
     }
+
+    /** Represents a connected display that has not been enabled yet. */
+    interface PendingDisplay {
+        /** Enables the display, making it available to the system. */
+        fun enable()
+
+        /** Disables the display, making it unavailable to the system. */
+        fun disable()
+    }
 }
 
 @SysUISingleton
 class ConnectedDisplayInteractorImpl
 @Inject
 constructor(
+    private val displayManager: DisplayManager,
     displayRepository: DisplayRepository,
 ) : ConnectedDisplayInteractor {
 
@@ -70,4 +86,22 @@
                 }
             }
             .distinctUntilChanged()
+
+    override val pendingDisplay: Flow<PendingDisplay?> =
+        displayRepository.pendingDisplay.distinctUntilChanged().map { it?.toPendingDisplay() }
+
+    private fun Int.toPendingDisplay() =
+        object : PendingDisplay {
+            val id = this@toPendingDisplay
+            override fun enable() {
+                traceSection("DisplayRepository#enable($id)") {
+                    displayManager.enableConnectedDisplay(id)
+                }
+            }
+            override fun disable() {
+                traceSection("DisplayRepository#enable($id)") {
+                    displayManager.disableConnectedDisplay(id)
+                }
+            }
+        }
 }
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
new file mode 100644
index 0000000..174c6ff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.ui.view
+
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+import android.view.Gravity
+import android.view.View
+import android.view.WindowManager
+import android.widget.TextView
+import com.android.systemui.R
+
+/** Dialog used to decide what to do with a connected display. */
+class MirroringConfirmationDialog(
+    context: Context,
+    private val onStartMirroringClickListener: View.OnClickListener,
+    private val onDismissClickListener: View.OnClickListener,
+) : Dialog(context, R.style.Theme_SystemUI_Dialog) {
+
+    private lateinit var mirrorButton: TextView
+    private lateinit var dismissButton: TextView
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        window?.apply {
+            setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
+            addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS)
+            setGravity(Gravity.BOTTOM)
+        }
+        setContentView(R.layout.connected_display_dialog)
+        setCanceledOnTouchOutside(true)
+        mirrorButton =
+            requireViewById<TextView>(R.id.enable_display).apply {
+                setOnClickListener(onStartMirroringClickListener)
+            }
+        dismissButton =
+            requireViewById<TextView>(R.id.cancel).apply {
+                setOnClickListener(onDismissClickListener)
+            }
+    }
+}
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
new file mode 100644
index 0000000..ece33b7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.ui.viewmodel
+
+import android.app.Dialog
+import android.content.Context
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
+import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay
+import com.android.systemui.display.ui.view.MirroringConfirmationDialog
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+
+/**
+ * Shows/hides a dialog to allow the user to decide whether to use the external display for
+ * mirroring.
+ */
+@SysUISingleton
+class ConnectingDisplayViewModel
+@Inject
+constructor(
+    private val context: Context,
+    private val connectedDisplayInteractor: ConnectedDisplayInteractor,
+    @Application private val scope: CoroutineScope,
+) {
+
+    private var dialog: Dialog? = null
+
+    /** Starts listening for pending displays. */
+    fun init() {
+        connectedDisplayInteractor.pendingDisplay
+            .onEach { pendingDisplay ->
+                if (pendingDisplay == null) {
+                    hideDialog()
+                } else {
+                    showDialog(pendingDisplay)
+                }
+            }
+            .launchIn(scope)
+    }
+
+    private fun showDialog(pendingDisplay: PendingDisplay) {
+        hideDialog()
+        dialog =
+            MirroringConfirmationDialog(
+                    context,
+                    onStartMirroringClickListener = {
+                        pendingDisplay.enable()
+                        hideDialog()
+                    },
+                    onDismissClickListener = { hideDialog() }
+                )
+                .apply { show() }
+    }
+
+    private fun hideDialog() {
+        dialog?.hide()
+        dialog = null
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt
index 9be54fb..7d836a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt
@@ -165,13 +165,78 @@
             assertThat(value?.ids()).containsExactly(1, 2, 3, 4)
         }
 
+    @Test
+    fun onDisplayConnected_pendingDisplayReceived() =
+        testScope.runTest {
+            val pendingDisplay by latestPendingDisplayFlowValue()
+
+            displayListener.value.onDisplayConnected(1)
+
+            assertThat(pendingDisplay).isEqualTo(1)
+        }
+
+    @Test
+    fun onDisplayDisconnected_pendingDisplayNull() =
+        testScope.runTest {
+            val pendingDisplay by latestPendingDisplayFlowValue()
+            displayListener.value.onDisplayConnected(1)
+
+            assertThat(pendingDisplay).isNotNull()
+
+            displayListener.value.onDisplayDisconnected(1)
+
+            assertThat(pendingDisplay).isNull()
+        }
+
+    @Test
+    fun onDisplayDisconnected_unknownDisplay_doesNotSendNull() =
+        testScope.runTest {
+            val pendingDisplay by latestPendingDisplayFlowValue()
+            displayListener.value.onDisplayConnected(1)
+
+            assertThat(pendingDisplay).isNotNull()
+
+            displayListener.value.onDisplayDisconnected(2)
+
+            assertThat(pendingDisplay).isNotNull()
+        }
+
+    @Test
+    fun onDisplayConnected_multipleTimes_sendsOnlyTheLastOne() =
+        testScope.runTest {
+            val pendingDisplay by latestPendingDisplayFlowValue()
+            displayListener.value.onDisplayConnected(1)
+            displayListener.value.onDisplayConnected(2)
+
+            assertThat(pendingDisplay).isEqualTo(2)
+        }
+
     private fun Iterable<Display>.ids(): List<Int> = map { it.displayId }
 
     // Wrapper to capture the displayListener.
     private fun TestScope.latestDisplayFlowValue(): FlowValue<Set<Display>?> {
         val flowValue = collectLastValue(displayRepository.displays)
         verify(displayManager)
-            .registerDisplayListener(displayListener.capture(), eq(testHandler), anyLong())
+            .registerDisplayListener(
+                displayListener.capture(),
+                eq(testHandler),
+                eq(
+                    DisplayManager.EVENT_FLAG_DISPLAY_ADDED or
+                        DisplayManager.EVENT_FLAG_DISPLAY_CHANGED or
+                        DisplayManager.EVENT_FLAG_DISPLAY_REMOVED
+                )
+            )
+        return flowValue
+    }
+
+    private fun TestScope.latestPendingDisplayFlowValue(): FlowValue<Int?> {
+        val flowValue = collectLastValue(displayRepository.pendingDisplay)
+        verify(displayManager)
+            .registerDisplayListener(
+                displayListener.capture(),
+                eq(testHandler),
+                eq(DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED)
+            )
         return flowValue
     }
 
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 eb0ad69..fb19eca 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
@@ -16,6 +16,7 @@
 
 package com.android.systemui.display.domain.interactor
 
+import android.hardware.display.DisplayManager
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.Display
@@ -27,7 +28,10 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.display.data.repository.FakeDisplayRepository
 import com.android.systemui.display.data.repository.display
+import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay
 import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
@@ -35,6 +39,7 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito
 
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
@@ -42,9 +47,10 @@
 @SmallTest
 class ConnectedDisplayInteractorTest : SysuiTestCase() {
 
+    private val displayManager = mock<DisplayManager>()
     private val fakeDisplayRepository = FakeDisplayRepository()
     private val connectedDisplayStateProvider: ConnectedDisplayInteractor =
-        ConnectedDisplayInteractorImpl(fakeDisplayRepository)
+        ConnectedDisplayInteractorImpl(displayManager, fakeDisplayRepository)
     private val testScope = TestScope(UnconfinedTestDispatcher())
 
     @Test
@@ -126,6 +132,42 @@
             assertThat(value).isEqualTo(State.CONNECTED_SECURE)
         }
 
+    @Test
+    fun pendingDisplay_propagated() =
+        testScope.runTest {
+            val value by lastPendingDisplay()
+            val pendingDisplayId = 4
+
+            fakeDisplayRepository.emit(pendingDisplayId)
+
+            assertThat(value).isNotNull()
+        }
+
+    @Test
+    fun onPendingDisplay_enable_displayEnabled() =
+        testScope.runTest {
+            val pendingDisplay by lastPendingDisplay()
+
+            fakeDisplayRepository.emit(1)
+            pendingDisplay!!.enable()
+
+            Mockito.verify(displayManager).enableConnectedDisplay(eq(1))
+        }
+
+    @Test
+    fun onPendingDisplay_disable_displayDisabled() =
+        testScope.runTest {
+            val pendingDisplay by lastPendingDisplay()
+
+            fakeDisplayRepository.emit(1)
+            pendingDisplay!!.disable()
+
+            Mockito.verify(displayManager).disableConnectedDisplay(eq(1))
+        }
+
     private fun TestScope.lastValue(): FlowValue<State?> =
         collectLastValue(connectedDisplayStateProvider.connectedDisplayState)
+
+    private fun TestScope.lastPendingDisplay(): FlowValue<PendingDisplay?> =
+        collectLastValue(connectedDisplayStateProvider.pendingDisplay)
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt
new file mode 100644
index 0000000..7059647
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.ui.view
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
[email protected](setAsMainLooper = true)
+class MirroringConfirmationDialogTest : SysuiTestCase() {
+
+    private lateinit var dialog: MirroringConfirmationDialog
+
+    private val onStartMirroringCallback = mock<View.OnClickListener>()
+    private val onCancelCallback = mock<View.OnClickListener>()
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        dialog = MirroringConfirmationDialog(context, onStartMirroringCallback, onCancelCallback)
+    }
+
+    @Test
+    fun startMirroringButton_clicked_callsCorrectCallback() {
+        dialog.show()
+
+        dialog.requireViewById<View>(R.id.enable_display).callOnClick()
+
+        verify(onStartMirroringCallback).onClick(any())
+        verify(onCancelCallback, never()).onClick(any())
+    }
+
+    @Test
+    fun cancelButton_clicked_callsCorrectCallback() {
+        dialog.show()
+
+        dialog.requireViewById<View>(R.id.cancel).callOnClick()
+
+        verify(onCancelCallback).onClick(any())
+        verify(onStartMirroringCallback, never()).onClick(any())
+    }
+
+    @After
+    fun teardown() {
+        if (::dialog.isInitialized) {
+            dialog.dismiss()
+        }
+    }
+}
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 786856b..66d2465 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
@@ -20,6 +20,7 @@
 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.flags.FakeFeatureFlags
 import com.android.systemui.privacy.PrivacyItemController
@@ -105,5 +106,7 @@
         suspend fun emit(value: ConnectedDisplayInteractor.State) = flow.emit(value)
         override val connectedDisplayState: Flow<ConnectedDisplayInteractor.State>
             get() = flow
+        override val pendingDisplay: Flow<PendingDisplay?>
+            get() = MutableSharedFlow<PendingDisplay>()
     }
 }
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 9795b9d..71c27dec 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
@@ -29,6 +29,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
 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
 import com.android.systemui.privacy.PrivacyItemController
 import com.android.systemui.privacy.logging.PrivacyLogger
@@ -316,5 +317,7 @@
         suspend fun emit(value: State) = flow.emit(value)
         override val connectedDisplayState: Flow<State>
             get() = flow
+        override val pendingDisplay: Flow<PendingDisplay?>
+            get() = TODO("Not yet implemented")
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt
index 715d661..d54ef73 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt
@@ -33,10 +33,17 @@
 /** Fake [DisplayRepository] implementation for testing. */
 class FakeDisplayRepository : DisplayRepository {
     private val flow = MutableSharedFlow<Set<Display>>()
+    private val pendingDisplayFlow = MutableSharedFlow<Int?>()
 
     /** Emits [value] as [displays] flow value. */
     suspend fun emit(value: Set<Display>) = flow.emit(value)
 
+    /** Emits [value] as [pendingDisplay] flow value. */
+    suspend fun emit(value: Int?) = pendingDisplayFlow.emit(value)
+
     override val displays: Flow<Set<Display>>
         get() = flow
+
+    override val pendingDisplay: Flow<Int?>
+        get() = pendingDisplayFlow
 }