[SB][Screen Chips] If sharing a single app, show app name in stop dialog
This CL updates the share-to-app and cast-to-other-device dialogs to
show "You will stop sharing [app name]" if you chose to share just a
single app at the beginning. We need to do this for screen recording
too, but that's more complicated because RecordingController doesn't
have this information yet, so that'll come in another CL.
Bug: 332662551
Flag: com.android.systemui.status_bar_screen_sharing_chips
Test: Start a share-to-app session with a specific app -> click chip ->
verify dialog says "You will stop sharing [app name]", with [app name]
in bold
Test: Start a cast-to-other-device session with a specific app -> click
chip -> verify dialog says "You will stop casting [app name]", with [app
name] in bold
Test: atest MediaProjectionChipInteractorTest
Test: atest EndCastToOtherDeviceDialogDelegateTest
EndShareToAppDialogDelegateTest EndMediaProjectionDialogHelperTest
Change-Id: Ia4aa6132b342127b4b9202ecd40d561c0fb882c2
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 3570d56..32a4c16 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -330,6 +330,8 @@
<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>
+ <!-- Text telling a user that they will stop sharing the contents of the specified [app_name] if they click the "Stop sharing" button. Note that the app name will appear in bold. [CHAR LIMIT=100] -->
+ <string name="share_to_app_stop_dialog_message_specific_app">You will stop sharing <b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g></b></string>
<!-- Button to stop screen sharing [CHAR LIMIT=35] -->
<string name="share_to_app_stop_dialog_button">Stop sharing</string>
@@ -337,6 +339,8 @@
<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>
+ <!-- Text telling a user that they will stop casting the contents of the specified [app_name] to a different device if they click the "Stop casting" button. Note that the app name will appear in bold. [CHAR LIMIT=100] -->
+ <string name="cast_to_other_device_stop_dialog_message_specific_app">You will stop casting <b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g></b></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>
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 6611434..f6fbe38 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
@@ -30,8 +30,8 @@
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.EndMediaProjectionDialogHelper
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
@@ -60,8 +60,8 @@
private val mediaProjectionRepository: MediaProjectionRepository,
private val packageManager: PackageManager,
private val systemClock: SystemClock,
- private val dialogFactory: SystemUIDialog.Factory,
private val dialogTransitionAnimator: DialogTransitionAnimator,
+ private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
) : OngoingActivityChipInteractor {
override val chip: StateFlow<OngoingActivityChipModel> =
mediaProjectionRepository.mediaProjectionState
@@ -70,9 +70,9 @@
is MediaProjectionState.NotProjecting -> OngoingActivityChipModel.Hidden
is MediaProjectionState.Projecting -> {
if (isProjectionToOtherDevice(state.hostPackage)) {
- createCastToOtherDeviceChip()
+ createCastToOtherDeviceChip(state)
} else {
- createShareToAppChip()
+ createShareToAppChip(state)
}
}
}
@@ -97,7 +97,9 @@
return Utils.isHeadlessRemoteDisplayProvider(packageManager, packageName)
}
- private fun createCastToOtherDeviceChip(): OngoingActivityChipModel.Shown {
+ private fun createCastToOtherDeviceChip(
+ state: MediaProjectionState.Projecting,
+ ): OngoingActivityChipModel.Shown {
return OngoingActivityChipModel.Shown(
icon =
Icon.Resource(
@@ -107,32 +109,39 @@
// TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
startTimeMs = systemClock.elapsedRealtime(),
createDialogLaunchOnClickListener(
- castToOtherDeviceDialogDelegate,
+ createCastToOtherDeviceDialogDelegate(state),
dialogTransitionAnimator,
),
)
}
- private val castToOtherDeviceDialogDelegate =
+ private fun createCastToOtherDeviceDialogDelegate(state: MediaProjectionState.Projecting) =
EndCastToOtherDeviceDialogDelegate(
- dialogFactory,
+ endMediaProjectionDialogHelper,
this@MediaProjectionChipInteractor,
+ state,
)
- private fun createShareToAppChip(): OngoingActivityChipModel.Shown {
+ private fun createShareToAppChip(
+ state: MediaProjectionState.Projecting,
+ ): 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),
+ createDialogLaunchOnClickListener(
+ createShareToAppDialogDelegate(state),
+ dialogTransitionAnimator
+ ),
)
}
- private val shareToAppDialogDelegate =
+ private fun createShareToAppDialogDelegate(state: MediaProjectionState.Projecting) =
EndShareToAppDialogDelegate(
- dialogFactory,
+ endMediaProjectionDialogHelper,
this@MediaProjectionChipInteractor,
+ state,
)
companion object {
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
index 33cec97..596fbf8 100644
--- 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
@@ -17,25 +17,33 @@
package com.android.systemui.statusbar.chips.mediaprojection.ui.view
import android.os.Bundle
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
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 endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
private val interactor: MediaProjectionChipInteractor,
+ private val state: MediaProjectionState.Projecting,
) : SystemUIDialog.Delegate {
override fun createDialog(): SystemUIDialog {
- return systemUIDialogFactory.create(this)
+ return endMediaProjectionDialogHelper.createDialog(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)
+ setMessage(
+ endMediaProjectionDialogHelper.getDialogMessage(
+ state,
+ genericMessageResId = R.string.cast_to_other_device_stop_dialog_message,
+ specificAppMessageResId =
+ R.string.cast_to_other_device_stop_dialog_message_specific_app,
+ )
+ )
// 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)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt
new file mode 100644
index 0000000..347be02
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.annotation.StringRes
+import android.content.Context
+import android.content.pm.PackageManager
+import android.text.Html
+import android.text.Html.FROM_HTML_MODE_LEGACY
+import android.text.TextUtils
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import javax.inject.Inject
+
+/** Helper class for showing dialogs that let users end different types of media projections. */
+@SysUISingleton
+class EndMediaProjectionDialogHelper
+@Inject
+constructor(
+ private val dialogFactory: SystemUIDialog.Factory,
+ private val packageManager: PackageManager,
+ private val context: Context
+) {
+ /** Creates a new [SystemUIDialog] using the given delegate. */
+ fun createDialog(delegate: SystemUIDialog.Delegate): SystemUIDialog {
+ return dialogFactory.create(delegate)
+ }
+
+ /**
+ * Returns the message to show in the dialog based on the specific media projection state.
+ *
+ * @param genericMessageResId a res ID for a more generic "end projection" message
+ * @param specificAppMessageResId a res ID for an "end projection" message that also lets us
+ * specify which app is currently being projected.
+ */
+ fun getDialogMessage(
+ state: MediaProjectionState.Projecting,
+ @StringRes genericMessageResId: Int,
+ @StringRes specificAppMessageResId: Int,
+ ): CharSequence {
+ when (state) {
+ is MediaProjectionState.Projecting.EntireScreen ->
+ return context.getString(genericMessageResId)
+ is MediaProjectionState.Projecting.SingleTask -> {
+ val packageName =
+ state.task.baseIntent.component?.packageName
+ ?: return context.getString(genericMessageResId)
+ try {
+ val appInfo = packageManager.getApplicationInfo(packageName, 0)
+ val appName = appInfo.loadLabel(packageManager)
+ return getSpecificAppMessageText(specificAppMessageResId, appName)
+ } catch (e: PackageManager.NameNotFoundException) {
+ // TODO(b/332662551): Log this error.
+ return context.getString(genericMessageResId)
+ }
+ }
+ }
+ }
+
+ private fun getSpecificAppMessageText(
+ @StringRes specificAppMessageResId: Int,
+ appName: CharSequence,
+ ): CharSequence {
+ // https://developer.android.com/guide/topics/resources/string-resource#StylingWithHTML
+ val escapedAppName = TextUtils.htmlEncode(appName.toString())
+ val text = context.getString(specificAppMessageResId, escapedAppName)
+ return Html.fromHtml(text, FROM_HTML_MODE_LEGACY)
+ }
+}
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
index 3a863b1..749a11f 100644
--- 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
@@ -17,25 +17,32 @@
package com.android.systemui.statusbar.chips.mediaprojection.ui.view
import android.os.Bundle
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
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 endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
private val interactor: MediaProjectionChipInteractor,
+ private val state: MediaProjectionState.Projecting,
) : SystemUIDialog.Delegate {
override fun createDialog(): SystemUIDialog {
- return systemUIDialogFactory.create(this)
+ return endMediaProjectionDialogHelper.createDialog(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)
+ setMessage(
+ endMediaProjectionDialogHelper.getDialogMessage(
+ state,
+ genericMessageResId = R.string.share_to_app_stop_dialog_message,
+ specificAppMessageResId = R.string.share_to_app_stop_dialog_message_specific_app
+ )
+ )
// 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)
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 a4505a9..327eec4 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
@@ -28,6 +28,7 @@
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testScope
import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
@@ -55,7 +56,7 @@
@SmallTest
class MediaProjectionChipInteractorTest : SysuiTestCase() {
- private val kosmos = Kosmos()
+ private val kosmos = Kosmos().also { it.testCase = this }
private val testScope = kosmos.testScope
private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository
private val systemClock = kosmos.fakeSystemClock
@@ -178,7 +179,7 @@
}
@Test
- fun chip_castToOtherDevice_clickListenerShowsCastDialog() =
+ fun chip_castToOtherDevice_entireScreen_clickListenerShowsCastDialog() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
mediaProjectionRepo.mediaProjectionState.value =
@@ -200,7 +201,33 @@
}
@Test
- fun chip_shareToApp_clickListenerShowsShareDialog() =
+ fun chip_castToOtherDevice_singleTask_clickListenerShowsCastDialog() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(
+ CAST_TO_OTHER_DEVICES_PACKAGE,
+ createTask(taskId = 1)
+ )
+
+ 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_entireScreen_clickListenerShowsShareDialog() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
mediaProjectionRepo.mediaProjectionState.value =
@@ -221,6 +248,28 @@
}
}
+ @Test
+ fun chip_shareToApp_singleTask_clickListenerShowsShareDialog() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(NORMAL_PACKAGE, createTask(taskId = 1))
+
+ 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"
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
index 9a2f545..7b676e2 100644
--- 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
@@ -16,22 +16,28 @@
package com.android.systemui.statusbar.chips.mediaprojection.ui.view
+import android.content.ComponentName
import android.content.DialogInterface
+import android.content.Intent
+import android.content.packageManager
+import android.content.pm.ApplicationInfo
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
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.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
@@ -41,22 +47,14 @@
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
class EndCastToOtherDeviceDialogDelegateTest : SysuiTestCase() {
- private val kosmos = Kosmos()
+ private val kosmos = Kosmos().also { it.testCase = this }
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)
- }
+ private lateinit var underTest: EndCastToOtherDeviceDialogDelegate
@Test
fun icon() {
+ createAndSetDelegate(ENTIRE_SCREEN)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
verify(sysuiDialog).setIcon(R.drawable.ic_cast_connected)
@@ -64,20 +62,52 @@
@Test
fun title() {
+ createAndSetDelegate(SINGLE_TASK)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
verify(sysuiDialog).setTitle(R.string.cast_to_other_device_stop_dialog_title)
}
@Test
- fun message() {
+ fun message_entireScreen() {
+ createAndSetDelegate(ENTIRE_SCREEN)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
- verify(sysuiDialog).setMessage(R.string.cast_to_other_device_stop_dialog_message)
+ verify(sysuiDialog)
+ .setMessage(context.getString(R.string.cast_to_other_device_stop_dialog_message))
+ }
+
+ @Test
+ fun message_singleTask() {
+ val baseIntent =
+ Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
+ val appInfo = mock<ApplicationInfo>()
+ whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake Package")
+ whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
+ .thenReturn(appInfo)
+
+ createAndSetDelegate(
+ MediaProjectionState.Projecting.SingleTask(
+ HOST_PACKAGE,
+ createTask(taskId = 1, baseIntent = baseIntent)
+ )
+ )
+
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ // It'd be nice to use R.string.cast_to_other_device_stop_dialog_message_specific_app
+ // directly, but it includes the <b> tags which aren't in the returned string.
+ val result = argumentCaptor<CharSequence>()
+ verify(sysuiDialog).setMessage(result.capture())
+ assertThat(result.firstValue.toString()).isEqualTo("You will stop casting Fake Package")
}
@Test
fun negativeButton() {
+ createAndSetDelegate(ENTIRE_SCREEN)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
verify(sysuiDialog).setNegativeButton(R.string.close_dialog_button, null)
@@ -86,6 +116,8 @@
@Test
fun positiveButton() =
kosmos.testScope.runTest {
+ createAndSetDelegate(SINGLE_TASK)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
val clickListener = argumentCaptor<DialogInterface.OnClickListener>()
@@ -105,4 +137,20 @@
assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue()
}
+
+ private fun createAndSetDelegate(state: MediaProjectionState.Projecting) {
+ underTest =
+ EndCastToOtherDeviceDialogDelegate(
+ kosmos.endMediaProjectionDialogHelper,
+ kosmos.mediaProjectionChipInteractor,
+ state,
+ )
+ }
+
+ companion object {
+ private const val HOST_PACKAGE = "fake.host.package"
+ private val ENTIRE_SCREEN = MediaProjectionState.Projecting.EntireScreen(HOST_PACKAGE)
+ private val SINGLE_TASK =
+ MediaProjectionState.Projecting.SingleTask(HOST_PACKAGE, createTask(taskId = 1))
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
new file mode 100644
index 0000000..bbd1109
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
@@ -0,0 +1,144 @@
+/*
+ * 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.ComponentName
+import android.content.Intent
+import android.content.packageManager
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.res.R
+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 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 EndMediaProjectionDialogHelperTest : SysuiTestCase() {
+ private val kosmos = Kosmos().also { it.testCase = this }
+
+ private val underTest = kosmos.endMediaProjectionDialogHelper
+
+ @Test
+ fun createDialog_usesDelegateAndFactory() {
+ val dialog = mock<SystemUIDialog>()
+ val delegate = SystemUIDialog.Delegate { dialog }
+ whenever(kosmos.mockSystemUIDialogFactory.create(eq(delegate))).thenReturn(dialog)
+
+ underTest.createDialog(delegate)
+
+ verify(kosmos.mockSystemUIDialogFactory).create(delegate)
+ }
+
+ @Test
+ fun getDialogMessage_entireScreen_isGenericMessage() {
+ val result =
+ underTest.getDialogMessage(
+ MediaProjectionState.Projecting.EntireScreen("host.package"),
+ R.string.accessibility_home,
+ R.string.cast_to_other_device_stop_dialog_message_specific_app
+ )
+
+ assertThat(result).isEqualTo(context.getString(R.string.accessibility_home))
+ }
+
+ @Test
+ fun getDialogMessage_singleTask_cannotFindPackage_isGenericMessage() {
+ val baseIntent =
+ Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
+ whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
+ .thenThrow(PackageManager.NameNotFoundException())
+
+ val projectionState =
+ MediaProjectionState.Projecting.SingleTask(
+ "host.package",
+ createTask(taskId = 1, baseIntent = baseIntent)
+ )
+
+ val result =
+ underTest.getDialogMessage(
+ projectionState,
+ R.string.accessibility_home,
+ R.string.cast_to_other_device_stop_dialog_message_specific_app
+ )
+
+ assertThat(result).isEqualTo(context.getString(R.string.accessibility_home))
+ }
+
+ @Test
+ fun getDialogMessage_singleTask_findsPackage_isSpecificMessageWithAppLabel() {
+ val baseIntent =
+ Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
+ val appInfo = mock<ApplicationInfo>()
+ whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake Package")
+ whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
+ .thenReturn(appInfo)
+
+ val projectionState =
+ MediaProjectionState.Projecting.SingleTask(
+ "host.package",
+ createTask(taskId = 1, baseIntent = baseIntent)
+ )
+
+ val result =
+ underTest.getDialogMessage(
+ projectionState,
+ R.string.accessibility_home,
+ R.string.cast_to_other_device_stop_dialog_message_specific_app
+ )
+
+ // It'd be nice to use the R.string resources directly, but they include the <b> tags which
+ // aren't in the returned string.
+ assertThat(result.toString()).isEqualTo("You will stop casting Fake Package")
+ }
+
+ @Test
+ fun getDialogMessage_appLabelHasSpecialCharacters_isEscaped() {
+ val baseIntent =
+ Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
+ val appInfo = mock<ApplicationInfo>()
+ whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake & Package <Here>")
+ whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
+ .thenReturn(appInfo)
+
+ val projectionState =
+ MediaProjectionState.Projecting.SingleTask(
+ "host.package",
+ createTask(taskId = 1, baseIntent = baseIntent)
+ )
+
+ val result =
+ underTest.getDialogMessage(
+ projectionState,
+ R.string.accessibility_home,
+ R.string.cast_to_other_device_stop_dialog_message_specific_app
+ )
+
+ assertThat(result.toString()).isEqualTo("You will stop casting Fake & Package <Here>")
+ }
+}
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
index 1d6e866..4ddca52 100644
--- 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
@@ -16,22 +16,28 @@
package com.android.systemui.statusbar.chips.mediaprojection.ui.view
+import android.content.ComponentName
import android.content.DialogInterface
+import android.content.Intent
+import android.content.packageManager
+import android.content.pm.ApplicationInfo
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
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.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
@@ -41,22 +47,14 @@
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
class EndShareToAppDialogDelegateTest : SysuiTestCase() {
- private val kosmos = Kosmos()
+ private val kosmos = Kosmos().also { it.testCase = this }
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)
- }
+ private lateinit var underTest: EndShareToAppDialogDelegate
@Test
fun icon() {
+ createAndSetDelegate(SINGLE_TASK)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
verify(sysuiDialog).setIcon(R.drawable.ic_screenshot_share)
@@ -64,20 +62,51 @@
@Test
fun title() {
+ createAndSetDelegate(ENTIRE_SCREEN)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
verify(sysuiDialog).setTitle(R.string.share_to_app_stop_dialog_title)
}
@Test
- fun message() {
+ fun message_entireScreen() {
+ createAndSetDelegate(ENTIRE_SCREEN)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
- verify(sysuiDialog).setMessage(R.string.share_to_app_stop_dialog_message)
+ verify(sysuiDialog).setMessage(context.getString(R.string.share_to_app_stop_dialog_message))
+ }
+
+ @Test
+ fun message_singleTask() {
+ val baseIntent =
+ Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
+ val appInfo = mock<ApplicationInfo>()
+ whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake Package")
+ whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
+ .thenReturn(appInfo)
+
+ createAndSetDelegate(
+ MediaProjectionState.Projecting.SingleTask(
+ HOST_PACKAGE,
+ createTask(taskId = 1, baseIntent = baseIntent)
+ )
+ )
+
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ // It'd be nice to use R.string.share_to_app_stop_dialog_message_specific_app directly, but
+ // it includes the <b> tags which aren't in the returned string.
+ val result = argumentCaptor<CharSequence>()
+ verify(sysuiDialog).setMessage(result.capture())
+ assertThat(result.firstValue.toString()).isEqualTo("You will stop sharing Fake Package")
}
@Test
fun negativeButton() {
+ createAndSetDelegate(SINGLE_TASK)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
verify(sysuiDialog).setNegativeButton(R.string.close_dialog_button, null)
@@ -86,6 +115,8 @@
@Test
fun positiveButton() =
kosmos.testScope.runTest {
+ createAndSetDelegate(ENTIRE_SCREEN)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
val clickListener = argumentCaptor<DialogInterface.OnClickListener>()
@@ -105,4 +136,20 @@
assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue()
}
+
+ private fun createAndSetDelegate(state: MediaProjectionState.Projecting) {
+ underTest =
+ EndShareToAppDialogDelegate(
+ kosmos.endMediaProjectionDialogHelper,
+ kosmos.mediaProjectionChipInteractor,
+ state,
+ )
+ }
+
+ companion object {
+ private const val HOST_PACKAGE = "fake.host.package"
+ private val ENTIRE_SCREEN = MediaProjectionState.Projecting.EntireScreen(HOST_PACKAGE)
+ private val SINGLE_TASK =
+ MediaProjectionState.Projecting.SingleTask(HOST_PACKAGE, createTask(taskId = 1))
+ }
}
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 6712963..65bf0bc 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
@@ -22,6 +22,7 @@
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testScope
import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
@@ -39,8 +40,7 @@
@SmallTest
class OngoingActivityChipsViewModelTest : SysuiTestCase() {
-
- private val kosmos = Kosmos()
+ private val kosmos = Kosmos().also { it.testCase = this }
private val testScope = kosmos.testScope
private val screenRecordState = kosmos.screenRecordRepository.screenRecordState
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 cdb2b88..b8299e5 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
@@ -29,6 +29,7 @@
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.assertLogsWtf
@@ -63,7 +64,10 @@
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
class CollapsedStatusBarViewModelImplTest : SysuiTestCase() {
- private val kosmos = Kosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
+ private val kosmos = Kosmos().also {
+ it.testCase = this
+ it.testDispatcher = UnconfinedTestDispatcher()
+ }
private val testScope = kosmos.testScope
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
index 062b448..9d22811 100644
--- 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
@@ -21,7 +21,7 @@
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.statusbar.chips.mediaprojection.ui.view.endMediaProjectionDialogHelper
import com.android.systemui.util.time.fakeSystemClock
val Kosmos.mediaProjectionChipInteractor: MediaProjectionChipInteractor by
@@ -31,7 +31,7 @@
mediaProjectionRepository = fakeMediaProjectionRepository,
packageManager = packageManager,
systemClock = fakeSystemClock,
- dialogFactory = mockSystemUIDialogFactory,
+ endMediaProjectionDialogHelper = endMediaProjectionDialogHelper,
dialogTransitionAnimator = mockDialogTransitionAnimator,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt
new file mode 100644
index 0000000..4f82662
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.applicationContext
+import android.content.packageManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
+
+val Kosmos.endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper by
+ Kosmos.Fixture {
+ EndMediaProjectionDialogHelper(
+ dialogFactory = mockSystemUIDialogFactory,
+ packageManager = packageManager,
+ context = applicationContext,
+ )
+ }