Merge changes I9d8c2997,I16cd74a6,Ic22cc8a6 into main
* changes:
[SB][Screen Chips] Show a stop dialog for screen share and screen cast.
[SB][Screen Chips] Show a stop dialog when tapping on screen record chip
[SB][Screen Chips] Distinguish share-to-app and cast-to-other-device.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
index e87c8ad..899122d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
@@ -35,6 +35,7 @@
import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
import com.android.systemui.screenrecord.RecordingController
import com.android.systemui.screenrecord.data.model.ScreenRecordModel
+import com.android.systemui.screenrecord.data.repository.ScreenRecordRepositoryImpl
import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
@@ -72,11 +73,18 @@
.thenReturn(dialog)
}
+ private val screenRecordRepository =
+ ScreenRecordRepositoryImpl(
+ bgCoroutineContext = testScope.testScheduler,
+ recordingController = recordingController,
+ )
+
private val underTest =
ScreenRecordTileUserActionInteractor(
context,
testScope.testScheduler,
testScope.testScheduler,
+ screenRecordRepository,
recordingController,
keyguardInteractor,
keyguardDismissUtil,
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index d3569c6..fe968a7 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -319,6 +319,29 @@
<string name="screenrecord_save_error">Error saving screen recording</string>
<!-- A toast message shown when the screen recording cannot be started due to a generic error [CHAR LIMIT=NONE] -->
<string name="screenrecord_start_error">Error starting screen recording</string>
+ <!-- Title for a dialog shown to the user that will let them stop recording their screen [CHAR LIMIT=50] -->
+ <string name="screenrecord_stop_dialog_title">Stop recording screen?</string>
+ <!-- Text telling a user that they will stop recording their screen if they click the "Stop recording" button [CHAR LIMIT=100] -->
+ <string name="screenrecord_stop_dialog_message">You will stop recording your screen</string>
+ <!-- Button to stop a screen recording [CHAR LIMIT=35] -->
+ <string name="screenrecord_stop_dialog_button">Stop recording</string>
+
+ <!-- Title for a dialog shown to the user that will let them stop sharing their screen to another app on the device [CHAR LIMIT=50] -->
+ <string name="share_to_app_stop_dialog_title">Stop sharing screen?</string>
+ <!-- Text telling a user that they will stop sharing their screen if they click the "Stop sharing" button [CHAR LIMIT=100] -->
+ <string name="share_to_app_stop_dialog_message">You will stop sharing your screen</string>
+ <!-- Button to stop screen sharing [CHAR LIMIT=35] -->
+ <string name="share_to_app_stop_dialog_button">Stop sharing</string>
+
+ <!-- Title for a dialog shown to the user that will let them stop casting their screen to a different device [CHAR LIMIT=50] -->
+ <string name="cast_to_other_device_stop_dialog_title">Stop casting screen?</string>
+ <!-- Text telling a user that they will stop casting their screen to a different device if they click the "Stop casting" button [CHAR LIMIT=100] -->
+ <string name="cast_to_other_device_stop_dialog_message">You will stop casting your screen</string>
+ <!-- Button to stop screen casting to a different device [CHAR LIMIT=35] -->
+ <string name="cast_to_other_device_stop_dialog_button">Stop casting</string>
+
+ <!-- Button to close a dialog without doing any action [CHAR LIMIT=20] -->
+ <string name="close_dialog_button">Close</string>
<!-- Notification title displayed for issue recording [CHAR LIMIT=50]-->
<string name="issuerecord_title">Issue Recorder</string>
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
index 1d5f6f5..de300b2 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
@@ -20,7 +20,21 @@
/** Represents the state of media projection. */
sealed interface MediaProjectionState {
- object NotProjecting : MediaProjectionState
- object EntireScreen : MediaProjectionState
- data class SingleTask(val task: RunningTaskInfo) : MediaProjectionState
+ /** There is no media being projected. */
+ data object NotProjecting : MediaProjectionState
+
+ /**
+ * Media is currently being projected.
+ *
+ * @property hostPackage the package name of the app that is receiving the content of the media
+ * projection (aka which app the phone screen contents are being sent to).
+ */
+ sealed class Projecting(open val hostPackage: String) : MediaProjectionState {
+ /** The entire screen is being projected. */
+ data class EntireScreen(override val hostPackage: String) : Projecting(hostPackage)
+
+ /** Only a single task is being projected. */
+ data class SingleTask(override val hostPackage: String, val task: RunningTaskInfo) :
+ Projecting(hostPackage)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
index 3ce0a1e0..8a9adc7 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
@@ -64,6 +64,10 @@
}
}
+ override suspend fun stopProjecting() {
+ withContext(backgroundDispatcher) { mediaProjectionManager.stopActiveProjection() }
+ }
+
override val mediaProjectionState: Flow<MediaProjectionState> =
conflatedCallbackFlow {
val callback =
@@ -83,7 +87,9 @@
session: ContentRecordingSession?
) {
Log.d(TAG, "MediaProjectionManager.Callback#onSessionStarted: $session")
- launch { trySendWithFailureLogging(stateForSession(session), TAG) }
+ launch {
+ trySendWithFailureLogging(stateForSession(info, session), TAG)
+ }
}
}
mediaProjectionManager.addCallback(callback, handler)
@@ -95,19 +101,23 @@
initialValue = MediaProjectionState.NotProjecting,
)
- private suspend fun stateForSession(session: ContentRecordingSession?): MediaProjectionState {
+ private suspend fun stateForSession(
+ info: MediaProjectionInfo,
+ session: ContentRecordingSession?
+ ): MediaProjectionState {
if (session == null) {
return MediaProjectionState.NotProjecting
}
+
+ val hostPackage = info.packageName
if (session.contentToRecord == RECORD_CONTENT_DISPLAY || session.tokenToRecord == null) {
- return MediaProjectionState.EntireScreen
+ return MediaProjectionState.Projecting.EntireScreen(hostPackage)
}
val matchingTask =
tasksRepository.findRunningTaskFromWindowContainerToken(
checkNotNull(session.tokenToRecord)
- )
- ?: return MediaProjectionState.EntireScreen
- return MediaProjectionState.SingleTask(matchingTask)
+ ) ?: return MediaProjectionState.Projecting.EntireScreen(hostPackage)
+ return MediaProjectionState.Projecting.SingleTask(hostPackage, matchingTask)
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepository.kt
index 21300db..50182d7 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepository.kt
@@ -26,6 +26,9 @@
/** Switches the task that should be projected. */
suspend fun switchProjectedTask(task: RunningTaskInfo)
+ /** Stops the currently active projection. */
+ suspend fun stopProjecting()
+
/** Represents the current [MediaProjectionState]. */
val mediaProjectionState: Flow<MediaProjectionState>
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt
index c232d4d..118639c 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt
@@ -57,7 +57,7 @@
mediaProjectionRepository.mediaProjectionState.flatMapLatest { projectionState ->
Log.d(TAG, "MediaProjectionState -> $projectionState")
when (projectionState) {
- is MediaProjectionState.SingleTask -> {
+ is MediaProjectionState.Projecting.SingleTask -> {
val projectedTask = projectionState.task
tasksRepository.foregroundTask.map { foregroundTask ->
if (hasForegroundTaskSwitched(projectedTask, foregroundTask)) {
@@ -67,7 +67,7 @@
}
}
}
- is MediaProjectionState.EntireScreen,
+ is MediaProjectionState.Projecting.EntireScreen,
is MediaProjectionState.NotProjecting -> {
flowOf(TaskSwitchState.NotProjectingTask)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
index 79720c1..5637115 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
@@ -35,6 +35,7 @@
import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
import com.android.systemui.screenrecord.RecordingController
import com.android.systemui.screenrecord.data.model.ScreenRecordModel
+import com.android.systemui.screenrecord.data.repository.ScreenRecordRepository
import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
@@ -47,6 +48,7 @@
@Application private val context: Context,
@Main private val mainContext: CoroutineContext,
@Background private val backgroundContext: CoroutineContext,
+ private val screenRecordRepository: ScreenRecordRepository,
private val recordingController: RecordingController,
private val keyguardInteractor: KeyguardInteractor,
private val keyguardDismissUtil: KeyguardDismissUtil,
@@ -65,8 +67,7 @@
Log.d(TAG, "Cancelling countdown")
withContext(backgroundContext) { recordingController.cancelCountdown() }
}
- is ScreenRecordModel.Recording ->
- withContext(backgroundContext) { recordingController.stopRecording() }
+ is ScreenRecordModel.Recording -> screenRecordRepository.stopRecording()
is ScreenRecordModel.DoingNothing ->
withContext(mainContext) {
showPrompt(action.expandable, user.identifier)
@@ -122,8 +123,7 @@
controller,
animateBackgroundBoundsChange = true,
)
- }
- ?: dialog.show()
+ } ?: dialog.show()
} else {
dialog.show()
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepository.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepository.kt
index d59d220..9eeb3b9 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepository.kt
@@ -28,6 +28,7 @@
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.withContext
/**
* Repository storing information about the state of screen recording.
@@ -38,6 +39,9 @@
interface ScreenRecordRepository {
/** The current screen recording state. Note that this is a cold flow. */
val screenRecordState: Flow<ScreenRecordModel>
+
+ /** Stops the recording. */
+ suspend fun stopRecording()
}
@SysUISingleton
@@ -90,4 +94,8 @@
ScreenRecordModel.DoingNothing
}
}
+
+ override suspend fun stopRecording() {
+ withContext(bgCoroutineContext) { recordingController.stopRecording() }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractor.kt
index c3d37fb..086a32d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractor.kt
@@ -16,11 +16,37 @@
package com.android.systemui.statusbar.chips.domain.interactor
+import android.view.View
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.phone.SystemUIDialog
import kotlinx.coroutines.flow.StateFlow
/** Interface for an interactor that knows the state of a single type of ongoing activity chip. */
interface OngoingActivityChipInteractor {
/** A flow modeling the chip that should be shown. */
val chip: StateFlow<OngoingActivityChipModel>
+
+ companion object {
+ /** Creates a chip click listener that launches a dialog created by [dialogDelegate]. */
+ fun createDialogLaunchOnClickListener(
+ dialogDelegate: SystemUIDialog.Delegate,
+ dialogTransitionAnimator: DialogTransitionAnimator,
+ ): View.OnClickListener {
+ return View.OnClickListener { view ->
+ val dialog = dialogDelegate.createDialog()
+ val launchableView =
+ view.requireViewById<ChipBackgroundContainer>(
+ R.id.ongoing_activity_chip_background
+ )
+ // TODO(b/343699052): This makes a beautiful animate-in, but the
+ // animate-out looks odd because the dialog animates back into the chip
+ // but then the chip disappears. If we aren't able to address
+ // b/343699052 in time for launch, we should just use `dialog.show`.
+ dialogTransitionAnimator.showFromView(dialog, launchableView)
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
index ac16d26..6611434 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
@@ -16,6 +16,9 @@
package com.android.systemui.statusbar.chips.mediaprojection.domain.interactor
+import android.content.pm.PackageManager
+import androidx.annotation.DrawableRes
+import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
@@ -24,7 +27,12 @@
import com.android.systemui.mediaprojection.data.repository.MediaProjectionRepository
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor
+import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor.Companion.createDialogLaunchOnClickListener
import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndCastToOtherDeviceDialogDelegate
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndShareToAppDialogDelegate
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.Utils
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -32,6 +40,7 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
/**
* Interactor for media-projection-related chips in the status bar.
@@ -47,33 +56,88 @@
class MediaProjectionChipInteractor
@Inject
constructor(
- @Application scope: CoroutineScope,
- mediaProjectionRepository: MediaProjectionRepository,
- val systemClock: SystemClock,
+ @Application private val scope: CoroutineScope,
+ private val mediaProjectionRepository: MediaProjectionRepository,
+ private val packageManager: PackageManager,
+ private val systemClock: SystemClock,
+ private val dialogFactory: SystemUIDialog.Factory,
+ private val dialogTransitionAnimator: DialogTransitionAnimator,
) : OngoingActivityChipInteractor {
override val chip: StateFlow<OngoingActivityChipModel> =
mediaProjectionRepository.mediaProjectionState
.map { state ->
when (state) {
is MediaProjectionState.NotProjecting -> OngoingActivityChipModel.Hidden
- is MediaProjectionState.EntireScreen,
- is MediaProjectionState.SingleTask -> {
- // TODO(b/332662551): Distinguish between cast-to-other-device and
- // share-to-app.
- OngoingActivityChipModel.Shown(
- icon =
- Icon.Resource(
- R.drawable.ic_cast_connected,
- ContentDescription.Resource(R.string.accessibility_casting)
- ),
- // TODO(b/332662551): See if we can use a MediaProjection API to fetch
- // this time.
- startTimeMs = systemClock.elapsedRealtime()
- ) {
- // TODO(b/332662551): Implement the pause dialog.
+ is MediaProjectionState.Projecting -> {
+ if (isProjectionToOtherDevice(state.hostPackage)) {
+ createCastToOtherDeviceChip()
+ } else {
+ createShareToAppChip()
}
}
}
}
.stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+
+ /** Stops the currently active projection. */
+ fun stopProjecting() {
+ scope.launch { mediaProjectionRepository.stopProjecting() }
+ }
+
+ /**
+ * Returns true iff projecting to the given [packageName] means that we're projecting to a
+ * *different* device (as opposed to projecting to some application on *this* device).
+ */
+ private fun isProjectionToOtherDevice(packageName: String?): Boolean {
+ // The [isHeadlessRemoteDisplayProvider] check approximates whether a projection is to a
+ // different device or the same device, because headless remote display packages are the
+ // only kinds of packages that do cast-to-other-device. This isn't exactly perfect,
+ // because it means that any projection by those headless remote display packages will be
+ // marked as going to a different device, even if that isn't always true. See b/321078669.
+ return Utils.isHeadlessRemoteDisplayProvider(packageManager, packageName)
+ }
+
+ private fun createCastToOtherDeviceChip(): OngoingActivityChipModel.Shown {
+ return OngoingActivityChipModel.Shown(
+ icon =
+ Icon.Resource(
+ CAST_TO_OTHER_DEVICE_ICON,
+ ContentDescription.Resource(R.string.accessibility_casting)
+ ),
+ // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
+ startTimeMs = systemClock.elapsedRealtime(),
+ createDialogLaunchOnClickListener(
+ castToOtherDeviceDialogDelegate,
+ dialogTransitionAnimator,
+ ),
+ )
+ }
+
+ private val castToOtherDeviceDialogDelegate =
+ EndCastToOtherDeviceDialogDelegate(
+ dialogFactory,
+ this@MediaProjectionChipInteractor,
+ )
+
+ private fun createShareToAppChip(): OngoingActivityChipModel.Shown {
+ return OngoingActivityChipModel.Shown(
+ // TODO(b/332662551): Use the right content description.
+ icon = Icon.Resource(SHARE_TO_APP_ICON, contentDescription = null),
+ // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
+ startTimeMs = systemClock.elapsedRealtime(),
+ createDialogLaunchOnClickListener(shareToAppDialogDelegate, dialogTransitionAnimator),
+ )
+ }
+
+ private val shareToAppDialogDelegate =
+ EndShareToAppDialogDelegate(
+ dialogFactory,
+ this@MediaProjectionChipInteractor,
+ )
+
+ companion object {
+ // TODO(b/332662551): Use the right icon.
+ @DrawableRes val SHARE_TO_APP_ICON = R.drawable.ic_screenshot_share
+ @DrawableRes val CAST_TO_OTHER_DEVICE_ICON = R.drawable.ic_cast_connected
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegate.kt
new file mode 100644
index 0000000..33cec97
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegate.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 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.statusbar.chips.mediaprojection.ui.view
+
+import android.os.Bundle
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/** A dialog that lets the user stop an ongoing cast-screen-to-other-device event. */
+class EndCastToOtherDeviceDialogDelegate(
+ private val systemUIDialogFactory: SystemUIDialog.Factory,
+ private val interactor: MediaProjectionChipInteractor,
+) : SystemUIDialog.Delegate {
+ override fun createDialog(): SystemUIDialog {
+ return systemUIDialogFactory.create(this)
+ }
+
+ override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+ with(dialog) {
+ setIcon(MediaProjectionChipInteractor.CAST_TO_OTHER_DEVICE_ICON)
+ setTitle(R.string.cast_to_other_device_stop_dialog_title)
+ // TODO(b/332662551): Use a different message if they're sharing just a single app.
+ setMessage(R.string.cast_to_other_device_stop_dialog_message)
+ // No custom on-click, because the dialog will automatically be dismissed when the
+ // button is clicked anyway.
+ setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
+ setPositiveButton(R.string.cast_to_other_device_stop_dialog_button) { _, _ ->
+ interactor.stopProjecting()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegate.kt
new file mode 100644
index 0000000..3a863b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegate.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 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.statusbar.chips.mediaprojection.ui.view
+
+import android.os.Bundle
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/** A dialog that lets the user stop an ongoing share-screen-to-app event. */
+class EndShareToAppDialogDelegate(
+ private val systemUIDialogFactory: SystemUIDialog.Factory,
+ private val interactor: MediaProjectionChipInteractor,
+) : SystemUIDialog.Delegate {
+ override fun createDialog(): SystemUIDialog {
+ return systemUIDialogFactory.create(this)
+ }
+
+ override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+ with(dialog) {
+ setIcon(MediaProjectionChipInteractor.SHARE_TO_APP_ICON)
+ setTitle(R.string.share_to_app_stop_dialog_title)
+ // TODO(b/332662551): Use a different message if they're sharing just a single app.
+ setMessage(R.string.share_to_app_stop_dialog_message)
+ // No custom on-click, because the dialog will automatically be dismissed when the
+ // button is clicked anyway.
+ setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
+ setPositiveButton(R.string.share_to_app_stop_dialog_button) { _, _ ->
+ interactor.stopProjecting()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
index 585ff5f..4959b09 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.chips.screenrecord.domain.interactor
+import androidx.annotation.DrawableRes
+import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -23,7 +25,10 @@
import com.android.systemui.screenrecord.data.model.ScreenRecordModel
import com.android.systemui.screenrecord.data.repository.ScreenRecordRepository
import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor
+import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor.Companion.createDialogLaunchOnClickListener
import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.screenrecord.ui.view.EndScreenRecordingDialogDelegate
+import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -31,15 +36,18 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
/** Interactor for the screen recording chip shown in the status bar. */
@SysUISingleton
class ScreenRecordChipInteractor
@Inject
constructor(
- @Application scope: CoroutineScope,
- screenRecordRepository: ScreenRecordRepository,
- val systemClock: SystemClock,
+ @Application private val scope: CoroutineScope,
+ private val screenRecordRepository: ScreenRecordRepository,
+ private val systemClock: SystemClock,
+ private val dialogFactory: SystemUIDialog.Factory,
+ private val dialogTransitionAnimator: DialogTransitionAnimator,
) : OngoingActivityChipInteractor {
override val chip: StateFlow<OngoingActivityChipModel> =
screenRecordRepository.screenRecordState
@@ -51,16 +59,29 @@
is ScreenRecordModel.Recording ->
OngoingActivityChipModel.Shown(
// TODO(b/332662551): Also provide a content description.
- icon =
- Icon.Resource(
- R.drawable.stat_sys_screen_record,
- contentDescription = null
- ),
- startTimeMs = systemClock.elapsedRealtime()
- ) {
- // TODO(b/332662551): Implement the pause dialog.
- }
+ icon = Icon.Resource(ICON, contentDescription = null),
+ startTimeMs = systemClock.elapsedRealtime(),
+ createDialogLaunchOnClickListener(
+ dialogDelegate,
+ dialogTransitionAnimator
+ ),
+ )
}
}
.stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+
+ /** Stops the recording. */
+ fun stopRecording() {
+ scope.launch { screenRecordRepository.stopRecording() }
+ }
+
+ private val dialogDelegate =
+ EndScreenRecordingDialogDelegate(
+ dialogFactory,
+ this@ScreenRecordChipInteractor,
+ )
+
+ companion object {
+ @DrawableRes val ICON = R.drawable.ic_screenrecord
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt
new file mode 100644
index 0000000..b8e8cfa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 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.statusbar.chips.screenrecord.ui.view
+
+import android.os.Bundle
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/** A dialog that lets the user stop an ongoing screen recording. */
+class EndScreenRecordingDialogDelegate(
+ private val systemUIDialogFactory: SystemUIDialog.Factory,
+ private val interactor: ScreenRecordChipInteractor,
+) : SystemUIDialog.Delegate {
+
+ override fun createDialog(): SystemUIDialog {
+ return systemUIDialogFactory.create(this)
+ }
+
+ override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+ with(dialog) {
+ setIcon(ScreenRecordChipInteractor.ICON)
+ setTitle(R.string.screenrecord_stop_dialog_title)
+ // TODO(b/332662551): Use a different message if they're sharing just a single app.
+ setMessage(R.string.screenrecord_stop_dialog_message)
+ // No custom on-click, because the dialog will automatically be dismissed when the
+ // button is clicked anyway.
+ setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
+ setPositiveButton(R.string.screenrecord_stop_dialog_button) { _, _ ->
+ interactor.stopRecording()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
index a2ec1f2..44b5baf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
@@ -94,7 +94,7 @@
is OngoingActivityChipModel.Shown -> {
IconViewBinder.bind(chipModel.icon, chipIconView)
ChipChronometerBinder.bind(chipModel.startTimeMs, chipTimeView)
- // TODO(b/332662551): Attach click listener to chip
+ chipView.setOnClickListener(chipModel.onClickListener)
listener.onOngoingActivityStatusChanged(
hasOngoingActivity = true
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
index b7fefc0..c0d411b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
@@ -16,7 +16,9 @@
package com.android.systemui.mediaprojection.data.repository
+import android.media.projection.MediaProjectionInfo
import android.os.Binder
+import android.os.UserHandle
import android.view.ContentRecordingSession
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -26,6 +28,7 @@
import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken
+import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager.Companion.createDisplaySession
import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager
import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager
import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos
@@ -33,6 +36,7 @@
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.verify
@RunWith(AndroidJUnit4::class)
@SmallTest
@@ -55,7 +59,8 @@
fakeActivityTaskManager.addRunningTasks(task)
repo.switchProjectedTask(task)
- assertThat(state).isEqualTo(MediaProjectionState.SingleTask(task))
+ assertThat(state).isInstanceOf(MediaProjectionState.Projecting.SingleTask::class.java)
+ assertThat((state as MediaProjectionState.Projecting.SingleTask).task).isEqualTo(task)
}
@Test
@@ -97,7 +102,7 @@
session = ContentRecordingSession.createDisplaySession(/* displayToMirror= */ 123)
)
- assertThat(state).isEqualTo(MediaProjectionState.EntireScreen)
+ assertThat(state).isInstanceOf(MediaProjectionState.Projecting.EntireScreen::class.java)
}
@Test
@@ -110,7 +115,27 @@
session = ContentRecordingSession.createTaskSession(taskWindowContainerToken)
)
- assertThat(state).isEqualTo(MediaProjectionState.EntireScreen)
+ assertThat(state).isInstanceOf(MediaProjectionState.Projecting.EntireScreen::class.java)
+ }
+
+ @Test
+ fun mediaProjectionState_entireScreen_hasHostPackage() =
+ testScope.runTest {
+ val state by collectLastValue(repo.mediaProjectionState)
+
+ val info =
+ MediaProjectionInfo(
+ /* packageName= */ "com.media.projection.repository.test",
+ /* handle= */ UserHandle.getUserHandleForUid(UserHandle.myUserId()),
+ /* launchCookie = */ null,
+ )
+ fakeMediaProjectionManager.dispatchOnSessionSet(
+ info = info,
+ session = createDisplaySession(),
+ )
+
+ assertThat((state as MediaProjectionState.Projecting.EntireScreen).hostPackage)
+ .isEqualTo("com.media.projection.repository.test")
}
@Test
@@ -125,6 +150,39 @@
session = ContentRecordingSession.createTaskSession(token.asBinder())
)
- assertThat(state).isEqualTo(MediaProjectionState.SingleTask(task))
+ assertThat(state).isInstanceOf(MediaProjectionState.Projecting.SingleTask::class.java)
+ assertThat((state as MediaProjectionState.Projecting.SingleTask).task).isEqualTo(task)
+ }
+
+ @Test
+ fun mediaProjectionState_singleTask_hasHostPackage() =
+ testScope.runTest {
+ val state by collectLastValue(repo.mediaProjectionState)
+
+ val token = createToken()
+ val task = createTask(taskId = 1, token = token)
+ fakeActivityTaskManager.addRunningTasks(task)
+
+ val info =
+ MediaProjectionInfo(
+ /* packageName= */ "com.media.projection.repository.test",
+ /* handle= */ UserHandle.getUserHandleForUid(UserHandle.myUserId()),
+ /* launchCookie = */ null,
+ )
+ fakeMediaProjectionManager.dispatchOnSessionSet(
+ info = info,
+ session = ContentRecordingSession.createTaskSession(token.asBinder())
+ )
+
+ assertThat((state as MediaProjectionState.Projecting.SingleTask).hostPackage)
+ .isEqualTo("com.media.projection.repository.test")
+ }
+
+ @Test
+ fun stopProjecting_invokesManager() =
+ testScope.runTest {
+ repo.stopProjecting()
+
+ verify(fakeMediaProjectionManager.mediaProjectionManager).stopActiveProjection()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepositoryTest.kt
index b77a15b..61ea437 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepositoryTest.kt
@@ -119,4 +119,12 @@
assertThat(lastModel).isEqualTo(isRecording)
}
+
+ @Test
+ fun stopRecording_invokesController() =
+ testScope.runTest {
+ underTest.stopRecording()
+
+ verify(recordingController).stopRecording()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractorTest.kt
new file mode 100644
index 0000000..abb6e2b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractorTest.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 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.statusbar.chips.domain.interactor
+
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor.Companion.createDialogLaunchOnClickListener
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import kotlin.test.Test
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+class OngoingActivityChipInteractorTest : SysuiTestCase() {
+ private val mockSystemUIDialog = mock<SystemUIDialog>()
+ private val dialogDelegate = SystemUIDialog.Delegate { mockSystemUIDialog }
+ private val dialogTransitionAnimator = mock<DialogTransitionAnimator>()
+
+ private val chipBackgroundView = mock<ChipBackgroundContainer>()
+ private val chipView =
+ mock<View>().apply {
+ whenever(
+ this.requireViewById<ChipBackgroundContainer>(
+ R.id.ongoing_activity_chip_background
+ )
+ )
+ .thenReturn(chipBackgroundView)
+ }
+
+ @Test
+ fun createDialogLaunchOnClickListener_showsDialogOnClick() {
+ val clickListener =
+ createDialogLaunchOnClickListener(dialogDelegate, dialogTransitionAnimator)
+
+ // Dialogs must be created on the main thread
+ context.mainExecutor.execute {
+ clickListener.onClick(chipView)
+ verify(dialogTransitionAnimator)
+ .showFromView(
+ eq(mockSystemUIDialog),
+ eq(chipBackgroundView),
+ eq(null),
+ anyBoolean(),
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
index 0f33b9d..a4505a9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
@@ -16,8 +16,15 @@
package com.android.systemui.statusbar.chips.mediaprojection.domain.interactor
+import android.Manifest
+import android.content.Intent
+import android.content.packageManager
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.mockDialogTransitionAnimator
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
@@ -27,11 +34,24 @@
import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
-import com.android.systemui.statusbar.chips.ui.viewmodel.mediaProjectionChipInteractor
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndCastToOtherDeviceDialogDelegate
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndShareToAppDialogDelegate
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doAnswer
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
@SmallTest
class MediaProjectionChipInteractorTest : SysuiTestCase() {
@@ -40,6 +60,30 @@
private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository
private val systemClock = kosmos.fakeSystemClock
+ private val mockCastDialog = mock<SystemUIDialog>()
+ private val mockShareDialog = mock<SystemUIDialog>()
+
+ private val chipBackgroundView = mock<ChipBackgroundContainer>()
+ private val chipView =
+ mock<View>().apply {
+ whenever(
+ this.requireViewById<ChipBackgroundContainer>(
+ R.id.ongoing_activity_chip_background
+ )
+ )
+ .thenReturn(chipBackgroundView)
+ }
+
+ @Before
+ fun setUp() {
+ setUpPackageManagerForMediaProjection(kosmos)
+
+ whenever(kosmos.mockSystemUIDialogFactory.create(any<EndCastToOtherDeviceDialogDelegate>()))
+ .thenReturn(mockCastDialog)
+ whenever(kosmos.mockSystemUIDialogFactory.create(any<EndShareToAppDialogDelegate>()))
+ .thenReturn(mockShareDialog)
+ }
+
private val underTest = kosmos.mediaProjectionChipInteractor
@Test
@@ -53,12 +97,15 @@
}
@Test
- fun chip_singleTaskState_isShownWithIcon() =
+ fun chip_singleTaskState_otherDevicesPackage_castToOtherDeviceChipShown() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
mediaProjectionRepo.mediaProjectionState.value =
- MediaProjectionState.SingleTask(createTask(taskId = 1))
+ MediaProjectionState.Projecting.SingleTask(
+ CAST_TO_OTHER_DEVICES_PACKAGE,
+ createTask(taskId = 1)
+ )
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
val icon = (latest as OngoingActivityChipModel.Shown).icon
@@ -66,11 +113,12 @@
}
@Test
- fun chip_entireScreenState_isShownWithIcon() =
+ fun chip_entireScreenState_otherDevicesPackage_castToOtherDeviceChipShown() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
- mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.EntireScreen
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
val icon = (latest as OngoingActivityChipModel.Shown).icon
@@ -78,12 +126,39 @@
}
@Test
+ fun chip_singleTaskState_normalPackage_shareToAppChipShown() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(NORMAL_PACKAGE, createTask(taskId = 1))
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+ val icon = (latest as OngoingActivityChipModel.Shown).icon
+ assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenshot_share)
+ }
+
+ @Test
+ fun chip_entireScreenState_normalPackage_shareToAppChipShown() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+ val icon = (latest as OngoingActivityChipModel.Shown).icon
+ assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenshot_share)
+ }
+
+ @Test
fun chip_timeResetsOnEachNewShare() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
systemClock.setElapsedRealtime(1234)
- mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.EntireScreen
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(1234)
@@ -92,9 +167,99 @@
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
systemClock.setElapsedRealtime(5678)
- mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.EntireScreen
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(
+ CAST_TO_OTHER_DEVICES_PACKAGE,
+ createTask(taskId = 1)
+ )
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(5678)
}
+
+ @Test
+ fun chip_castToOtherDevice_clickListenerShowsCastDialog() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
+
+ val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+
+ // Dialogs must be created on the main thread
+ context.mainExecutor.execute {
+ clickListener.onClick(chipView)
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ eq(mockCastDialog),
+ eq(chipBackgroundView),
+ eq(null),
+ anyBoolean(),
+ )
+ }
+ }
+
+ @Test
+ fun chip_shareToApp_clickListenerShowsShareDialog() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
+
+ val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+
+ // Dialogs must be created on the main thread
+ context.mainExecutor.execute {
+ clickListener.onClick(chipView)
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ eq(mockShareDialog),
+ eq(chipBackgroundView),
+ eq(null),
+ anyBoolean(),
+ )
+ }
+ }
+
+ companion object {
+ const val CAST_TO_OTHER_DEVICES_PACKAGE = "other.devices.package"
+ const val NORMAL_PACKAGE = "some.normal.package"
+
+ /**
+ * Sets up [kosmos.packageManager] so that [CAST_TO_OTHER_DEVICES_PACKAGE] is marked as a
+ * package that casts to other devices, and [NORMAL_PACKAGE] is *not* marked as casting to
+ * other devices.
+ */
+ fun setUpPackageManagerForMediaProjection(kosmos: Kosmos) {
+ kosmos.packageManager.apply {
+ whenever(
+ this.checkPermission(
+ Manifest.permission.REMOTE_DISPLAY_PROVIDER,
+ CAST_TO_OTHER_DEVICES_PACKAGE
+ )
+ )
+ .thenReturn(PackageManager.PERMISSION_GRANTED)
+ whenever(
+ this.checkPermission(
+ Manifest.permission.REMOTE_DISPLAY_PROVIDER,
+ NORMAL_PACKAGE
+ )
+ )
+ .thenReturn(PackageManager.PERMISSION_DENIED)
+
+ doAnswer {
+ // See Utils.isHeadlessRemoteDisplayProvider
+ if (
+ (it.arguments[0] as Intent).`package` == CAST_TO_OTHER_DEVICES_PACKAGE
+ ) {
+ emptyList()
+ } else {
+ listOf(mock<ResolveInfo>())
+ }
+ }
+ .whenever(this)
+ .queryIntentActivities(any(), anyInt())
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt
new file mode 100644
index 0000000..9a2f545
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 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.statusbar.chips.mediaprojection.ui.view
+
+import android.content.DialogInterface
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class EndCastToOtherDeviceDialogDelegateTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val sysuiDialog = mock<SystemUIDialog>()
+ private val sysuiDialogFactory = kosmos.mockSystemUIDialogFactory
+ private val underTest =
+ EndCastToOtherDeviceDialogDelegate(
+ sysuiDialogFactory,
+ kosmos.mediaProjectionChipInteractor,
+ )
+
+ @Before
+ fun setUp() {
+ whenever(sysuiDialogFactory.create(eq(underTest), eq(context))).thenReturn(sysuiDialog)
+ }
+
+ @Test
+ fun icon() {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(sysuiDialog).setIcon(R.drawable.ic_cast_connected)
+ }
+
+ @Test
+ fun title() {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(sysuiDialog).setTitle(R.string.cast_to_other_device_stop_dialog_title)
+ }
+
+ @Test
+ fun message() {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(sysuiDialog).setMessage(R.string.cast_to_other_device_stop_dialog_message)
+ }
+
+ @Test
+ fun negativeButton() {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(sysuiDialog).setNegativeButton(R.string.close_dialog_button, null)
+ }
+
+ @Test
+ fun positiveButton() =
+ kosmos.testScope.runTest {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ val clickListener = argumentCaptor<DialogInterface.OnClickListener>()
+
+ // Verify the button has the right text
+ verify(sysuiDialog)
+ .setPositiveButton(
+ eq(R.string.cast_to_other_device_stop_dialog_button),
+ clickListener.capture()
+ )
+
+ // Verify that clicking the button stops the recording
+ assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isFalse()
+
+ clickListener.firstValue.onClick(mock<DialogInterface>(), 0)
+ runCurrent()
+
+ assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegateTest.kt
new file mode 100644
index 0000000..1d6e866
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegateTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 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.statusbar.chips.mediaprojection.ui.view
+
+import android.content.DialogInterface
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class EndShareToAppDialogDelegateTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val sysuiDialog = mock<SystemUIDialog>()
+ private val sysuiDialogFactory = kosmos.mockSystemUIDialogFactory
+ private val underTest =
+ EndShareToAppDialogDelegate(
+ sysuiDialogFactory,
+ kosmos.mediaProjectionChipInteractor,
+ )
+
+ @Before
+ fun setUp() {
+ whenever(sysuiDialogFactory.create(eq(underTest), eq(context))).thenReturn(sysuiDialog)
+ }
+
+ @Test
+ fun icon() {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(sysuiDialog).setIcon(R.drawable.ic_screenshot_share)
+ }
+
+ @Test
+ fun title() {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(sysuiDialog).setTitle(R.string.share_to_app_stop_dialog_title)
+ }
+
+ @Test
+ fun message() {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(sysuiDialog).setMessage(R.string.share_to_app_stop_dialog_message)
+ }
+
+ @Test
+ fun negativeButton() {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(sysuiDialog).setNegativeButton(R.string.close_dialog_button, null)
+ }
+
+ @Test
+ fun positiveButton() =
+ kosmos.testScope.runTest {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ val clickListener = argumentCaptor<DialogInterface.OnClickListener>()
+
+ // Verify the button has the right text
+ verify(sysuiDialog)
+ .setPositiveButton(
+ eq(R.string.share_to_app_stop_dialog_button),
+ clickListener.capture()
+ )
+
+ // Verify that clicking the button stops the recording
+ assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isFalse()
+
+ clickListener.firstValue.onClick(mock<DialogInterface>(), 0)
+ runCurrent()
+
+ assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
index 25efaf1..f6c3adb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
@@ -16,8 +16,10 @@
package com.android.systemui.statusbar.chips.screenrecord.domain.interactor
+import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.mockDialogTransitionAnimator
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
@@ -26,11 +28,21 @@
import com.android.systemui.screenrecord.data.model.ScreenRecordModel
import com.android.systemui.screenrecord.data.repository.screenRecordRepository
import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
import com.android.systemui.statusbar.chips.ui.viewmodel.screenRecordChipInteractor
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
@SmallTest
class ScreenRecordChipInteractorTest : SysuiTestCase() {
@@ -38,9 +50,27 @@
private val testScope = kosmos.testScope
private val screenRecordRepo = kosmos.screenRecordRepository
private val systemClock = kosmos.fakeSystemClock
+ private val mockSystemUIDialog = mock<SystemUIDialog>()
+
+ private val chipBackgroundView = mock<ChipBackgroundContainer>()
+ private val chipView =
+ mock<View>().apply {
+ whenever(
+ this.requireViewById<ChipBackgroundContainer>(
+ R.id.ongoing_activity_chip_background
+ )
+ )
+ .thenReturn(chipBackgroundView)
+ }
private val underTest = kosmos.screenRecordChipInteractor
+ @Before
+ fun setUp() {
+ whenever(kosmos.mockSystemUIDialogFactory.create(any<SystemUIDialog.Delegate>()))
+ .thenReturn(mockSystemUIDialog)
+ }
+
@Test
fun chip_doingNothingState_isHidden() =
testScope.runTest {
@@ -70,7 +100,7 @@
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
val icon = (latest as OngoingActivityChipModel.Shown).icon
- assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.stat_sys_screen_record)
+ assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenrecord)
}
@Test
@@ -93,4 +123,25 @@
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(5678)
}
+
+ @Test
+ fun chip_clickListenerShowsDialog() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+ screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
+
+ val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+
+ // Dialogs must be created on the main thread
+ context.mainExecutor.execute {
+ clickListener.onClick(chipView)
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ eq(mockSystemUIDialog),
+ eq(chipBackgroundView),
+ eq(null),
+ anyBoolean(),
+ )
+ }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
new file mode 100644
index 0000000..bca6763
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 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.statusbar.chips.screenrecord.ui.view
+
+import android.content.DialogInterface
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.screenrecord.data.repository.screenRecordRepository
+import com.android.systemui.statusbar.chips.ui.viewmodel.screenRecordChipInteractor
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class EndScreenRecordingDialogDelegateTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+
+ private val sysuiDialog = mock<SystemUIDialog>()
+ private val sysuiDialogFactory = kosmos.mockSystemUIDialogFactory
+
+ private val underTest =
+ EndScreenRecordingDialogDelegate(
+ sysuiDialogFactory,
+ kosmos.screenRecordChipInteractor,
+ )
+
+ @Before
+ fun setUp() {
+ whenever(sysuiDialogFactory.create(eq(underTest), eq(context))).thenReturn(sysuiDialog)
+ }
+
+ @Test
+ fun icon() {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(sysuiDialog).setIcon(R.drawable.ic_screenrecord)
+ }
+
+ @Test
+ fun title() {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(sysuiDialog).setTitle(R.string.screenrecord_stop_dialog_title)
+ }
+
+ @Test
+ fun message() {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(sysuiDialog).setMessage(R.string.screenrecord_stop_dialog_message)
+ }
+
+ @Test
+ fun negativeButton() {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(sysuiDialog).setNegativeButton(R.string.close_dialog_button, null)
+ }
+
+ @Test
+ fun positiveButton() =
+ kosmos.testScope.runTest {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ val clickListener = argumentCaptor<DialogInterface.OnClickListener>()
+
+ // Verify the button has the right text
+ verify(sysuiDialog)
+ .setPositiveButton(
+ eq(R.string.screenrecord_stop_dialog_button),
+ clickListener.capture()
+ )
+
+ // Verify that clicking the button stops the recording
+ assertThat(kosmos.screenRecordRepository.stopRecordingInvoked).isFalse()
+
+ clickListener.firstValue.onClick(mock<DialogInterface>(), 0)
+ runCurrent()
+
+ assertThat(kosmos.screenRecordRepository.stopRecordingInvoked).isTrue()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index 121229c..6712963 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -30,8 +30,11 @@
import com.android.systemui.screenrecord.data.model.ScreenRecordModel
import com.android.systemui.screenrecord.data.repository.screenRecordRepository
import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
@SmallTest
@@ -46,6 +49,11 @@
private val underTest = kosmos.ongoingActivityChipsViewModel
+ @Before
+ fun setUp() {
+ setUpPackageManagerForMediaProjection(kosmos)
+ }
+
@Test
fun chip_allHidden_hidden() =
testScope.runTest {
@@ -91,7 +99,8 @@
fun chip_screenRecordShowAndMediaProjectionShow_screenRecordShown() =
testScope.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- mediaProjectionState.value = MediaProjectionState.EntireScreen
+ mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
callState.value = OngoingActivityChipModel.Hidden
val latest by collectLastValue(underTest.chip)
@@ -103,7 +112,8 @@
fun chip_mediaProjectionShowAndCallShow_mediaProjectionShown() =
testScope.runTest {
screenRecordState.value = ScreenRecordModel.DoingNothing
- mediaProjectionState.value = MediaProjectionState.EntireScreen
+ mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
val callChip =
OngoingActivityChipModel.Shown(
Icon.Resource(R.drawable.ic_call, ContentDescription.Loaded("icon")),
@@ -113,7 +123,7 @@
val latest by collectLastValue(underTest.chip)
- assertIsMediaProjectionChip(latest)
+ assertIsShareToAppChip(latest)
}
@Test
@@ -152,10 +162,14 @@
assertThat(latest).isEqualTo(callChip)
// WHEN the higher priority media projection chip is added
- mediaProjectionState.value = MediaProjectionState.SingleTask(createTask(taskId = 1))
+ mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(
+ NORMAL_PACKAGE,
+ createTask(taskId = 1),
+ )
// THEN the higher priority media projection chip is used
- assertIsMediaProjectionChip(latest)
+ assertIsShareToAppChip(latest)
// WHEN the higher priority screen record chip is added
screenRecordState.value = ScreenRecordModel.Recording
@@ -169,7 +183,8 @@
testScope.runTest {
// WHEN all chips are active
screenRecordState.value = ScreenRecordModel.Recording
- mediaProjectionState.value = MediaProjectionState.EntireScreen
+ mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
val callChip =
OngoingActivityChipModel.Shown(
@@ -187,7 +202,7 @@
screenRecordState.value = ScreenRecordModel.DoingNothing
// THEN the lower priority media projection is used
- assertIsMediaProjectionChip(latest)
+ assertIsShareToAppChip(latest)
// WHEN the higher priority media projection is removed
mediaProjectionState.value = MediaProjectionState.NotProjecting
@@ -200,13 +215,13 @@
fun assertIsScreenRecordChip(latest: OngoingActivityChipModel?) {
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
val icon = (latest as OngoingActivityChipModel.Shown).icon
- assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.stat_sys_screen_record)
+ assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenrecord)
}
- fun assertIsMediaProjectionChip(latest: OngoingActivityChipModel?) {
+ fun assertIsShareToAppChip(latest: OngoingActivityChipModel?) {
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
val icon = (latest as OngoingActivityChipModel.Shown).icon
- assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected)
+ assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenshot_share)
}
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
index c9fe449..cdb2b88 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
@@ -37,8 +37,10 @@
import com.android.systemui.screenrecord.data.model.ScreenRecordModel
import com.android.systemui.screenrecord.data.repository.screenRecordRepository
import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
-import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsMediaProjectionChip
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsScreenRecordChip
+import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsShareToAppChip
import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel
import com.android.systemui.statusbar.data.model.StatusBarMode
import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository.Companion.DISPLAY_ID
@@ -55,6 +57,7 @@
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
@SmallTest
@@ -77,6 +80,11 @@
kosmos.applicationCoroutineScope,
)
+ @Before
+ fun setUp() {
+ setUpPackageManagerForMediaProjection(kosmos)
+ }
+
@Test
fun isTransitioningFromLockscreenToOccluded_started_isTrue() =
testScope.runTest {
@@ -405,9 +413,9 @@
assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden)
kosmos.fakeMediaProjectionRepository.mediaProjectionState.value =
- MediaProjectionState.EntireScreen
+ MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- assertIsMediaProjectionChip(latest)
+ assertIsShareToAppChip(latest)
}
private fun activeNotificationsStore(notifications: List<ActiveNotificationModel>) =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt
index 62e56be..976a19c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt
@@ -20,6 +20,7 @@
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.mockito.mock
val Kosmos.dialogTransitionAnimator by Fixture {
fakeDialogTransitionAnimator(
@@ -29,3 +30,5 @@
interactionJankMonitor = interactionJankMonitor,
)
}
+
+val Kosmos.mockDialogTransitionAnimator by Fixture { mock<DialogTransitionAnimator>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/FakeMediaProjectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/FakeMediaProjectionRepository.kt
index c4365c9..d631f92 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/FakeMediaProjectionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/FakeMediaProjectionRepository.kt
@@ -25,4 +25,10 @@
override val mediaProjectionState: MutableStateFlow<MediaProjectionState> =
MutableStateFlow(MediaProjectionState.NotProjecting)
+
+ var stopProjectingInvoked = false
+
+ override suspend fun stopProjecting() {
+ stopProjectingInvoked = true
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/screenrecord/data/repository/FakeScreenRecordRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/screenrecord/data/repository/FakeScreenRecordRepository.kt
index fb0e368..30b4763 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/screenrecord/data/repository/FakeScreenRecordRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/screenrecord/data/repository/FakeScreenRecordRepository.kt
@@ -22,4 +22,10 @@
class FakeScreenRecordRepository : ScreenRecordRepository {
override val screenRecordState: MutableStateFlow<ScreenRecordModel> =
MutableStateFlow(ScreenRecordModel.DoingNothing)
+
+ var stopRecordingInvoked = false
+
+ override suspend fun stopRecording() {
+ stopRecordingInvoked = true
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorKosmos.kt
new file mode 100644
index 0000000..062b448
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorKosmos.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 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.statusbar.chips.mediaprojection.domain.interactor
+
+import android.content.packageManager
+import com.android.systemui.animation.mockDialogTransitionAnimator
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
+import com.android.systemui.util.time.fakeSystemClock
+
+val Kosmos.mediaProjectionChipInteractor: MediaProjectionChipInteractor by
+ Kosmos.Fixture {
+ MediaProjectionChipInteractor(
+ scope = applicationCoroutineScope,
+ mediaProjectionRepository = fakeMediaProjectionRepository,
+ packageManager = packageManager,
+ systemClock = fakeSystemClock,
+ dialogFactory = mockSystemUIDialogFactory,
+ dialogTransitionAnimator = mockDialogTransitionAnimator,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt
index 88bde2e..51ec540 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt
@@ -16,13 +16,14 @@
package com.android.systemui.statusbar.chips.ui.viewmodel
+import com.android.systemui.animation.mockDialogTransitionAnimator
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testScope
-import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
import com.android.systemui.screenrecord.data.repository.screenRecordRepository
-import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor
import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
import com.android.systemui.util.time.fakeSystemClock
val Kosmos.screenRecordChipInteractor: ScreenRecordChipInteractor by
@@ -30,15 +31,8 @@
ScreenRecordChipInteractor(
scope = applicationCoroutineScope,
screenRecordRepository = screenRecordRepository,
- systemClock = fakeSystemClock,
- )
- }
-
-val Kosmos.mediaProjectionChipInteractor: MediaProjectionChipInteractor by
- Kosmos.Fixture {
- MediaProjectionChipInteractor(
- scope = applicationCoroutineScope,
- mediaProjectionRepository = fakeMediaProjectionRepository,
+ dialogFactory = mockSystemUIDialogFactory,
+ dialogTransitionAnimator = mockDialogTransitionAnimator,
systemClock = fakeSystemClock,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt
index 3bb9580..1851c89 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt
@@ -21,8 +21,9 @@
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.model.sysUiState
+import com.android.systemui.util.mockito.mock
-val Kosmos.systemUIDialogFactory by
+val Kosmos.systemUIDialogFactory: SystemUIDialogFactory by
Kosmos.Fixture {
SystemUIDialogFactory(
applicationContext,
@@ -32,3 +33,6 @@
dialogTransitionAnimator,
)
}
+
+val Kosmos.mockSystemUIDialogFactory: SystemUIDialog.Factory by
+ Kosmos.Fixture { mock<SystemUIDialog.Factory>() }