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 & 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
}