diff options
| author | 2024-10-28 19:30:47 +0000 | |
|---|---|---|
| committer | 2024-10-28 19:30:47 +0000 | |
| commit | 534cbb1d2b730843ed2b7e7454f6314495f486d3 (patch) | |
| tree | 9204fb2b80b75ac4c7bf968b718d69f185622b94 | |
| parent | f4f58948893f3cbad1cc145954df22fbaf159d23 (diff) | |
| parent | f85ad48ee75ccea631ea82985e89c9c250c7d426 (diff) | |
Merge "[Screen share] Update our status to "projecting" earlier in process." into main
18 files changed, 505 insertions, 136 deletions
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 2a65d3938b11..970c8954600b 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -378,6 +378,17 @@ flag { } 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 785d5a8e6184..02825a55923f 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.media.projection.MediaProjectionInfo 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 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { } @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 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { } @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 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { ) 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 5005d1609113..e33ce9ccaaa6 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 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { createAndSetDelegate( MediaProjectionState.Projecting.EntireScreen( HOST_PACKAGE, - hostDeviceName = "My Favorite Device" + hostDeviceName = "My Favorite Device", ) ) @@ -118,8 +118,8 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { 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 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { 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 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { 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 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { HOST_PACKAGE, hostDeviceName = "My Favorite Device", createTask(taskId = 1, baseIntent = baseIntent), - ), + ) ) underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) @@ -235,7 +235,7 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { 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 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { 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 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { 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 77992dbaecc2..01e55011a175 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 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { } @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 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { } @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 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { 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 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { 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 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { 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 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { 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 d0c5e7a102e0..611318acde96 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.Intent 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 @@ class MediaProjectionChipInteractorTest : SysuiTestCase() { } @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 @@ class MediaProjectionChipInteractorTest : SysuiTestCase() { 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).receiver) + .isEqualTo(ProjectionChipModel.Receiver.CastToOtherDevice) + assertThat((latest as ProjectionChipModel.Projecting).contentType) + .isEqualTo(ProjectionChipModel.ContentType.Screen) + } + + @Test + @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).type) - .isEqualTo(ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE) + 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_isShareToAppChipType() = + fun projection_singleTaskState_normalPackage_isShareToAppAndScreen() = testScope.runTest { val latest by collectLastValue(underTest.projection) @@ -109,12 +145,14 @@ class MediaProjectionChipInteractorTest : SysuiTestCase() { ) 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 @@ class MediaProjectionChipInteractorTest : SysuiTestCase() { 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 @@ class MediaProjectionChipInteractorTest : SysuiTestCase() { 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 000000000000..411d306f163c --- /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 index 325a42bca7d1..6885a6bd7229 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 @@ import org.mockito.kotlin.whenever @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 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { MediaProjectionState.Projecting.SingleTask( HOST_PACKAGE, hostDeviceName = null, - createTask(taskId = 1, baseIntent = baseIntent) + createTask(taskId = 1, baseIntent = baseIntent), ) ) @@ -142,7 +142,7 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { MediaProjectionState.Projecting.SingleTask( HOST_PACKAGE, hostDeviceName = null, - createTask(taskId = 1, baseIntent = baseIntent) + createTask(taskId = 1, baseIntent = baseIntent), ) ) @@ -181,7 +181,7 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { 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 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { 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 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { 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 791a21d0fb63..d7d57c87f48c 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.res.R 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 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { 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 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { 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 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @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 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @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 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { // 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 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @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 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { clickListener!!.onClick(chipView) verify(kosmos.mockDialogTransitionAnimator) .showFromView( - eq(mockShareDialog), + eq(mockScreenShareDialog), eq(chipBackgroundView), any(), anyBoolean(), @@ -242,7 +305,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @Test - fun chip_singleTask_clickListenerShowsShareDialog() = + fun chip_singleTask_clickListenerShowsScreenShareDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) mediaProjectionRepo.mediaProjectionState.value = @@ -258,7 +321,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { clickListener!!.onClick(chipView) verify(kosmos.mockDialogTransitionAnimator) .showFromView( - eq(mockShareDialog), + eq(mockScreenShareDialog), eq(chipBackgroundView), any(), anyBoolean(), @@ -281,12 +344,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { 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 c838180f9541..fe720b9af0b6 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 82b48251cc8a..2fa34053c8b2 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 @@ sealed interface MediaProjectionState { * 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 @@ sealed interface MediaProjectionState { 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 5704e8048b7d..35efd751b8fe 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.media.projection.MediaProjectionManager 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 @@ constructor( {}, { "MediaProjectionManager.Callback#onStart" }, ) - trySendWithFailureLogging(CallbackEvent.OnStart, TAG) + trySendWithFailureLogging(CallbackEvent.OnStart(info), TAG) } override fun onStop(info: MediaProjectionInfo?) { @@ -109,7 +110,7 @@ constructor( override fun onRecordingSessionSet( info: MediaProjectionInfo, - session: ContentRecordingSession? + session: ContentRecordingSession?, ) { logger.log( TAG, @@ -142,7 +143,21 @@ constructor( // #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 @@ constructor( private suspend fun stateForSession( info: MediaProjectionInfo, - session: ContentRecordingSession? + session: ContentRecordingSession?, ): MediaProjectionState { if (session == null) { return MediaProjectionState.NotProjecting @@ -184,7 +199,7 @@ constructor( * 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 118639c521f6..ccc54f18d419 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 @@ constructor( } } 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 d4ad6ee0d04b..11072068bef9 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 @@ constructor( 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 @@ constructor( * 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 @@ constructor( 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 @@ constructor( } private fun createCastScreenToOtherDeviceChip( - state: ProjectionChipModel.Projecting, + state: ProjectionChipModel.Projecting ): OngoingActivityChipModel.Shown { return OngoingActivityChipModel.Shown.Timer( icon = @@ -195,7 +196,7 @@ constructor( 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 @@ constructor( ) } - 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 8abe1d329a63..27b2465d52b3 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 @@ constructor( 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(" + + "receiver=${if (bool1) "CastToOtherDevice" else "ShareToApp"} " + + "contentType=${if (bool2) "Screen" else "Audio"} " + + "hostPackage=$str1 " + + "hostDevice=$str2)" }, - { "State: Projecting(type=$str1 hostPackage=$str2 hostDevice=$str3)" } ) - 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 85682f5eb8ff..c6283e9962e8 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 @@ sealed class ProjectionChipModel { /** 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 000000000000..8ec05677107e --- /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 index d10bd7705ce9..053016e3109d 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.chips.sharetoapp.ui.viewmodel.ShareToAppCh 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 @@ class EndShareToAppDialogDelegate( 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 d99a916b78a7..11d077fc09f1 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.StatusBarChipsLog 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( // 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 @@ constructor( ) } - 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" |