[Screen share] Update our status to "projecting" earlier in process.
In the old screen share icon, we started showing the icon as soon as
MediaProjectionManager.Callback#onStart is called. With the new screen
share chips, we only started showing the icon when
MediaProjectionManager.Callback#onRecordingSessionSet is called. If a
user is only sharing *audio* to an app, but not their screen,
the #onRecordingSessionSet method is never triggered but we should still
show the screen share chip.
Bug: 373308507
Bug: 332662551
Flag: com.android.systemui.status_bar_show_audio_only_projection_chip
Regression tests:
Test: Screen record single app -> verify chip does 3-2-1 countdown then
starts timer, has screen record icon
Test: Screen record full screen -> verify chip does 3-2-1 countdown then
starts timer, has screen record icon
Test: Share screen to app that does a 3-2-1 countdown -> verify chip
starts showing with just share-to-app icon, then shows timer after 3-2-1
countdown is done
Test: Share screen to app that immediately starts recording -> verify
chip timer immediately starts
Test: Cast screen to another device -> verify chip shows cast icon +
timer
Test: Cast audio to another device -> verify chip just shows cast icon
Test: atest MediaProjectionManagerRepositoryTest
New test:
Test: Share *audio* to another app -> verify chip shows, and has
share-to-app icon
Change-Id: Ifa84ec811ef3456792a3cd6b7766d6c738536af2
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index a21a805..bd872eb 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -338,6 +338,17 @@
}
flag {
+ name: "status_bar_show_audio_only_projection_chip"
+ namespace: "systemui"
+ description: "Show chip on the left side of the status bar when a user is only sharing *audio* "
+ "during a media projection"
+ bug: "373308507"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "status_bar_use_repos_for_call_chip"
namespace: "systemui"
description: "Use repositories as the source of truth for call notifications shown as a chip in"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
index 785d5a8..02825a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
@@ -21,10 +21,13 @@
import android.os.Binder
import android.os.Handler
import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.view.ContentRecordingSession
import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.applicationCoroutineScope
@@ -74,7 +77,8 @@
}
@Test
- fun mediaProjectionState_onStart_emitsNotProjecting() =
+ @DisableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun mediaProjectionState_onStart_flagOff_emitsNotProjecting() =
testScope.runTest {
val state by collectLastValue(repo.mediaProjectionState)
@@ -84,6 +88,35 @@
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun mediaProjectionState_onStart_flagOn_emitsProjectingNoScreen() =
+ testScope.runTest {
+ val state by collectLastValue(repo.mediaProjectionState)
+
+ fakeMediaProjectionManager.dispatchOnStart()
+
+ assertThat(state).isInstanceOf(MediaProjectionState.Projecting.NoScreen::class.java)
+ }
+
+ @Test
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun mediaProjectionState_noScreen_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.dispatchOnStart(info)
+
+ assertThat((state as MediaProjectionState.Projecting).hostPackage)
+ .isEqualTo("com.media.projection.repository.test")
+ }
+
+ @Test
fun mediaProjectionState_onStop_emitsNotProjecting() =
testScope.runTest {
val state by collectLastValue(repo.mediaProjectionState)
@@ -212,7 +245,7 @@
)
fakeMediaProjectionManager.dispatchOnSessionSet(
info = info,
- session = ContentRecordingSession.createTaskSession(token.asBinder())
+ session = ContentRecordingSession.createTaskSession(token.asBinder()),
)
assertThat((state as MediaProjectionState.Projecting.SingleTask).hostPackage)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
index 5005d16..e33ce9c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
@@ -92,7 +92,7 @@
createAndSetDelegate(
MediaProjectionState.Projecting.EntireScreen(
HOST_PACKAGE,
- hostDeviceName = "My Favorite Device"
+ hostDeviceName = "My Favorite Device",
)
)
@@ -118,8 +118,8 @@
MediaProjectionState.Projecting.SingleTask(
HOST_PACKAGE,
hostDeviceName = null,
- createTask(taskId = 1, baseIntent = baseIntent)
- ),
+ createTask(taskId = 1, baseIntent = baseIntent),
+ )
)
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
@@ -141,8 +141,8 @@
MediaProjectionState.Projecting.SingleTask(
HOST_PACKAGE,
hostDeviceName = "My Favorite Device",
- createTask(taskId = 1, baseIntent = baseIntent)
- ),
+ createTask(taskId = 1, baseIntent = baseIntent),
+ )
)
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
@@ -169,8 +169,8 @@
MediaProjectionState.Projecting.SingleTask(
HOST_PACKAGE,
hostDeviceName = null,
- createTask(taskId = 1, baseIntent = baseIntent)
- ),
+ createTask(taskId = 1, baseIntent = baseIntent),
+ )
)
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
@@ -198,7 +198,7 @@
HOST_PACKAGE,
hostDeviceName = "My Favorite Device",
createTask(taskId = 1, baseIntent = baseIntent),
- ),
+ )
)
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
@@ -235,7 +235,7 @@
verify(sysuiDialog)
.setPositiveButton(
eq(R.string.cast_to_other_device_stop_dialog_button),
- clickListener.capture()
+ clickListener.capture(),
)
// Verify that clicking the button stops the recording
@@ -254,7 +254,8 @@
kosmos.applicationContext,
stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting,
ProjectionChipModel.Projecting(
- ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE,
+ ProjectionChipModel.Receiver.CastToOtherDevice,
+ ProjectionChipModel.ContentType.Screen,
state,
),
)
@@ -268,7 +269,7 @@
MediaProjectionState.Projecting.SingleTask(
HOST_PACKAGE,
hostDeviceName = null,
- createTask(taskId = 1)
+ createTask(taskId = 1),
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
index 77992db..01e5501 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
@@ -17,9 +17,11 @@
package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel
import android.content.DialogInterface
+import android.platform.test.annotations.EnableFlags
import android.view.View
import androidx.test.filters.SmallTest
import com.android.internal.jank.Cuj
+import com.android.systemui.Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.mockDialogTransitionAnimator
@@ -135,6 +137,29 @@
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun chip_projectionIsAudioOnly_otherDevicePackage_isShownAsIconOnly() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+ mediaRouterRepo.castDevices.value = emptyList()
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.NoScreen(
+ hostPackage = CAST_TO_OTHER_DEVICES_PACKAGE
+ )
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
+ val icon =
+ (((latest as OngoingActivityChipModel.Shown).icon)
+ as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
+ .impl as Icon.Resource
+ assertThat(icon.res).isEqualTo(R.drawable.ic_cast_connected)
+ // This content description is just generic "Casting", not "Casting screen"
+ assertThat((icon.contentDescription as ContentDescription.Resource).res)
+ .isEqualTo(R.string.accessibility_casting)
+ }
+
+ @Test
fun chip_projectionIsEntireScreenState_otherDevicesPackage_isShownAsTimer_forScreen() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -292,6 +317,18 @@
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun chip_projectionIsNoScreenState_normalPackage_isHidden() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.NoScreen(NORMAL_PACKAGE)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+ }
+
+ @Test
fun chip_projectionIsSingleTaskState_normalPackage_isHidden() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -387,12 +424,7 @@
clickListener!!.onClick(chipView)
verify(kosmos.mockDialogTransitionAnimator)
- .showFromView(
- eq(mockScreenCastDialog),
- eq(chipBackgroundView),
- any(),
- anyBoolean(),
- )
+ .showFromView(eq(mockScreenCastDialog), eq(chipBackgroundView), any(), anyBoolean())
}
@Test
@@ -412,12 +444,7 @@
clickListener!!.onClick(chipView)
verify(kosmos.mockDialogTransitionAnimator)
- .showFromView(
- eq(mockScreenCastDialog),
- eq(chipBackgroundView),
- any(),
- anyBoolean(),
- )
+ .showFromView(eq(mockScreenCastDialog), eq(chipBackgroundView), any(), anyBoolean())
}
@Test
@@ -461,12 +488,7 @@
val cujCaptor = argumentCaptor<DialogCuj>()
verify(kosmos.mockDialogTransitionAnimator)
- .showFromView(
- any(),
- any(),
- cujCaptor.capture(),
- anyBoolean(),
- )
+ .showFromView(any(), any(), cujCaptor.capture(), anyBoolean())
assertThat(cujCaptor.firstValue.cujType)
.isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
@@ -494,12 +516,7 @@
val cujCaptor = argumentCaptor<DialogCuj>()
verify(kosmos.mockDialogTransitionAnimator)
- .showFromView(
- any(),
- any(),
- cujCaptor.capture(),
- anyBoolean(),
- )
+ .showFromView(any(), any(), cujCaptor.capture(), anyBoolean())
assertThat(cujCaptor.firstValue.cujType)
.isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
index d0c5e7a..611318a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
@@ -21,7 +21,9 @@
import android.content.packageManager
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
+import android.platform.test.annotations.EnableFlags
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
@@ -65,7 +67,23 @@
}
@Test
- fun projection_singleTaskState_otherDevicesPackage_isCastToOtherDeviceType() =
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun projection_noScreenState_otherDevicesPackage_isCastToOtherAndAudio() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.projection)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.NoScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
+
+ assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
+ assertThat((latest as ProjectionChipModel.Projecting).receiver)
+ .isEqualTo(ProjectionChipModel.Receiver.CastToOtherDevice)
+ assertThat((latest as ProjectionChipModel.Projecting).contentType)
+ .isEqualTo(ProjectionChipModel.ContentType.Audio)
+ }
+
+ @Test
+ fun projection_singleTaskState_otherDevicesPackage_isCastToOtherAndScreen() =
testScope.runTest {
val latest by collectLastValue(underTest.projection)
@@ -73,31 +91,49 @@
MediaProjectionState.Projecting.SingleTask(
CAST_TO_OTHER_DEVICES_PACKAGE,
hostDeviceName = null,
- createTask(taskId = 1)
+ createTask(taskId = 1),
)
assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
- assertThat((latest as ProjectionChipModel.Projecting).type)
- .isEqualTo(ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE)
+ assertThat((latest as ProjectionChipModel.Projecting).receiver)
+ .isEqualTo(ProjectionChipModel.Receiver.CastToOtherDevice)
+ assertThat((latest as ProjectionChipModel.Projecting).contentType)
+ .isEqualTo(ProjectionChipModel.ContentType.Screen)
}
@Test
- fun projection_entireScreenState_otherDevicesPackage_isCastToOtherDeviceChipType() =
+ fun projection_entireScreenState_otherDevicesPackage_isCastToOtherAndScreen() =
testScope.runTest {
val latest by collectLastValue(underTest.projection)
mediaProjectionRepo.mediaProjectionState.value =
- MediaProjectionState.Projecting.EntireScreen(
- CAST_TO_OTHER_DEVICES_PACKAGE,
- )
+ MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
- assertThat((latest as ProjectionChipModel.Projecting).type)
- .isEqualTo(ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE)
+ assertThat((latest as ProjectionChipModel.Projecting).receiver)
+ .isEqualTo(ProjectionChipModel.Receiver.CastToOtherDevice)
+ assertThat((latest as ProjectionChipModel.Projecting).contentType)
+ .isEqualTo(ProjectionChipModel.ContentType.Screen)
}
@Test
- fun projection_singleTaskState_normalPackage_isShareToAppChipType() =
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun projection_noScreenState_normalPackage_isShareToAppAndAudio() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.projection)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.NoScreen(NORMAL_PACKAGE)
+
+ assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
+ assertThat((latest as ProjectionChipModel.Projecting).receiver)
+ .isEqualTo(ProjectionChipModel.Receiver.ShareToApp)
+ assertThat((latest as ProjectionChipModel.Projecting).contentType)
+ .isEqualTo(ProjectionChipModel.ContentType.Audio)
+ }
+
+ @Test
+ fun projection_singleTaskState_normalPackage_isShareToAppAndScreen() =
testScope.runTest {
val latest by collectLastValue(underTest.projection)
@@ -109,12 +145,14 @@
)
assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
- assertThat((latest as ProjectionChipModel.Projecting).type)
- .isEqualTo(ProjectionChipModel.Type.SHARE_TO_APP)
+ assertThat((latest as ProjectionChipModel.Projecting).receiver)
+ .isEqualTo(ProjectionChipModel.Receiver.ShareToApp)
+ assertThat((latest as ProjectionChipModel.Projecting).contentType)
+ .isEqualTo(ProjectionChipModel.ContentType.Screen)
}
@Test
- fun projection_entireScreenState_normalPackage_isShareToAppChipType() =
+ fun projection_entireScreenState_normalPackage_isShareToAppAndScreen() =
testScope.runTest {
val latest by collectLastValue(underTest.projection)
@@ -122,8 +160,10 @@
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
- assertThat((latest as ProjectionChipModel.Projecting).type)
- .isEqualTo(ProjectionChipModel.Type.SHARE_TO_APP)
+ assertThat((latest as ProjectionChipModel.Projecting).receiver)
+ .isEqualTo(ProjectionChipModel.Receiver.ShareToApp)
+ assertThat((latest as ProjectionChipModel.Projecting).contentType)
+ .isEqualTo(ProjectionChipModel.ContentType.Screen)
}
companion object {
@@ -140,14 +180,14 @@
whenever(
this.checkPermission(
Manifest.permission.REMOTE_DISPLAY_PROVIDER,
- CAST_TO_OTHER_DEVICES_PACKAGE
+ CAST_TO_OTHER_DEVICES_PACKAGE,
)
)
.thenReturn(PackageManager.PERMISSION_GRANTED)
whenever(
this.checkPermission(
Manifest.permission.REMOTE_DISPLAY_PROVIDER,
- NORMAL_PACKAGE
+ NORMAL_PACKAGE,
)
)
.thenReturn(PackageManager.PERMISSION_DENIED)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt
new file mode 100644
index 0000000..411d306
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.sharetoapp.ui.view
+
+import android.content.DialogInterface
+import android.content.applicationContext
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.endMediaProjectionDialogHelper
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.testKosmos
+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.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class EndGenericShareToAppDialogDelegateTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val sysuiDialog = mock<SystemUIDialog>()
+ private val underTest =
+ EndGenericShareToAppDialogDelegate(
+ kosmos.endMediaProjectionDialogHelper,
+ kosmos.applicationContext,
+ stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting,
+ )
+
+ @Test
+ fun positiveButton_clickStopsRecording() =
+ kosmos.testScope.runTest {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isFalse()
+
+ val clickListener = argumentCaptor<DialogInterface.OnClickListener>()
+ verify(sysuiDialog).setPositiveButton(any(), clickListener.capture())
+ clickListener.firstValue.onClick(mock<DialogInterface>(), 0)
+ runCurrent()
+
+ assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt
similarity index 93%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt
index 325a42b..6885a6b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt
@@ -50,10 +50,10 @@
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
-class EndShareToAppDialogDelegateTest : SysuiTestCase() {
+class EndShareScreenToAppDialogDelegateTest : SysuiTestCase() {
private val kosmos = Kosmos().also { it.testCase = this }
private val sysuiDialog = mock<SystemUIDialog>()
- private lateinit var underTest: EndShareToAppDialogDelegate
+ private lateinit var underTest: EndShareScreenToAppDialogDelegate
@Test
fun icon() {
@@ -117,7 +117,7 @@
MediaProjectionState.Projecting.SingleTask(
HOST_PACKAGE,
hostDeviceName = null,
- createTask(taskId = 1, baseIntent = baseIntent)
+ createTask(taskId = 1, baseIntent = baseIntent),
)
)
@@ -142,7 +142,7 @@
MediaProjectionState.Projecting.SingleTask(
HOST_PACKAGE,
hostDeviceName = null,
- createTask(taskId = 1, baseIntent = baseIntent)
+ createTask(taskId = 1, baseIntent = baseIntent),
)
)
@@ -181,7 +181,7 @@
verify(sysuiDialog)
.setPositiveButton(
eq(R.string.share_to_app_stop_dialog_button),
- clickListener.capture()
+ clickListener.capture(),
)
// Verify that clicking the button stops the recording
@@ -195,12 +195,13 @@
private fun createAndSetDelegate(state: MediaProjectionState.Projecting) {
underTest =
- EndShareToAppDialogDelegate(
+ EndShareScreenToAppDialogDelegate(
kosmos.endMediaProjectionDialogHelper,
kosmos.applicationContext,
stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting,
ProjectionChipModel.Projecting(
- ProjectionChipModel.Type.SHARE_TO_APP,
+ ProjectionChipModel.Receiver.ShareToApp,
+ ProjectionChipModel.ContentType.Screen,
state,
),
)
@@ -213,7 +214,7 @@
MediaProjectionState.Projecting.SingleTask(
HOST_PACKAGE,
hostDeviceName = null,
- createTask(taskId = 1)
+ createTask(taskId = 1),
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
index 791a21d..d7d57c8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
@@ -17,12 +17,15 @@
package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel
import android.content.DialogInterface
+import android.platform.test.annotations.EnableFlags
import android.view.View
import androidx.test.filters.SmallTest
import com.android.internal.jank.Cuj
+import com.android.systemui.Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.mockDialogTransitionAnimator
+import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
@@ -35,7 +38,8 @@
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.CAST_TO_OTHER_DEVICES_PACKAGE
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.sharetoapp.ui.view.EndShareToAppDialogDelegate
+import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndGenericShareToAppDialogDelegate
+import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareScreenToAppDialogDelegate
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
@@ -62,7 +66,8 @@
private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository
private val systemClock = kosmos.fakeSystemClock
- private val mockShareDialog = mock<SystemUIDialog>()
+ private val mockScreenShareDialog = mock<SystemUIDialog>()
+ private val mockGenericShareDialog = mock<SystemUIDialog>()
private val chipBackgroundView = mock<ChipBackgroundContainer>()
private val chipView =
mock<View>().apply {
@@ -80,8 +85,10 @@
fun setUp() {
setUpPackageManagerForMediaProjection(kosmos)
- whenever(kosmos.mockSystemUIDialogFactory.create(any<EndShareToAppDialogDelegate>()))
- .thenReturn(mockShareDialog)
+ whenever(kosmos.mockSystemUIDialogFactory.create(any<EndShareScreenToAppDialogDelegate>()))
+ .thenReturn(mockScreenShareDialog)
+ whenever(kosmos.mockSystemUIDialogFactory.create(any<EndGenericShareToAppDialogDelegate>()))
+ .thenReturn(mockGenericShareDialog)
}
@Test
@@ -95,6 +102,21 @@
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun chip_noScreenState_otherDevicesPackage_isHidden() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.NoScreen(
+ CAST_TO_OTHER_DEVICES_PACKAGE,
+ hostDeviceName = null,
+ )
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+ }
+
+ @Test
fun chip_singleTaskState_otherDevicesPackage_isHidden() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -121,6 +143,26 @@
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun chip_noScreenState_normalPackage_isShownAsIconOnly() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.NoScreen(NORMAL_PACKAGE, hostDeviceName = null)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
+ val icon =
+ (((latest as OngoingActivityChipModel.Shown).icon)
+ as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
+ .impl as Icon.Resource
+ assertThat(icon.res).isEqualTo(R.drawable.ic_present_to_all)
+ // This content description is just generic "Sharing content", not "Sharing screen"
+ assertThat((icon.contentDescription as ContentDescription.Resource).res)
+ .isEqualTo(R.string.share_to_app_chip_accessibility_label_generic)
+ }
+
+ @Test
fun chip_singleTaskState_normalPackage_isShownAsTimer() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -170,7 +212,7 @@
// WHEN the stop action on the dialog is clicked
val dialogStopAction =
- getStopActionFromDialog(latest, chipView, mockShareDialog, kosmos)
+ getStopActionFromDialog(latest, chipView, mockScreenShareDialog, kosmos)
dialogStopAction.onClick(mock<DialogInterface>(), 0)
// THEN the chip is immediately hidden...
@@ -222,7 +264,28 @@
}
@Test
- fun chip_entireScreen_clickListenerShowsShareDialog() =
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun chip_noScreen_clickListenerShowsGenericShareDialog() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.NoScreen(NORMAL_PACKAGE)
+
+ val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+ assertThat(clickListener).isNotNull()
+
+ clickListener!!.onClick(chipView)
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ eq(mockGenericShareDialog),
+ eq(chipBackgroundView),
+ any(),
+ anyBoolean(),
+ )
+ }
+
+ @Test
+ fun chip_entireScreen_clickListenerShowsScreenShareDialog() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
mediaProjectionRepo.mediaProjectionState.value =
@@ -234,7 +297,7 @@
clickListener!!.onClick(chipView)
verify(kosmos.mockDialogTransitionAnimator)
.showFromView(
- eq(mockShareDialog),
+ eq(mockScreenShareDialog),
eq(chipBackgroundView),
any(),
anyBoolean(),
@@ -242,7 +305,7 @@
}
@Test
- fun chip_singleTask_clickListenerShowsShareDialog() =
+ fun chip_singleTask_clickListenerShowsScreenShareDialog() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
mediaProjectionRepo.mediaProjectionState.value =
@@ -258,7 +321,7 @@
clickListener!!.onClick(chipView)
verify(kosmos.mockDialogTransitionAnimator)
.showFromView(
- eq(mockShareDialog),
+ eq(mockScreenShareDialog),
eq(chipBackgroundView),
any(),
anyBoolean(),
@@ -281,12 +344,7 @@
val cujCaptor = argumentCaptor<DialogCuj>()
verify(kosmos.mockDialogTransitionAnimator)
- .showFromView(
- any(),
- any(),
- cujCaptor.capture(),
- anyBoolean(),
- )
+ .showFromView(any(), any(), cujCaptor.capture(), anyBoolean())
assertThat(cujCaptor.firstValue.cujType)
.isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 2c5fb56..cf07abb 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -342,8 +342,12 @@
<!-- Content description for the status bar chip shown to the user when they're sharing their screen to another app on the device [CHAR LIMIT=NONE] -->
<string name="share_to_app_chip_accessibility_label">Sharing screen</string>
+ <!-- Content description for the status bar chip shown to the user when they're sharing their screen or audio to another app on the device [CHAR LIMIT=NONE] -->
+ <string name="share_to_app_chip_accessibility_label_generic">Sharing content</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>
+ <!-- Title for a dialog shown to the user that will let them stop sharing their screen or audio to another app on the device [CHAR LIMIT=50] -->
+ <string name="share_to_app_stop_dialog_title_generic">Stop sharing?</string>
<!-- Text telling a user that they're currently sharing their entire screen to [host_app_name] (i.e. [host_app_name] can currently see all screen content) [CHAR LIMIT=150] -->
<string name="share_to_app_stop_dialog_message_entire_screen_with_host_app">You\'re currently sharing your entire screen with <xliff:g id="host_app_name" example="Screen Recorder App">%1$s</xliff:g></string>
<!-- Text telling a user that they're currently sharing their entire screen to an app (but we don't know what app) [CHAR LIMIT=150] -->
@@ -352,6 +356,8 @@
<string name="share_to_app_stop_dialog_message_single_app_specific">You\'re currently sharing <xliff:g id="app_being_shared_name" example="Photos App">%1$s</xliff:g></string>
<!-- Text telling a user that they're currently sharing their screen [CHAR LIMIT=150] -->
<string name="share_to_app_stop_dialog_message_single_app_generic">You\'re currently sharing an app</string>
+ <!-- Text telling a user that they're currently sharing something to an app [CHAR LIMIT=100] -->
+ <string name="share_to_app_stop_dialog_message_generic">You\'re currently sharing with an app</string>
<!-- Button to stop screen sharing [CHAR LIMIT=35] -->
<string name="share_to_app_stop_dialog_button">Stop sharing</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 82b4825..2fa3405 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
@@ -32,10 +32,8 @@
* media projection. Null if the media projection is going to this same device (e.g. another
* app is recording the screen).
*/
- sealed class Projecting(
- open val hostPackage: String,
- open val hostDeviceName: String?,
- ) : MediaProjectionState {
+ sealed class Projecting(open val hostPackage: String, open val hostDeviceName: String?) :
+ MediaProjectionState {
/** The entire screen is being projected. */
data class EntireScreen(
override val hostPackage: String,
@@ -48,5 +46,11 @@
override val hostDeviceName: String?,
val task: RunningTaskInfo,
) : Projecting(hostPackage, hostDeviceName)
+
+ /** The screen is not being projected, only audio is being projected. */
+ data class NoScreen(
+ override val hostPackage: String,
+ override val hostDeviceName: String? = null,
+ ) : Projecting(hostPackage, hostDeviceName)
}
}
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 5704e80..35efd75 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
@@ -23,6 +23,7 @@
import android.os.Handler
import android.view.ContentRecordingSession
import android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY
+import com.android.systemui.Flags
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -94,7 +95,7 @@
{},
{ "MediaProjectionManager.Callback#onStart" },
)
- trySendWithFailureLogging(CallbackEvent.OnStart, TAG)
+ trySendWithFailureLogging(CallbackEvent.OnStart(info), TAG)
}
override fun onStop(info: MediaProjectionInfo?) {
@@ -109,7 +110,7 @@
override fun onRecordingSessionSet(
info: MediaProjectionInfo,
- session: ContentRecordingSession?
+ session: ContentRecordingSession?,
) {
logger.log(
TAG,
@@ -142,7 +143,21 @@
// #onRecordingSessionSet and we don't emit "Projecting".
.mapLatest {
when (it) {
- is CallbackEvent.OnStart,
+ is CallbackEvent.OnStart -> {
+ if (!Flags.statusBarShowAudioOnlyProjectionChip()) {
+ return@mapLatest MediaProjectionState.NotProjecting
+ }
+ // It's possible for a projection to be audio-only, in which case `OnStart`
+ // will occur but `OnRecordingSessionSet` will not. We should still consider
+ // us to be projecting even if only audio is projecting. See b/373308507.
+ if (it.info != null) {
+ MediaProjectionState.Projecting.NoScreen(
+ hostPackage = it.info.packageName
+ )
+ } else {
+ MediaProjectionState.NotProjecting
+ }
+ }
is CallbackEvent.OnStop -> MediaProjectionState.NotProjecting
is CallbackEvent.OnRecordingSessionSet -> stateForSession(it.info, it.session)
}
@@ -155,7 +170,7 @@
private suspend fun stateForSession(
info: MediaProjectionInfo,
- session: ContentRecordingSession?
+ session: ContentRecordingSession?,
): MediaProjectionState {
if (session == null) {
return MediaProjectionState.NotProjecting
@@ -184,7 +199,7 @@
* the correct callback ordering.
*/
sealed interface CallbackEvent {
- data object OnStart : CallbackEvent
+ data class OnStart(val info: MediaProjectionInfo?) : CallbackEvent
data object OnStop : CallbackEvent
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 118639c..ccc54f1 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
@@ -68,6 +68,7 @@
}
}
is MediaProjectionState.Projecting.EntireScreen,
+ is MediaProjectionState.Projecting.NoScreen,
is MediaProjectionState.NotProjecting -> {
flowOf(TaskSwitchState.NotProjectingTask)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
index d4ad6ee..1107206 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
@@ -68,23 +68,24 @@
private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
@StatusBarChipsLog private val logger: LogBuffer,
) : OngoingActivityChipViewModel {
- /**
- * The cast chip to show, based only on MediaProjection API events.
- *
- * This chip will only be [OngoingActivityChipModel.Shown] when the user is casting their
- * *screen*. If the user is only casting audio, this chip will be
- * [OngoingActivityChipModel.Hidden].
- */
+ /** The cast chip to show, based only on MediaProjection API events. */
private val projectionChip: StateFlow<OngoingActivityChipModel> =
mediaProjectionChipInteractor.projection
.map { projectionModel ->
when (projectionModel) {
is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden()
is ProjectionChipModel.Projecting -> {
- if (projectionModel.type != ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE) {
- OngoingActivityChipModel.Hidden()
- } else {
- createCastScreenToOtherDeviceChip(projectionModel)
+ when (projectionModel.receiver) {
+ ProjectionChipModel.Receiver.CastToOtherDevice -> {
+ when (projectionModel.contentType) {
+ ProjectionChipModel.ContentType.Screen ->
+ createCastScreenToOtherDeviceChip(projectionModel)
+ ProjectionChipModel.ContentType.Audio ->
+ createIconOnlyCastChip(deviceName = null)
+ }
+ }
+ ProjectionChipModel.Receiver.ShareToApp ->
+ OngoingActivityChipModel.Hidden()
}
}
}
@@ -98,9 +99,9 @@
* This chip will be [OngoingActivityChipModel.Shown] when the user is casting their screen *or*
* their audio.
*
- * The MediaProjection APIs are not invoked for casting *only audio* to another device because
- * MediaProjection is only concerned with *screen* sharing (see b/342169876). We listen to
- * MediaRouter APIs here to cover audio-only casting.
+ * The MediaProjection APIs are typically not invoked for casting *only audio* to another device
+ * because MediaProjection is only concerned with *screen* sharing (see b/342169876). We listen
+ * to MediaRouter APIs here to cover audio-only casting.
*
* Note that this means we will start showing the cast chip before the casting actually starts,
* for **both** audio-only casting and screen casting. MediaRouter is aware of all
@@ -139,7 +140,7 @@
str1 = projection.logName
str2 = router.logName
},
- { "projectionChip=$str1 > routerChip=$str2" }
+ { "projectionChip=$str1 > routerChip=$str2" },
)
// A consequence of b/269975671 is that MediaRouter and MediaProjection APIs fire at
@@ -186,7 +187,7 @@
}
private fun createCastScreenToOtherDeviceChip(
- state: ProjectionChipModel.Projecting,
+ state: ProjectionChipModel.Projecting
): OngoingActivityChipModel.Shown {
return OngoingActivityChipModel.Shown.Timer(
icon =
@@ -195,7 +196,7 @@
CAST_TO_OTHER_DEVICE_ICON,
// This string is "Casting screen"
ContentDescription.Resource(
- R.string.cast_screen_to_other_device_chip_accessibility_label,
+ R.string.cast_screen_to_other_device_chip_accessibility_label
),
)
),
@@ -236,9 +237,7 @@
)
}
- private fun createCastScreenToOtherDeviceDialogDelegate(
- state: ProjectionChipModel.Projecting,
- ) =
+ private fun createCastScreenToOtherDeviceDialogDelegate(state: ProjectionChipModel.Projecting) =
EndCastScreenToOtherDeviceDialogDelegate(
endMediaProjectionDialogHelper,
context,
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 c5f78d2..4b0c3cc 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
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.chips.mediaprojection.domain.interactor
import android.content.pm.PackageManager
+import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.LogBuffer
@@ -59,23 +60,43 @@
ProjectionChipModel.NotProjecting
}
is MediaProjectionState.Projecting -> {
- val type =
+ val receiver =
if (packageHasCastingCapabilities(packageManager, state.hostPackage)) {
- ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE
+ ProjectionChipModel.Receiver.CastToOtherDevice
} else {
- ProjectionChipModel.Type.SHARE_TO_APP
+ ProjectionChipModel.Receiver.ShareToApp
}
+ val contentType =
+ if (Flags.statusBarShowAudioOnlyProjectionChip()) {
+ when (state) {
+ is MediaProjectionState.Projecting.EntireScreen,
+ is MediaProjectionState.Projecting.SingleTask ->
+ ProjectionChipModel.ContentType.Screen
+ is MediaProjectionState.Projecting.NoScreen ->
+ ProjectionChipModel.ContentType.Audio
+ }
+ } else {
+ ProjectionChipModel.ContentType.Screen
+ }
+
logger.log(
TAG,
LogLevel.INFO,
{
- str1 = type.name
- str2 = state.hostPackage
- str3 = state.hostDeviceName
+ bool1 = receiver == ProjectionChipModel.Receiver.CastToOtherDevice
+ bool2 = contentType == ProjectionChipModel.ContentType.Screen
+ str1 = state.hostPackage
+ str2 = state.hostDeviceName
},
- { "State: Projecting(type=$str1 hostPackage=$str2 hostDevice=$str3)" }
+ {
+ "State: Projecting(" +
+ "receiver=${if (bool1) "CastToOtherDevice" else "ShareToApp"} " +
+ "contentType=${if (bool2) "Screen" else "Audio"} " +
+ "hostPackage=$str1 " +
+ "hostDevice=$str2)"
+ },
)
- ProjectionChipModel.Projecting(type, state)
+ ProjectionChipModel.Projecting(receiver, contentType, state)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt
index 85682f5..c6283e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt
@@ -28,16 +28,22 @@
/** Media is currently being projected. */
data class Projecting(
- val type: Type,
+ val receiver: Receiver,
+ val contentType: ContentType,
val projectionState: MediaProjectionState.Projecting,
) : ProjectionChipModel()
- enum class Type {
- /**
- * This projection is sharing your phone screen content to another app on the same device.
- */
- SHARE_TO_APP,
- /** This projection is sharing your phone screen content to a different device. */
- CAST_TO_OTHER_DEVICE,
+ enum class Receiver {
+ /** This projection is sharing to another app on the same device. */
+ ShareToApp,
+ /** This projection is sharing to a different device. */
+ CastToOtherDevice,
+ }
+
+ enum class ContentType {
+ /** This projection is sharing your device's screen content. */
+ Screen,
+ /** This projection is sharing your device's audio (but *not* screen). */
+ Audio,
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt
new file mode 100644
index 0000000..8ec0567
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.sharetoapp.ui.view
+
+import android.content.Context
+import android.os.Bundle
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
+import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel.Companion.SHARE_TO_APP_ICON
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/**
+ * A dialog that lets the user stop an ongoing share-to-app event. The user could be sharing their
+ * screen or just sharing their audio. This dialog uses generic strings to handle both cases well.
+ */
+class EndGenericShareToAppDialogDelegate(
+ private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
+ private val context: Context,
+ private val stopAction: () -> Unit,
+) : SystemUIDialog.Delegate {
+ override fun createDialog(): SystemUIDialog {
+ return endMediaProjectionDialogHelper.createDialog(this)
+ }
+
+ override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+ val message = context.getString(R.string.share_to_app_stop_dialog_message_generic)
+ with(dialog) {
+ setIcon(SHARE_TO_APP_ICON)
+ setTitle(R.string.share_to_app_stop_dialog_title_generic)
+ setMessage(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,
+ endMediaProjectionDialogHelper.wrapStopAction(stopAction),
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt
index d10bd77..053016e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt
@@ -26,7 +26,7 @@
import com.android.systemui.statusbar.phone.SystemUIDialog
/** A dialog that lets the user stop an ongoing share-screen-to-app event. */
-class EndShareToAppDialogDelegate(
+class EndShareScreenToAppDialogDelegate(
private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
private val context: Context,
private val stopAction: () -> Unit,
@@ -71,7 +71,7 @@
if (hostAppName != null) {
context.getString(
R.string.share_to_app_stop_dialog_message_entire_screen_with_host_app,
- hostAppName
+ hostAppName,
)
} else {
context.getString(R.string.share_to_app_stop_dialog_message_entire_screen)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
index d99a916..11d077f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
@@ -32,7 +32,8 @@
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor
import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel
import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
-import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareToAppDialogDelegate
+import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndGenericShareToAppDialogDelegate
+import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareScreenToAppDialogDelegate
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper
@@ -68,10 +69,17 @@
when (projectionModel) {
is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden()
is ProjectionChipModel.Projecting -> {
- if (projectionModel.type != ProjectionChipModel.Type.SHARE_TO_APP) {
- OngoingActivityChipModel.Hidden()
- } else {
- createShareToAppChip(projectionModel)
+ when (projectionModel.receiver) {
+ ProjectionChipModel.Receiver.ShareToApp -> {
+ when (projectionModel.contentType) {
+ ProjectionChipModel.ContentType.Screen ->
+ createShareScreenToAppChip(projectionModel)
+ ProjectionChipModel.ContentType.Audio ->
+ createIconOnlyShareToAppChip()
+ }
+ }
+ ProjectionChipModel.Receiver.CastToOtherDevice ->
+ OngoingActivityChipModel.Hidden()
}
}
}
@@ -105,8 +113,8 @@
mediaProjectionChipInteractor.stopProjecting()
}
- private fun createShareToAppChip(
- state: ProjectionChipModel.Projecting,
+ private fun createShareScreenToAppChip(
+ state: ProjectionChipModel.Projecting
): OngoingActivityChipModel.Shown {
return OngoingActivityChipModel.Shown.Timer(
icon =
@@ -120,11 +128,33 @@
// TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
startTimeMs = systemClock.elapsedRealtime(),
createDialogLaunchOnClickListener(
- createShareToAppDialogDelegate(state),
+ createShareScreenToAppDialogDelegate(state),
+ dialogTransitionAnimator,
+ DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Share to app"),
+ logger,
+ TAG,
+ ),
+ )
+ }
+
+ private fun createIconOnlyShareToAppChip(): OngoingActivityChipModel.Shown {
+ return OngoingActivityChipModel.Shown.IconOnly(
+ icon =
+ OngoingActivityChipModel.ChipIcon.SingleColorIcon(
+ Icon.Resource(
+ SHARE_TO_APP_ICON,
+ ContentDescription.Resource(
+ R.string.share_to_app_chip_accessibility_label_generic
+ ),
+ )
+ ),
+ colors = ColorsModel.Red,
+ createDialogLaunchOnClickListener(
+ createGenericShareToAppDialogDelegate(),
dialogTransitionAnimator,
DialogCuj(
Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP,
- tag = "Share to app",
+ tag = "Share to app audio only",
),
logger,
TAG,
@@ -132,14 +162,21 @@
)
}
- private fun createShareToAppDialogDelegate(state: ProjectionChipModel.Projecting) =
- EndShareToAppDialogDelegate(
+ private fun createShareScreenToAppDialogDelegate(state: ProjectionChipModel.Projecting) =
+ EndShareScreenToAppDialogDelegate(
endMediaProjectionDialogHelper,
context,
stopAction = this::stopProjectingFromDialog,
state,
)
+ private fun createGenericShareToAppDialogDelegate() =
+ EndGenericShareToAppDialogDelegate(
+ endMediaProjectionDialogHelper,
+ context,
+ stopAction = this::stopProjectingFromDialog,
+ )
+
companion object {
@DrawableRes val SHARE_TO_APP_ICON = R.drawable.ic_present_to_all
private const val TAG = "ShareToAppVM"