diff options
14 files changed, 508 insertions, 3 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 6778d5a08506..b5b873c8231f 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 000000000000..569dd4cf9252 --- /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 b37aeeecea61..6840108e2149 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 6fdb4ca7238f..dcacd090fa27 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.Dependency; 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 @@ public interface SysUIComponent { getMediaMuteAwaitConnectionCli(); getNearbyMediaDevicesManager(); getUnfoldLatencyTracker().init(); + getConnectingDisplayViewModel().init(); getFoldStateLoggingProvider().ifPresent(FoldStateLoggingProvider::init); getFoldStateLogger().ifPresent(FoldStateLogger::init); getUnfoldTransitionProgressProvider().ifPresent((progressProvider) -> @@ -229,6 +231,11 @@ public interface SysUIComponent { 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 b18f7bfe7acc..0c8e2934088e 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_ADDED 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 @@ import kotlinx.coroutines.flow.stateIn 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 @@ constructor( 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 4b957c7f435c..308b7d7844f5 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 @@ interface ConnectedDisplayInteractor { */ 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 @@ constructor( } } .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 000000000000..174c6ff04a7d --- /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 000000000000..ece33b7f6032 --- /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 9be54fbb0a82..7d836a0bebcc 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 @@ class DisplayRepositoryTest : SysuiTestCase() { 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 eb0ad698e34b..fb19ecad3891 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.FlowValue 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.UnconfinedTestDispatcher 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 @@ import org.junit.runner.RunWith @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 @@ class ConnectedDisplayInteractorTest : SysuiTestCase() { 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 000000000000..705964736a49 --- /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) +@TestableLooper.RunWithLooper(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 786856b0baa6..66d2465750ad 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 android.testing.TestableLooper 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 @@ class SystemEventCoordinatorTest : SysuiTestCase() { 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 9795b9d3c169..71c27dec74d4 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 androidx.test.filters.SmallTest 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 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() { 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 715d66191428..d54ef739cdd0 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 @@ fun display(type: Int, flags: Int = 0, id: Int = 0): Display { /** 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 } |