summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt128
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogController.java18
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt115
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaControlModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java25
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaOutputSwitcherViewModel.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt41
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/BroadcastDialogControllerKosmos.kt22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractorKosmos.kt12
12 files changed, 431 insertions, 17 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt
index a1cee8aaac7c..164acdbc49ba 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt
@@ -16,10 +16,17 @@
package com.android.systemui.media.controls.domain.interactor
+import android.app.PendingIntent
+import android.os.Bundle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
import com.android.systemui.SysuiTestCase
+import com.android.systemui.activityIntentHelper
+import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
+import com.android.systemui.bluetooth.mockBroadcastDialogController
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl
@@ -28,13 +35,22 @@ import com.android.systemui.media.controls.domain.pipeline.interactor.mediaContr
import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.util.mediaInstanceId
+import com.android.systemui.media.mediaOutputDialogManager
+import com.android.systemui.mockActivityIntentHelper
+import com.android.systemui.plugins.activityStarter
import com.android.systemui.statusbar.notificationLockscreenUserManager
+import com.android.systemui.statusbar.policy.keyguardStateController
import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -44,10 +60,16 @@ class MediaControlInteractorTest : SysuiTestCase() {
private val testScope = kosmos.testScope
private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter
+ private val activityStarter = kosmos.activityStarter
+ private val keyguardStateController = kosmos.keyguardStateController
private val instanceId: InstanceId = kosmos.mediaInstanceId
private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager
- private val underTest: MediaControlInteractor = kosmos.mediaControlInteractor
+ private val underTest: MediaControlInteractor =
+ with(kosmos) {
+ activityIntentHelper = mockActivityIntentHelper
+ kosmos.mediaControlInteractor
+ }
@Test
fun onMediaDataUpdated() =
@@ -84,9 +106,113 @@ class MediaControlInteractorTest : SysuiTestCase() {
assertThat(controlModel?.artistName).isEqualTo(SESSION_ARTIST_2)
}
+ @Test
+ fun startSettings() {
+ underTest.startSettings()
+
+ verify(activityStarter).startActivity(any(), eq(true))
+ }
+
+ @Test
+ fun startClickIntent_showOverLockscreen() {
+ whenever(keyguardStateController.isShowing).thenReturn(true)
+ whenever(kosmos.activityIntentHelper.wouldPendingShowOverLockscreen(any(), any()))
+ .thenReturn(true)
+
+ val clickIntent = mock<PendingIntent> { whenever(isActivity).thenReturn(true) }
+ val expandable = mock<Expandable>()
+
+ underTest.startClickIntent(expandable, clickIntent)
+
+ verify(clickIntent).send(any<Bundle>())
+ }
+
+ @Test
+ fun startClickIntent_hideOverLockscreen() {
+ whenever(keyguardStateController.isShowing).thenReturn(false)
+
+ val clickIntent = mock<PendingIntent> { whenever(isActivity).thenReturn(true) }
+ val expandable = mock<Expandable>()
+ val activityController = mock<ActivityTransitionAnimator.Controller>()
+ whenever(expandable.activityTransitionController(any())).thenReturn(activityController)
+
+ underTest.startClickIntent(expandable, clickIntent)
+
+ verify(activityStarter)
+ .postStartActivityDismissingKeyguard(eq(clickIntent), eq(activityController))
+ }
+
+ @Test
+ fun startDeviceIntent_showOverLockscreen() {
+ whenever(keyguardStateController.isShowing).thenReturn(true)
+ whenever(kosmos.activityIntentHelper.wouldPendingShowOverLockscreen(any(), any()))
+ .thenReturn(true)
+
+ val deviceIntent = mock<PendingIntent> { whenever(isActivity).thenReturn(true) }
+
+ underTest.startDeviceIntent(deviceIntent)
+
+ verify(deviceIntent).send(any<Bundle>())
+ }
+
+ @Test
+ fun startDeviceIntent_intentNotActivity() {
+ whenever(keyguardStateController.isShowing).thenReturn(true)
+ whenever(kosmos.activityIntentHelper.wouldPendingShowOverLockscreen(any(), any()))
+ .thenReturn(true)
+
+ val deviceIntent = mock<PendingIntent> { whenever(isActivity).thenReturn(false) }
+
+ underTest.startDeviceIntent(deviceIntent)
+
+ verify(deviceIntent, never()).send(any<Bundle>())
+ }
+
+ @Test
+ fun startDeviceIntent_hideOverLockscreen() {
+ whenever(keyguardStateController.isShowing).thenReturn(false)
+
+ val deviceIntent = mock<PendingIntent> { whenever(isActivity).thenReturn(true) }
+
+ underTest.startDeviceIntent(deviceIntent)
+
+ verify(activityStarter).postStartActivityDismissingKeyguard(eq(deviceIntent))
+ }
+
+ @Test
+ fun startMediaOutputDialog() {
+ val expandable = mock<Expandable>()
+ val dialogTransitionController = mock<DialogTransitionAnimator.Controller>()
+ whenever(expandable.dialogTransitionController(any()))
+ .thenReturn(dialogTransitionController)
+
+ underTest.startMediaOutputDialog(expandable, PACKAGE_NAME)
+
+ verify(kosmos.mediaOutputDialogManager)
+ .createAndShowWithController(eq(PACKAGE_NAME), eq(true), eq(dialogTransitionController))
+ }
+
+ @Test
+ fun startBroadcastDialog() {
+ val expandable = mock<Expandable>()
+ val dialogTransitionController = mock<DialogTransitionAnimator.Controller>()
+ whenever(expandable.dialogTransitionController()).thenReturn(dialogTransitionController)
+
+ underTest.startBroadcastDialog(expandable, APP_NAME, PACKAGE_NAME)
+
+ verify(kosmos.mockBroadcastDialogController)
+ .createBroadcastDialogWithController(
+ eq(APP_NAME),
+ eq(PACKAGE_NAME),
+ eq(dialogTransitionController)
+ )
+ }
+
companion object {
private const val USER_ID = 0
private const val KEY = "key"
+ private const val PACKAGE_NAME = "com.example.app"
+ private const val APP_NAME = "app"
private const val SESSION_ARTIST = "artist"
private const val SESSION_ARTIST_2 = "artist2"
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogController.java b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogController.java
index 161458fdb33e..a90e60d2a8a3 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogController.java
@@ -56,4 +56,22 @@ public class BroadcastDialogController {
broadcastDialog.show();
}
}
+
+ /** Creates a [BroadcastDialog] for the user to switch broadcast or change the output device
+ *
+ * @param currentBroadcastAppName Indicates the APP name currently broadcasting
+ * @param outputPkgName Indicates the output media package name to be switched
+ * @param controller Indicates the dialog controller of the source view.
+ */
+ public void createBroadcastDialogWithController(
+ String currentBroadcastAppName, String outputPkgName,
+ DialogTransitionAnimator.Controller controller) {
+ SystemUIDialog broadcastDialog = mBroadcastDialogFactory.create(
+ currentBroadcastAppName, outputPkgName).createDialog();
+ if (controller != null) {
+ mDialogTransitionAnimator.show(broadcastDialog, controller);
+ } else {
+ broadcastDialog.show();
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index 7412290e8fc5..1d7c0256b2ef 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -590,7 +590,7 @@ class MediaDataProcessor(
}
/** Dismiss a media entry. Returns false if the key was not found. */
- fun dismissMediaData(key: String, delay: Long): Boolean {
+ fun dismissMediaData(key: String, delayMs: Long): Boolean {
val existed = mediaDataRepository.mediaEntries.value[key] != null
backgroundExecutor.execute {
mediaDataRepository.mediaEntries.value[key]?.let { mediaData ->
@@ -602,10 +602,21 @@ class MediaDataProcessor(
}
}
}
- foregroundExecutor.executeDelayed({ removeEntry(key) }, delay)
+ foregroundExecutor.executeDelayed({ removeEntry(key) }, delayMs)
return existed
}
+ /** Dismiss a media entry. Returns false if the corresponding key was not found. */
+ fun dismissMediaData(instanceId: InstanceId, delayMs: Long): Boolean {
+ val mediaEntries = mediaDataRepository.mediaEntries.value
+ val filteredEntries = mediaEntries.filter { (_, data) -> data.instanceId == instanceId }
+ return if (filteredEntries.isNotEmpty()) {
+ dismissMediaData(filteredEntries.keys.first(), delayMs)
+ } else {
+ false
+ }
+ }
+
/**
* Called whenever the recommendation has been expired or removed by the user. This will remove
* the recommendation card entirely from the carousel.
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
index 5a0388de444e..f6c51a83e88e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
@@ -16,20 +16,43 @@
package com.android.systemui.media.controls.domain.pipeline.interactor
+import android.app.ActivityOptions
+import android.app.BroadcastOptions
+import android.app.PendingIntent
+import android.content.Intent
+import android.media.session.MediaSession
+import android.provider.Settings
+import android.util.Log
+import com.android.internal.jank.Cuj
import com.android.internal.logging.InstanceId
+import com.android.systemui.ActivityIntentHelper
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
+import com.android.systemui.bluetooth.BroadcastDialogController
import com.android.systemui.media.controls.data.repository.MediaFilterRepository
import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor
import com.android.systemui.media.controls.shared.model.MediaControlModel
import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.dialog.MediaOutputDialogManager
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.policy.KeyguardStateController
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/** Encapsulates business logic for single media control. */
class MediaControlInteractor(
- instanceId: InstanceId,
+ private val instanceId: InstanceId,
repository: MediaFilterRepository,
private val mediaDataProcessor: MediaDataProcessor,
+ private val keyguardStateController: KeyguardStateController,
+ private val activityStarter: ActivityStarter,
+ private val activityIntentHelper: ActivityIntentHelper,
+ private val lockscreenUserManager: NotificationLockscreenUserManager,
+ private val mediaOutputDialogManager: MediaOutputDialogManager,
+ private val broadcastDialogController: BroadcastDialogController,
) {
val mediaControl: Flow<MediaControlModel?> =
@@ -37,8 +60,19 @@ class MediaControlInteractor(
.map { entries -> entries[instanceId]?.let { toMediaControlModel(it) } }
.distinctUntilChanged()
- fun removeMediaControl(key: String, delayMs: Long): Boolean {
- return mediaDataProcessor.dismissMediaData(key, delayMs)
+ fun removeMediaControl(
+ token: MediaSession.Token?,
+ instanceId: InstanceId,
+ delayMs: Long
+ ): Boolean {
+ val dismissed = mediaDataProcessor.dismissMediaData(instanceId, delayMs)
+ if (!dismissed) {
+ Log.w(
+ TAG,
+ "Manager failed to dismiss media of instanceId=$instanceId, Token uid=${token?.uid}"
+ )
+ }
+ return dismissed
}
private fun toMediaControlModel(data: MediaData): MediaControlModel {
@@ -53,14 +87,89 @@ class MediaControlInteractor(
appName = app,
songName = song,
artistName = artist,
+ showExplicit = isExplicit,
artwork = artwork,
deviceData = device,
semanticActionButtons = semanticActions,
notificationActionButtons = actions,
actionsToShowInCollapsed = actionsToShowInCompact,
+ isDismissible = isClearable,
isResume = resumption,
resumeProgress = resumeProgress,
)
}
}
+
+ fun startSettings() {
+ activityStarter.startActivity(SETTINGS_INTENT, /* dismissShade= */ true)
+ }
+
+ fun startClickIntent(expandable: Expandable, clickIntent: PendingIntent) {
+ if (!launchOverLockscreen(clickIntent)) {
+ activityStarter.postStartActivityDismissingKeyguard(
+ clickIntent,
+ expandable.activityTransitionController(Cuj.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER)
+ )
+ }
+ }
+
+ fun startDeviceIntent(deviceIntent: PendingIntent) {
+ if (deviceIntent.isActivity) {
+ if (!launchOverLockscreen(deviceIntent)) {
+ activityStarter.postStartActivityDismissingKeyguard(deviceIntent)
+ }
+ } else {
+ Log.w(TAG, "Device pending intent of instanceId=$instanceId is not an activity.")
+ }
+ }
+
+ private fun launchOverLockscreen(pendingIntent: PendingIntent): Boolean {
+ val showOverLockscreen =
+ keyguardStateController.isShowing &&
+ activityIntentHelper.wouldPendingShowOverLockscreen(
+ pendingIntent,
+ lockscreenUserManager.currentUserId
+ )
+ if (showOverLockscreen) {
+ try {
+ val options = BroadcastOptions.makeBasic()
+ options.isInteractive = true
+ options.pendingIntentBackgroundActivityStartMode =
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+ pendingIntent.send(options.toBundle())
+ } catch (e: PendingIntent.CanceledException) {
+ Log.e(TAG, "pending intent of $instanceId was canceled")
+ }
+ return true
+ }
+ return false
+ }
+
+ fun startMediaOutputDialog(expandable: Expandable, packageName: String) {
+ mediaOutputDialogManager.createAndShowWithController(
+ packageName,
+ true,
+ expandable.dialogController()
+ )
+ }
+
+ fun startBroadcastDialog(expandable: Expandable, broadcastApp: String, packageName: String) {
+ broadcastDialogController.createBroadcastDialogWithController(
+ broadcastApp,
+ packageName,
+ expandable.dialogTransitionController()
+ )
+ }
+
+ private fun Expandable.dialogController(): DialogTransitionAnimator.Controller? {
+ return dialogTransitionController(
+ cuj =
+ DialogCuj(Cuj.CUJ_SHADE_DIALOG_OPEN, MediaOutputDialogManager.INTERACTION_JANK_TAG)
+ )
+ }
+
+ companion object {
+ private const val TAG = "MediaControlInteractor"
+ private val SETTINGS_INTENT = Intent(Settings.ACTION_MEDIA_CONTROLS_SETTINGS)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaControlModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaControlModel.kt
index d4e34b5af260..f9134f52b58f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaControlModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaControlModel.kt
@@ -33,6 +33,7 @@ data class MediaControlModel(
val appName: String?,
val songName: CharSequence?,
val artistName: CharSequence?,
+ val showExplicit: Boolean,
val artwork: Icon?,
val deviceData: MediaDeviceData?,
/** [MediaButton] contains [MediaAction] objects which represent specific buttons in the UI */
@@ -43,6 +44,7 @@ data class MediaControlModel(
* [Notification.MediaStyle.setShowActionsInCompactView].
*/
val actionsToShowInCollapsed: List<Int>,
+ val isDismissible: Boolean,
/** Whether player is in resumption state. */
val isResume: Boolean,
/** Track seek bar progress (0 - 1) when [isResume] is true. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
index 899b9ed103cd..c7e602904de3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
@@ -747,18 +747,23 @@ public class MediaControlPanel {
boolean showOverLockscreen = mKeyguardStateController.isShowing()
&& mActivityIntentHelper.wouldPendingShowOverLockscreen(
deviceIntent, mLockscreenUserManager.getCurrentUserId());
- if (deviceIntent.isActivity() && !showOverLockscreen) {
- mActivityStarter.postStartActivityDismissingKeyguard(deviceIntent);
- } else {
- try {
- BroadcastOptions options = BroadcastOptions.makeBasic();
- options.setInteractive(true);
- options.setPendingIntentBackgroundActivityStartMode(
+ if (deviceIntent.isActivity()) {
+ if (!showOverLockscreen) {
+ mActivityStarter.postStartActivityDismissingKeyguard(
+ deviceIntent);
+ } else {
+ try {
+ BroadcastOptions options = BroadcastOptions.makeBasic();
+ options.setInteractive(true);
+ options.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
- deviceIntent.send(options.toBundle());
- } catch (PendingIntent.CanceledException e) {
- Log.e(TAG, "Device pending intent was canceled");
+ deviceIntent.send(options.toBundle());
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(TAG, "Device pending intent was canceled");
+ }
}
+ } else {
+ Log.w(TAG, "Device pending intent is not an activity.");
}
} else {
mMediaOutputDialogManager.createAndShow(mPackageName, true,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt
new file mode 100644
index 000000000000..1e67a77250ee
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.media.controls.ui.viewmodel
+
+import android.graphics.drawable.Drawable
+import androidx.constraintlayout.widget.ConstraintSet
+
+/** Models UI state of media buttons in media control. */
+data class MediaActionViewModel(
+ val icon: Drawable?,
+ val contentDescription: CharSequence?,
+ val background: Drawable?,
+ val isVisible: Boolean = true,
+ val notVisibleValue: Int = ConstraintSet.GONE,
+ val showInCollapsed: Boolean,
+ val rebindId: Int? = null,
+ val buttonId: Int? = null,
+ val isEnabled: Boolean,
+ val onClicked: (Int) -> Unit,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaOutputSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaOutputSwitcherViewModel.kt
new file mode 100644
index 000000000000..9df9bccdf522
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaOutputSwitcherViewModel.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.media.controls.ui.viewmodel
+
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.shared.model.Icon
+
+/** Models UI state of output switcher chip. */
+data class MediaOutputSwitcherViewModel(
+ val isTapEnabled: Boolean,
+ val deviceString: CharSequence,
+ val deviceIcon: Icon,
+ val isCurrentBroadcastApp: Boolean,
+ val isIntentValid: Boolean,
+ val alpha: Float,
+ val isVisible: Boolean,
+ val onClicked: (Expandable) -> Unit,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt
new file mode 100644
index 000000000000..9029a65bd9ee
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.media.controls.ui.viewmodel
+
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.monet.ColorScheme
+
+/** Models UI state for media player. */
+data class MediaPlayerViewModel(
+ val contentDescription: (Boolean) -> CharSequence,
+ val backgroundCover: android.graphics.drawable.Icon?,
+ val appIcon: Icon,
+ val useGrayColorFilter: Boolean,
+ val artistName: CharSequence,
+ val titleName: CharSequence,
+ val isExplicitVisible: Boolean,
+ val colorScheme: ColorScheme,
+ val isTimeVisible: Boolean,
+ val playTurbulenceNoise: Boolean,
+ val useSemanticActions: Boolean,
+ val actionButtons: List<MediaActionViewModel?>,
+ val outputSwitcher: MediaOutputSwitcherViewModel,
+ val gutsMenu: GutsViewModel,
+ val onClicked: (Expandable) -> Unit,
+ val onLongClicked: () -> Unit,
+)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt
index 7185b7cd0ac6..96c4c45dbb91 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt
@@ -18,5 +18,7 @@ package com.android.systemui
import android.content.applicationContext
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
-val Kosmos.activityIntentHelper by Kosmos.Fixture { ActivityIntentHelper(applicationContext) }
+val Kosmos.mockActivityIntentHelper by Kosmos.Fixture { mock<ActivityIntentHelper>() }
+var Kosmos.activityIntentHelper by Kosmos.Fixture { ActivityIntentHelper(applicationContext) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/BroadcastDialogControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/BroadcastDialogControllerKosmos.kt
new file mode 100644
index 000000000000..e9d72664c6b0
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/BroadcastDialogControllerKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.bluetooth
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.mockBroadcastDialogController by Kosmos.Fixture { mock<BroadcastDialogController>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractorKosmos.kt
index 29c5bd5dd1d4..6e650a3c5391 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractorKosmos.kt
@@ -16,10 +16,16 @@
package com.android.systemui.media.controls.domain.pipeline.interactor
+import com.android.systemui.activityIntentHelper
+import com.android.systemui.bluetooth.mockBroadcastDialogController
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.media.controls.data.repository.mediaFilterRepository
import com.android.systemui.media.controls.domain.pipeline.mediaDataProcessor
import com.android.systemui.media.controls.util.mediaInstanceId
+import com.android.systemui.media.mediaOutputDialogManager
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.statusbar.notificationLockscreenUserManager
+import com.android.systemui.statusbar.policy.keyguardStateController
val Kosmos.mediaControlInteractor by
Kosmos.Fixture {
@@ -27,5 +33,11 @@ val Kosmos.mediaControlInteractor by
instanceId = mediaInstanceId,
repository = mediaFilterRepository,
mediaDataProcessor = mediaDataProcessor,
+ keyguardStateController = keyguardStateController,
+ activityStarter = activityStarter,
+ activityIntentHelper = activityIntentHelper,
+ lockscreenUserManager = notificationLockscreenUserManager,
+ mediaOutputDialogManager = mediaOutputDialogManager,
+ broadcastDialogController = mockBroadcastDialogController,
)
}