Merge "Add flag to use PlaybackState actions in media controls"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 4790412..43254aa 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2164,6 +2164,15 @@
<!-- Content description for media cotnrols progress bar [CHAR_LIMIT=NONE] -->
<string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1:30">%1$s</xliff:g> of <xliff:g id="total_time" example="3:00">%2$s</xliff:g></string>
+ <!-- Description for button in media controls. Pressing button starts playback [CHAR_LIMIT=NONE] -->
+ <string name="controls_media_button_play">Play</string>
+ <!-- Description for button in media controls. Pressing button pauses playback [CHAR_LIMIT=NONE] -->
+ <string name="controls_media_button_pause">Pause</string>
+ <!-- Description for button in media controls. Pressing button goes to previous track [CHAR_LIMIT=NONE] -->
+ <string name="controls_media_button_prev">Previous track</string>
+ <!-- Description for button in media controls. Pressing button goes to next track [CHAR_LIMIT=NONE] -->
+ <string name="controls_media_button_next">Next track</string>
+
<!-- Title for Smartspace recommendation card within media controls. The "Play" means the action to play a media [CHAR_LIMIT=10] -->
<string name="controls_media_smartspace_rec_title">Play</string>
<!-- Description for Smartspace recommendation card within media controls [CHAR_LIMIT=NONE] -->
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index c293493..97533c9 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -121,6 +121,7 @@
/***************************************/
// 900 - media
public static final BooleanFlag MEDIA_TAP_TO_TRANSFER = new BooleanFlag(900, false);
+ public static final BooleanFlag MEDIA_SESSION_ACTIONS = new BooleanFlag(901, false);
// Pay no attention to the reflection behind the curtain.
// ========================== Curtain ==========================
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index b85f1072..e921ad29c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
@@ -862,8 +862,23 @@
@VisibleForTesting
internal object MediaPlayerData {
- private val EMPTY = MediaData(-1, false, 0, null, null, null, null, null,
- emptyList(), emptyList(), "INVALID", null, null, null, true, null)
+ private val EMPTY = MediaData(
+ userId = -1,
+ initialized = false,
+ backgroundColor = 0,
+ app = null,
+ appIcon = null,
+ artist = null,
+ song = null,
+ artwork = null,
+ actions = emptyList(),
+ actionsToShowInCompact = emptyList(),
+ packageName = "INVALID",
+ token = null,
+ clickIntent = null,
+ device = null,
+ active = true,
+ resumeAction = null)
// Whether should prioritize Smartspace card.
internal var shouldPrioritizeSs: Boolean = false
private set
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index f66eb5b..63555bb 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -62,6 +62,7 @@
import com.android.systemui.util.time.SystemClock;
import java.net.URISyntaxException;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
@@ -122,6 +123,8 @@
private MediaCarouselController mMediaCarouselController;
private final MediaOutputDialogFactory mMediaOutputDialogFactory;
private final FalsingManager mFalsingManager;
+ private final MediaFlags mMediaFlags;
+
// Used for swipe-to-dismiss logging.
protected boolean mIsImpressed = false;
private SystemClock mSystemClock;
@@ -138,7 +141,7 @@
SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager,
KeyguardDismissUtil keyguardDismissUtil, MediaOutputDialogFactory
mediaOutputDialogFactory, MediaCarouselController mediaCarouselController,
- FalsingManager falsingManager, SystemClock systemClock) {
+ FalsingManager falsingManager, MediaFlags mediaFlags, SystemClock systemClock) {
mContext = context;
mBackgroundExecutor = backgroundExecutor;
mActivityStarter = activityStarter;
@@ -149,8 +152,8 @@
mMediaOutputDialogFactory = mediaOutputDialogFactory;
mMediaCarouselController = mediaCarouselController;
mFalsingManager = falsingManager;
+ mMediaFlags = mediaFlags;
mSystemClock = systemClock;
-
loadDimens();
mSeekBarViewModel.setLogSmartspaceClick(() -> {
@@ -426,33 +429,61 @@
deviceName.setText(deviceString);
seamlessView.setContentDescription(deviceString);
- List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
- // Media controls
- int i = 0;
+ // Media action buttons
List<MediaAction> actionIcons = data.getActions();
+ List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
+
+ if (mMediaFlags.areMediaSessionActionsEnabled() && data.getSemanticActions() != null) {
+ // Use PlaybackState actions instead
+ MediaButton semanticActions = data.getSemanticActions();
+
+ actionIcons = new ArrayList<MediaAction>();
+ actionIcons.add(semanticActions.getStartCustom());
+ actionIcons.add(semanticActions.getPrevOrCustom());
+ actionIcons.add(semanticActions.getPlayOrPause());
+ actionIcons.add(semanticActions.getNextOrCustom());
+ actionIcons.add(semanticActions.getEndCustom());
+
+ actionsWhenCollapsed = new ArrayList<Integer>();
+ actionsWhenCollapsed.add(1);
+ actionsWhenCollapsed.add(2);
+ actionsWhenCollapsed.add(3);
+ }
+
+ int i = 0;
for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) {
int actionId = ACTION_IDS[i];
+ boolean visibleInCompat = actionsWhenCollapsed.contains(i);
final ImageButton button = mPlayerViewHolder.getAction(actionId);
MediaAction mediaAction = actionIcons.get(i);
- button.setImageIcon(mediaAction.getIcon());
- button.setContentDescription(mediaAction.getContentDescription());
- Runnable action = mediaAction.getAction();
+ if (mediaAction != null) {
+ button.setImageIcon(mediaAction.getIcon());
+ button.setContentDescription(mediaAction.getContentDescription());
+ Runnable action = mediaAction.getAction();
- if (action == null) {
- button.setEnabled(false);
+ if (action == null) {
+ button.setEnabled(false);
+ } else {
+ button.setEnabled(true);
+ button.setOnClickListener(v -> {
+ if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+ logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
+ /* isRecommendationCard */ false);
+ action.run();
+ }
+ });
+ }
+ setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat);
+ setVisibleAndAlpha(expandedSet, actionId, true /*visible */);
} else {
- button.setEnabled(true);
- button.setOnClickListener(v -> {
- if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
- logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
- /* isRecommendationCard */ false);
- action.run();
- }
- });
+ button.setImageIcon(null);
+ button.setContentDescription(null);
+ button.setEnabled(false);
+ setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat);
+ // for expanded layout, set as INVISIBLE so that we still reserve space in the UI
+ expandedSet.setVisibility(actionId, ConstraintSet.INVISIBLE);
+ expandedSet.setAlpha(actionId, 0.0f);
}
- boolean visibleInCompat = actionsWhenCollapsed.contains(i);
- setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat);
- setVisibleAndAlpha(expandedSet, actionId, true /*visible */);
}
// Hide any unused buttons
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
index bda07f4..4b8dfde 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
@@ -47,7 +47,7 @@
*/
val artwork: Icon?,
/**
- * List of actions that can be performed on the player: prev, next, play, pause, etc.
+ * List of generic action buttons for the media player, based on notification actions
*/
val actions: List<MediaAction>,
/**
@@ -55,6 +55,11 @@
*/
val actionsToShowInCompact: List<Int>,
/**
+ * Semantic actions buttons, based on the PlaybackState of the media session.
+ * If present, these actions will be preferred in the UI over [actions]
+ */
+ val semanticActions: MediaButton? = null,
+ /**
* Package name of the app that's posting the media.
*/
val packageName: String,
@@ -125,6 +130,32 @@
}
}
+/**
+ * Contains [MediaAction] objects which represent specific buttons in the UI
+ */
+data class MediaButton(
+ /**
+ * Play/pause button
+ */
+ var playOrPause: MediaAction? = null,
+ /**
+ * Next button, or custom action
+ */
+ var nextOrCustom: MediaAction? = null,
+ /**
+ * Previous button, or custom action
+ */
+ var prevOrCustom: MediaAction? = null,
+ /**
+ * First custom action space
+ */
+ var startCustom: MediaAction? = null,
+ /**
+ * Last custom action space
+ */
+ var endCustom: MediaAction? = null
+)
+
/** State of a media action. */
data class MediaAction(
val icon: Icon?,
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index 7c0f7fc..49a63c3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -38,6 +38,7 @@
import android.media.MediaMetadata
import android.media.session.MediaController
import android.media.session.MediaSession
+import android.media.session.PlaybackState
import android.net.Uri
import android.os.Parcelable
import android.os.UserHandle
@@ -45,6 +46,7 @@
import android.service.notification.StatusBarNotification
import android.text.TextUtils
import android.util.Log
+import androidx.media.utils.MediaConstants
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.R
@@ -80,8 +82,24 @@
private const val DEBUG = true
private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent"
-private val LOADING = MediaData(-1, false, 0, null, null, null, null, null,
- emptyList(), emptyList(), "INVALID", null, null, null, true, null)
+private val LOADING = MediaData(
+ userId = -1,
+ initialized = false,
+ backgroundColor = 0,
+ app = null,
+ appIcon = null,
+ artist = null,
+ song = null,
+ artwork = null,
+ actions = emptyList(),
+ actionsToShowInCompact = emptyList(),
+ packageName = "INVALID",
+ token = null,
+ clickIntent = null,
+ device = null,
+ active = true,
+ resumeAction = null)
+
@VisibleForTesting
internal val EMPTY_SMARTSPACE_MEDIA_DATA = SmartspaceMediaData("INVALID", false, false,
"INVALID", null, emptyList(), null, 0, 0)
@@ -112,7 +130,8 @@
private var useMediaResumption: Boolean,
private val useQsMediaPlayer: Boolean,
private val systemClock: SystemClock,
- private val tunerService: TunerService
+ private val tunerService: TunerService,
+ private val mediaFlags: MediaFlags,
) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
companion object {
@@ -127,6 +146,10 @@
// Maximum number of actions allowed in compact view
@JvmField
val MAX_COMPACT_ACTIONS = 3
+
+ /** Maximum number of [PlaybackState.CustomAction] buttons supported */
+ @JvmField
+ val MAX_CUSTOM_ACTIONS = 4
}
private val themeText = com.android.settingslib.Utils.getColorAttr(context,
@@ -182,12 +205,13 @@
activityStarter: ActivityStarter,
smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
clock: SystemClock,
- tunerService: TunerService
+ tunerService: TunerService,
+ mediaFlags: MediaFlags
) : this(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory,
broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener,
mediaSessionBasedFilter, mediaDeviceManager, mediaDataCombineLatest, mediaDataFilter,
activityStarter, smartspaceMediaDataProvider, Utils.useMediaResumption(context),
- Utils.useQsMediaPlayer(context), clock, tunerService)
+ Utils.useQsMediaPlayer(context), clock, tunerService, mediaFlags)
private val appChangeReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@@ -522,7 +546,7 @@
foregroundExecutor.execute {
onMediaDataLoaded(packageName, null, MediaData(userId, true, bgColor, appName,
null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
- packageName, token, appIntent, device = null, active = false,
+ null, packageName, token, appIntent, device = null, active = false,
resumeAction = resumeAction, resumption = true, notificationKey = packageName,
hasCheckedForResume = true, lastActive = lastActive))
}
@@ -594,15 +618,55 @@
}
// Control buttons
+ // If flag is enabled and controller has a PlaybackState, create actions from session info
+ // Otherwise, use the notification actions
+ var actionIcons: List<MediaAction> = emptyList()
+ var actionsToShowCollapsed: List<Int> = emptyList()
+ var semanticActions: MediaButton? = null
+ if (mediaFlags.areMediaSessionActionsEnabled() && mediaController.playbackState != null) {
+ semanticActions = createActionsFromState(sbn.packageName, mediaController)
+ } else {
+ val actions = createActionsFromNotification(sbn)
+ actionIcons = actions.first
+ actionsToShowCollapsed = actions.second
+ }
+
+ val playbackLocation =
+ if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE
+ else if (mediaController.playbackInfo?.playbackType ==
+ MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) MediaData.PLAYBACK_LOCAL
+ else MediaData.PLAYBACK_CAST_LOCAL
+ val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null
+ val lastActive = systemClock.elapsedRealtime()
+ foregroundExecutor.execute {
+ val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
+ val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
+ val active = mediaEntries[key]?.active ?: true
+ onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, bgColor, app,
+ smallIcon, artist, song, artWorkIcon, actionIcons, actionsToShowCollapsed,
+ semanticActions, sbn.packageName, token, notif.contentIntent, null,
+ active, resumeAction = resumeAction, playbackLocation = playbackLocation,
+ notificationKey = key, hasCheckedForResume = hasCheckedForResume,
+ isPlaying = isPlaying, isClearable = sbn.isClearable(),
+ lastActive = lastActive))
+ }
+ }
+
+ /**
+ * Generate action buttons based on notification actions
+ */
+ private fun createActionsFromNotification(sbn: StatusBarNotification):
+ Pair<List<MediaAction>, List<Int>> {
+ val notif = sbn.notification
val actionIcons: MutableList<MediaAction> = ArrayList()
val actions = notif.actions
var actionsToShowCollapsed = notif.extras.getIntArray(
- Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() ?: mutableListOf<Int>()
+ Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() ?: mutableListOf()
if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) {
- Log.e(TAG, "Too many compact actions for $key, limiting to first $MAX_COMPACT_ACTIONS")
+ Log.e(TAG, "Too many compact actions for ${sbn.key}," +
+ "limiting to first $MAX_COMPACT_ACTIONS")
actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS)
}
- // TODO: b/153736623 look into creating actions when this isn't a media style notification
if (actions != null) {
for ((index, action) in actions.withIndex()) {
@@ -631,32 +695,150 @@
action.getIcon()
}.setTint(themeText)
val mediaAction = MediaAction(
- mediaActionIcon,
- runnable,
- action.title)
+ mediaActionIcon,
+ runnable,
+ action.title)
actionIcons.add(mediaAction)
}
}
+ return Pair(actionIcons, actionsToShowCollapsed)
+ }
- val playbackLocation =
- if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE
- else if (mediaController.playbackInfo?.playbackType ==
- MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) MediaData.PLAYBACK_LOCAL
- else MediaData.PLAYBACK_CAST_LOCAL
- val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null
- val lastActive = systemClock.elapsedRealtime()
- foregroundExecutor.execute {
- val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
- val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
- val active = mediaEntries[key]?.active ?: true
- onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, bgColor, app,
- smallIcon, artist, song, artWorkIcon, actionIcons,
- actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null,
- active, resumeAction = resumeAction, playbackLocation = playbackLocation,
- notificationKey = key, hasCheckedForResume = hasCheckedForResume,
- isPlaying = isPlaying, isClearable = sbn.isClearable(),
- lastActive = lastActive))
+ /**
+ * Generates action button info for this media session based on the PlaybackState
+ *
+ * @param packageName Package name for the media app
+ * @param controller MediaController for the current session
+ * @return a Pair consisting of a list of media actions, and a list of ints representing which
+ * of those actions should be shown in the compact player
+ */
+ private fun createActionsFromState(packageName: String, controller: MediaController):
+ MediaButton? {
+ val actions = MediaButton()
+ controller.playbackState?.let { state ->
+ // First, check for standard actions
+ actions.playOrPause = if (isPlayingState(state.state)) {
+ getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE)
+ } else {
+ getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY)
+ }
+ val prevButton = getStandardAction(controller, state.actions,
+ PlaybackState.ACTION_SKIP_TO_PREVIOUS)
+ val nextButton = getStandardAction(controller, state.actions,
+ PlaybackState.ACTION_SKIP_TO_NEXT)
+
+ // Then, check for custom actions
+ val customActions = MutableList<MediaAction?>(4) { null }
+ var customCount = 0
+ for (i in 0..MAX_CUSTOM_ACTIONS) {
+ getCustomAction(state, packageName, controller, customCount)?.let {
+ customActions[customCount++] = it
+ }
+ }
+
+ // Finally, assign the remaining button slots: C A play/pause B D
+ // A = previous, else custom action (if not reserved)
+ // B = next, else custom action (if not reserved)
+ // C and D are always custom actions
+ val reservePrev = controller.extras?.getBoolean(
+ MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV) == true
+ val reserveNext = controller.extras?.getBoolean(
+ MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT) == true
+ var customIdx = 0
+
+ actions.prevOrCustom = if (prevButton != null) {
+ prevButton
+ } else if (!reservePrev) {
+ customActions[customIdx++]
+ } else {
+ null
+ }
+
+ actions.nextOrCustom = if (nextButton != null) {
+ nextButton
+ } else if (!reserveNext) {
+ customActions[customIdx++]
+ } else {
+ null
+ }
+
+ actions.startCustom = customActions[customIdx++]
+ actions.endCustom = customActions[customIdx++]
}
+ return actions
+ }
+
+ /**
+ * Get a [MediaAction] representing one of
+ * - [PlaybackState.ACTION_PLAY]
+ * - [PlaybackState.ACTION_PAUSE]
+ * - [PlaybackState.ACTION_SKIP_TO_PREVIOUS]
+ * - [PlaybackState.ACTION_SKIP_TO_NEXT]
+ */
+ private fun getStandardAction(
+ controller: MediaController,
+ stateActions: Long,
+ action: Long
+ ): MediaAction? {
+ if (stateActions and action == 0L) {
+ return null
+ }
+
+ return when (action) {
+ PlaybackState.ACTION_PLAY -> {
+ MediaAction(
+ Icon.createWithResource(context, com.android.internal.R.drawable.ic_media_play),
+ { controller.transportControls.play() },
+ context.getString(R.string.controls_media_button_play)
+ )
+ }
+ PlaybackState.ACTION_PAUSE -> {
+ MediaAction(
+ Icon.createWithResource(context,
+ com.android.internal.R.drawable.ic_media_pause),
+ { controller.transportControls.pause() },
+ context.getString(R.string.controls_media_button_pause)
+ )
+ }
+ PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
+ MediaAction(
+ Icon.createWithResource(context,
+ com.android.internal.R.drawable.ic_media_previous),
+ { controller.transportControls.skipToPrevious() },
+ context.getString(R.string.controls_media_button_prev)
+ )
+ }
+ PlaybackState.ACTION_SKIP_TO_NEXT -> {
+ MediaAction(
+ Icon.createWithResource(context, com.android.internal.R.drawable.ic_media_next),
+ { controller.transportControls.skipToNext() },
+ context.getString(R.string.controls_media_button_next)
+ )
+ }
+ else -> null
+ }
+ }
+
+ /**
+ * Get a [MediaAction] representing a [PlaybackState.CustomAction]
+ */
+ private fun getCustomAction(
+ state: PlaybackState,
+ packageName: String,
+ controller: MediaController,
+ index: Int
+ ): MediaAction? {
+ if (state.customActions.size <= index || state.customActions[index] == null) {
+ if (DEBUG) { Log.d(TAG, "not enough actions or action was null at $index") }
+ return null
+ }
+
+ val it = state.customActions[index]
+ return MediaAction(
+ Icon.createWithResource(packageName, it.icon),
+ { controller.transportControls.sendCustomAction(it, it.extras) },
+ it.name
+ )
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt
new file mode 100644
index 0000000..b4a4b42
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 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
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
+
+@SysUISingleton
+class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) {
+ /**
+ * Check whether media control actions should be based on PlaybackState instead of notification
+ */
+ fun areMediaSessionActionsEnabled(): Boolean {
+ return featureFlags.isEnabled(Flags.MEDIA_SESSION_ACTIONS)
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
index 41ce941..7cc0172 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -19,6 +19,7 @@
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.Icon
import android.graphics.drawable.RippleDrawable
import android.media.MediaMetadata
import android.media.session.MediaSession
@@ -97,6 +98,7 @@
@Mock private lateinit var mediaOutputDialogFactory: MediaOutputDialogFactory
@Mock private lateinit var mediaCarouselController: MediaCarouselController
@Mock private lateinit var falsingManager: FalsingManager
+ @Mock private lateinit var mediaFlags: MediaFlags
private lateinit var appIcon: ImageView
private lateinit var albumView: ImageView
private lateinit var titleText: TextView
@@ -123,6 +125,7 @@
private lateinit var session: MediaSession
private val device = MediaDeviceData(true, null, DEVICE_NAME)
private val disabledDevice = MediaDeviceData(false, null, "Disabled Device")
+ private lateinit var mediaData: MediaData
private val clock = FakeSystemClock()
@JvmField @Rule val mockito = MockitoJUnit.rule()
@@ -135,7 +138,7 @@
player = MediaControlPanel(context, bgExecutor, activityStarter, mediaViewController,
seekBarViewModel, Lazy { mediaDataManager }, keyguardDismissUtil,
- mediaOutputDialogFactory, mediaCarouselController, falsingManager, clock)
+ mediaOutputDialogFactory, mediaCarouselController, falsingManager, mediaFlags, clock)
whenever(seekBarViewModel.progress).thenReturn(seekBarData)
// Mock out a view holder for the player to attach to.
@@ -165,14 +168,19 @@
whenever(holder.totalTimeView).thenReturn(totalTimeView)
action0 = ImageButton(context)
whenever(holder.action0).thenReturn(action0)
+ whenever(holder.getAction(R.id.action0)).thenReturn(action0)
action1 = ImageButton(context)
whenever(holder.action1).thenReturn(action1)
+ whenever(holder.getAction(R.id.action1)).thenReturn(action1)
action2 = ImageButton(context)
whenever(holder.action2).thenReturn(action2)
+ whenever(holder.getAction(R.id.action2)).thenReturn(action2)
action3 = ImageButton(context)
whenever(holder.action3).thenReturn(action3)
+ whenever(holder.getAction(R.id.action3)).thenReturn(action3)
action4 = ImageButton(context)
whenever(holder.action4).thenReturn(action4)
+ whenever(holder.getAction(R.id.action4)).thenReturn(action4)
whenever(holder.longPressText).thenReturn(longPressText)
whenever(longPressText.handler).thenReturn(handler)
settings = View(context)
@@ -200,6 +208,26 @@
setPlaybackState(playbackBuilder.build())
}
session.setActive(true)
+
+ mediaData = MediaData(
+ userId = USER_ID,
+ initialized = true,
+ backgroundColor = BG_COLOR,
+ app = APP,
+ appIcon = null,
+ artist = ARTIST,
+ song = TITLE,
+ artwork = null,
+ actions = emptyList(),
+ actionsToShowInCompact = emptyList(),
+ packageName = PACKAGE,
+ token = session.sessionToken,
+ clickIntent = null,
+ device = device,
+ active = true,
+ resumeAction = null)
+
+ whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(false)
}
@After
@@ -210,18 +238,50 @@
@Test
fun bindWhenUnattached() {
- val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, null, null, device, true, null)
+ val state = mediaData.copy(token = null)
player.bindPlayer(state, PACKAGE)
assertThat(player.isPlaying()).isFalse()
}
@Test
+ fun bindSemanticActions() {
+ whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(true)
+ val icon = Icon.createWithResource(context, android.R.drawable.ic_media_play)
+ val semanticActions = MediaButton(
+ playOrPause = MediaAction(icon, Runnable {}, "play"),
+ nextOrCustom = MediaAction(icon, Runnable {}, "next"),
+ startCustom = MediaAction(icon, null, "custom 1"),
+ endCustom = MediaAction(icon, null, "custom 2")
+ )
+ val state = mediaData.copy(semanticActions = semanticActions)
+
+ player.attachPlayer(holder)
+ player.bindPlayer(state, PACKAGE)
+
+ verify(expandedSet).setVisibility(R.id.action0, ConstraintSet.VISIBLE)
+ assertThat(action0.contentDescription).isEqualTo("custom 1")
+ assertThat(action0.isEnabled()).isFalse()
+
+ verify(expandedSet).setVisibility(R.id.action1, ConstraintSet.INVISIBLE)
+ assertThat(action1.isEnabled()).isFalse()
+
+ verify(expandedSet).setVisibility(R.id.action2, ConstraintSet.VISIBLE)
+ assertThat(action2.isEnabled()).isTrue()
+ assertThat(action2.contentDescription).isEqualTo("play")
+
+ verify(expandedSet).setVisibility(R.id.action3, ConstraintSet.VISIBLE)
+ assertThat(action3.isEnabled()).isTrue()
+ assertThat(action3.contentDescription).isEqualTo("next")
+
+ verify(expandedSet).setVisibility(R.id.action4, ConstraintSet.VISIBLE)
+ assertThat(action4.contentDescription).isEqualTo("custom 2")
+ assertThat(action4.isEnabled()).isFalse()
+ }
+
+ @Test
fun bindText() {
player.attachPlayer(holder)
- val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null)
- player.bindPlayer(state, PACKAGE)
+ player.bindPlayer(mediaData, PACKAGE)
assertThat(titleText.getText()).isEqualTo(TITLE)
assertThat(artistText.getText()).isEqualTo(ARTIST)
}
@@ -229,9 +289,7 @@
@Test
fun bindDevice() {
player.attachPlayer(holder)
- val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null)
- player.bindPlayer(state, PACKAGE)
+ player.bindPlayer(mediaData, PACKAGE)
assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
assertThat(seamless.contentDescription).isEqualTo(DEVICE_NAME)
assertThat(seamless.isEnabled()).isTrue()
@@ -242,8 +300,7 @@
seamless.id = 1
val fallbackString = context.getString(R.string.media_seamless_other_device)
player.attachPlayer(holder)
- val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, session.getSessionToken(), null, disabledDevice, true, null)
+ val state = mediaData.copy(device = disabledDevice)
player.bindPlayer(state, PACKAGE)
assertThat(seamless.isEnabled()).isFalse()
assertThat(seamlessText.getText()).isEqualTo(fallbackString)
@@ -254,8 +311,7 @@
fun bindNullDevice() {
val fallbackString = context.getResources().getString(R.string.media_seamless_other_device)
player.attachPlayer(holder)
- val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, session.getSessionToken(), null, null, true, null)
+ val state = mediaData.copy(device = null)
player.bindPlayer(state, PACKAGE)
assertThat(seamless.isEnabled()).isTrue()
assertThat(seamlessText.getText()).isEqualTo(fallbackString)
@@ -265,9 +321,7 @@
@Test
fun bindDeviceResumptionPlayer() {
player.attachPlayer(holder)
- val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null,
- resumption = true)
+ val state = mediaData.copy(resumption = true)
player.bindPlayer(state, PACKAGE)
assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
assertThat(seamless.isEnabled()).isFalse()
@@ -323,9 +377,7 @@
fun dismissButtonClick() {
val mediaKey = "key for dismissal"
player.attachPlayer(holder)
- val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, session.getSessionToken(), null, null, true, null,
- notificationKey = KEY)
+ val state = mediaData.copy(notificationKey = KEY)
player.bindPlayer(state, mediaKey)
assertThat(dismiss.isEnabled).isEqualTo(true)
@@ -337,9 +389,7 @@
fun dismissButtonDisabled() {
val mediaKey = "key for dismissal"
player.attachPlayer(holder)
- val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, session.getSessionToken(), null, null, true, null,
- isClearable = false, notificationKey = KEY)
+ val state = mediaData.copy(isClearable = false, notificationKey = KEY)
player.bindPlayer(state, mediaKey)
assertThat(dismiss.isEnabled).isEqualTo(false)
@@ -351,9 +401,7 @@
whenever(mediaDataManager.dismissMediaData(eq(mediaKey), anyLong())).thenReturn(false)
player.attachPlayer(holder)
- val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, session.getSessionToken(), null, null, true, null,
- notificationKey = KEY)
+ val state = mediaData.copy(notificationKey = KEY)
player.bindPlayer(state, mediaKey)
assertThat(dismiss.isEnabled).isEqualTo(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
index 09c83e5..7a487b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
@@ -74,8 +74,9 @@
mManager = new MediaDataCombineLatest();
mManager.addListener(mListener);
- mMediaData = new MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null,
- new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null,
+ mMediaData = new MediaData(
+ USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null,
+ new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null,
MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L);
mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
index 5a3c43c..6b203bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
@@ -96,11 +96,24 @@
setUser(USER_MAIN)
// Set up test media data
- dataMain = MediaData(USER_MAIN, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, null, null, device, true, null)
-
- dataGuest = MediaData(USER_GUEST, true, BG_COLOR, APP, null, ARTIST, TITLE, null,
- emptyList(), emptyList(), PACKAGE, null, null, device, true, null)
+ dataMain = MediaData(
+ userId = USER_MAIN,
+ initialized = true,
+ backgroundColor = BG_COLOR,
+ app = APP,
+ appIcon = null,
+ artist = ARTIST,
+ song = TITLE,
+ artwork = null,
+ actions = emptyList(),
+ actionsToShowInCompact = emptyList(),
+ packageName = PACKAGE,
+ token = null,
+ clickIntent = null,
+ device = device,
+ active = true,
+ resumeAction = null)
+ dataGuest = dataMain.copy(userId = USER_GUEST)
`when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
`when`(smartspaceData.isActive).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
index e2019e0..d0b957c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
@@ -11,12 +11,15 @@
import android.media.MediaMetadata
import android.media.session.MediaController
import android.media.session.MediaSession
+import android.media.session.PlaybackState
import android.os.Bundle
import android.provider.Settings
import android.service.notification.StatusBarNotification
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
+import androidx.media.utils.MediaConstants
import androidx.test.filters.SmallTest
+import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dump.DumpManager
@@ -67,6 +70,7 @@
@JvmField @Rule val mockito = MockitoJUnit.rule()
@Mock lateinit var mediaControllerFactory: MediaControllerFactory
@Mock lateinit var controller: MediaController
+ @Mock lateinit var transportControls: MediaController.TransportControls
@Mock lateinit var playbackInfo: MediaController.PlaybackInfo
lateinit var session: MediaSession
lateinit var metadataBuilder: MediaMetadata.Builder
@@ -87,6 +91,7 @@
@Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget
@Mock private lateinit var mediaRecommendationItem: SmartspaceAction
@Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction
+ @Mock private lateinit var mediaFlags: MediaFlags
lateinit var mediaDataManager: MediaDataManager
lateinit var mediaNotification: StatusBarNotification
@Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
@@ -122,7 +127,8 @@
useMediaResumption = true,
useQsMediaPlayer = true,
systemClock = clock,
- tunerService = tunerService
+ tunerService = tunerService,
+ mediaFlags = mediaFlags
)
verify(tunerService).addTunable(capture(tunableCaptor),
eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
@@ -140,6 +146,7 @@
putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
}
whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller)
+ whenever(controller.transportControls).thenReturn(transportControls)
whenever(controller.playbackInfo).thenReturn(playbackInfo)
whenever(playbackInfo.playbackType).thenReturn(
MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL)
@@ -161,6 +168,7 @@
whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA)
whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf(mediaRecommendationItem))
whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L)
+ whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(false)
}
@After
@@ -583,4 +591,157 @@
assertThat(mediaDataCaptor.value.actionsToShowInCompact.size).isEqualTo(
MediaDataManager.MAX_COMPACT_ACTIONS)
}
+
+ @Test
+ fun testPlaybackActions_noState_usesNotification() {
+ val desc = "Notification Action"
+ whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(true)
+ whenever(controller.playbackState).thenReturn(null)
+
+ val notifWithAction = SbnBuilder().run {
+ setPkg(PACKAGE_NAME)
+ modifyNotification(context).also {
+ it.setSmallIcon(android.R.drawable.ic_media_pause)
+ it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+ it.addAction(android.R.drawable.ic_media_play, desc, null)
+ }
+ build()
+ }
+ mediaDataManager.onNotificationAdded(KEY, notifWithAction)
+
+ assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+ assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+ verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
+ eq(0))
+
+ assertThat(mediaDataCaptor.value!!.semanticActions).isNull()
+ assertThat(mediaDataCaptor.value!!.actions).hasSize(1)
+ assertThat(mediaDataCaptor.value!!.actions[0]!!.contentDescription).isEqualTo(desc)
+ }
+
+ @Test
+ fun testPlaybackActions_hasPrevNext() {
+ val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
+ whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(true)
+ val stateActions = PlaybackState.ACTION_PLAY or
+ PlaybackState.ACTION_SKIP_TO_PREVIOUS or
+ PlaybackState.ACTION_SKIP_TO_NEXT
+ val stateBuilder = PlaybackState.Builder()
+ .setActions(stateActions)
+ customDesc.forEach {
+ stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause)
+ }
+ whenever(controller.playbackState).thenReturn(stateBuilder.build())
+
+ mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+ assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+ assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+ verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
+ eq(0))
+
+ assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
+ val actions = mediaDataCaptor.value!!.semanticActions!!
+
+ assertThat(actions.playOrPause).isNotNull()
+ assertThat(actions.playOrPause!!.contentDescription).isEqualTo(
+ context.getString(R.string.controls_media_button_play))
+ actions.playOrPause!!.action!!.run()
+ verify(transportControls).play()
+
+ assertThat(actions.prevOrCustom).isNotNull()
+ assertThat(actions.prevOrCustom!!.contentDescription).isEqualTo(
+ context.getString(R.string.controls_media_button_prev))
+ actions.prevOrCustom!!.action!!.run()
+ verify(transportControls).skipToPrevious()
+
+ assertThat(actions.nextOrCustom).isNotNull()
+ assertThat(actions.nextOrCustom!!.contentDescription).isEqualTo(
+ context.getString(R.string.controls_media_button_next))
+ actions.nextOrCustom!!.action!!.run()
+ verify(transportControls).skipToNext()
+
+ assertThat(actions.startCustom).isNotNull()
+ assertThat(actions.startCustom!!.contentDescription).isEqualTo(customDesc[0])
+
+ assertThat(actions.endCustom).isNotNull()
+ assertThat(actions.endCustom!!.contentDescription).isEqualTo(customDesc[1])
+ }
+
+ @Test
+ fun testPlaybackActions_noPrevNext_usesCustom() {
+ val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
+ whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(true)
+ val stateActions = PlaybackState.ACTION_PLAY
+ val stateBuilder = PlaybackState.Builder()
+ .setActions(stateActions)
+ customDesc.forEach {
+ stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause)
+ }
+ whenever(controller.playbackState).thenReturn(stateBuilder.build())
+
+ mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+ assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+ assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+ verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
+ eq(0))
+
+ assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
+ val actions = mediaDataCaptor.value!!.semanticActions!!
+
+ assertThat(actions.playOrPause).isNotNull()
+ assertThat(actions.playOrPause!!.contentDescription).isEqualTo(
+ context.getString(R.string.controls_media_button_play))
+
+ assertThat(actions.prevOrCustom).isNotNull()
+ assertThat(actions.prevOrCustom!!.contentDescription).isEqualTo(customDesc[0])
+
+ assertThat(actions.nextOrCustom).isNotNull()
+ assertThat(actions.nextOrCustom!!.contentDescription).isEqualTo(customDesc[1])
+
+ assertThat(actions.startCustom).isNotNull()
+ assertThat(actions.startCustom!!.contentDescription).isEqualTo(customDesc[2])
+
+ assertThat(actions.endCustom).isNotNull()
+ assertThat(actions.endCustom!!.contentDescription).isEqualTo(customDesc[3])
+ }
+
+ @Test
+ fun testPlaybackActions_reservedSpace() {
+ val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
+ whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(true)
+ val stateActions = PlaybackState.ACTION_PLAY
+ val stateBuilder = PlaybackState.Builder()
+ .setActions(stateActions)
+ customDesc.forEach {
+ stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause)
+ }
+ val extras = Bundle().apply {
+ putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true)
+ putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true)
+ }
+ whenever(controller.playbackState).thenReturn(stateBuilder.build())
+ whenever(controller.extras).thenReturn(extras)
+
+ mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+ assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+ assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+ verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
+ eq(0))
+
+ assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
+ val actions = mediaDataCaptor.value!!.semanticActions!!
+
+ assertThat(actions.playOrPause).isNotNull()
+ assertThat(actions.playOrPause!!.contentDescription).isEqualTo(
+ context.getString(R.string.controls_media_button_play))
+
+ assertThat(actions.prevOrCustom).isNull()
+ assertThat(actions.nextOrCustom).isNull()
+
+ assertThat(actions.startCustom).isNotNull()
+ assertThat(actions.startCustom!!.contentDescription).isEqualTo(customDesc[0])
+
+ assertThat(actions.endCustom).isNotNull()
+ assertThat(actions.endCustom!!.contentDescription).isEqualTo(customDesc[1])
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
index 7dadbad..3d59497 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
@@ -102,9 +102,24 @@
// Create a media sesssion and notification for testing.
session = MediaSession(context, SESSION_KEY)
- mediaData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null,
- emptyList(), emptyList(), PACKAGE, session.sessionToken, clickIntent = null,
- device = null, active = true, resumeAction = null)
+ mediaData = MediaData(
+ userId = USER_ID,
+ initialized = true,
+ backgroundColor = 0,
+ app = PACKAGE,
+ appIcon = null,
+ artist = null,
+ song = SESSION_TITLE,
+ artwork = null,
+ actions = emptyList(),
+ actionsToShowInCompact = emptyList(),
+ packageName = PACKAGE,
+ token = session.sessionToken,
+ clickIntent = null,
+ device = null,
+ active = true,
+ resumeAction = null)
+
whenever(controllerFactory.create(session.sessionToken))
.thenReturn(controller)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
index 421f9be..ceeb0db 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
@@ -163,8 +163,27 @@
isPlaying: Boolean?,
location: Int,
resumption: Boolean
- ) =
- MediaData(0, false, 0, app, null, null, null, null, emptyList(), emptyList<Int>(),
- "package:" + app, null, null, null, true, null, location, resumption, "key:" + app,
- false, isPlaying)
+ ) = MediaData(
+ userId = 0,
+ initialized = false,
+ backgroundColor = 0,
+ app = app,
+ appIcon = null,
+ artist = null,
+ song = null,
+ artwork = null,
+ actions = emptyList(),
+ actionsToShowInCompact = emptyList(),
+ packageName = "package: $app",
+ token = null,
+ clickIntent = null,
+ device = null,
+ active = true,
+ resumeAction = null,
+ playbackLocation = location,
+ resumption = resumption,
+ notificationKey = "key: $app",
+ hasCheckedForResume = false,
+ isPlaying = isPlaying
+ )
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
index de2235d..8c2fed5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
@@ -95,13 +95,26 @@
setPlaybackState(playbackBuilder.build())
}
session.setActive(true)
- mediaData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null,
- emptyList(), emptyList(), PACKAGE, session.sessionToken, clickIntent = null,
- device = null, active = true, resumeAction = null)
- resumeData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null,
- emptyList(), emptyList(), PACKAGE, null, clickIntent = null,
- device = null, active = false, resumeAction = null, resumption = true)
+ mediaData = MediaData(
+ userId = USER_ID,
+ initialized = true,
+ backgroundColor = 0,
+ app = PACKAGE,
+ appIcon = null,
+ artist = null,
+ song = SESSION_TITLE,
+ artwork = null,
+ actions = emptyList(),
+ actionsToShowInCompact = emptyList(),
+ packageName = PACKAGE,
+ token = session.sessionToken,
+ clickIntent = null,
+ device = null,
+ active = true,
+ resumeAction = null)
+
+ resumeData = mediaData.copy(token = null, active = false, resumption = true)
}
@Test