Add flag to use PlaybackState actions in media controls
If flag is enabled, media control actions will be created from the PlaybackState following the pattern: C A (play/pause) B D A = previous, or custom action (if not reserved) B = next, or custom action (if not reserved) C and D = custom actions Previous and next slots can be reserved by the app sending the extra SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV (or NEXT), same as Android Auto. This will result in a blank space in the UI. If the flag is not enabled, or the app doesn't include a PlaybackState, notification actions will be used instead (no change in behavior) Flag can be enabled using: adb shell am broadcast -a com.android.systemui.action.SET_FLAG --ei id 901 --ez value 1 Fixes: 203800354 Test: atest com.android.systemui.media Test: manual, toggle flag Change-Id: Iba20f8ab61c9057cbbd1849bb262da693edd6cb5
This commit is contained in:
@@ -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] -->
|
||||
|
||||
@@ -121,6 +121,7 @@ public class Flags {
|
||||
/***************************************/
|
||||
// 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 ==========================
|
||||
|
||||
@@ -862,8 +862,23 @@ class MediaCarouselController @Inject constructor(
|
||||
|
||||
@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
|
||||
|
||||
@@ -62,6 +62,7 @@ import com.android.systemui.util.animation.TransitionLayout;
|
||||
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 @@ public class MediaControlPanel {
|
||||
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 @@ public class MediaControlPanel {
|
||||
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 @@ public class MediaControlPanel {
|
||||
mMediaOutputDialogFactory = mediaOutputDialogFactory;
|
||||
mMediaCarouselController = mediaCarouselController;
|
||||
mFalsingManager = falsingManager;
|
||||
mMediaFlags = mediaFlags;
|
||||
mSystemClock = systemClock;
|
||||
|
||||
loadDimens();
|
||||
|
||||
mSeekBarViewModel.setLogSmartspaceClick(() -> {
|
||||
@@ -426,33 +429,61 @@ public class MediaControlPanel {
|
||||
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
|
||||
|
||||
@@ -47,13 +47,18 @@ data class MediaData(
|
||||
*/
|
||||
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>,
|
||||
/**
|
||||
* Same as above, but shown on smaller versions of the player, like in QQS or keyguard.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -125,6 +130,32 @@ data class MediaData(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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?,
|
||||
|
||||
@@ -38,6 +38,7 @@ import android.media.MediaDescription
|
||||
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.provider.Settings
|
||||
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 TAG = "MediaDataManager"
|
||||
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 @@ class MediaDataManager(
|
||||
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 @@ class MediaDataManager(
|
||||
// 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 @@ class MediaDataManager(
|
||||
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 @@ class MediaDataManager(
|
||||
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 @@ class MediaDataManager(
|
||||
}
|
||||
|
||||
// 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 @@ class MediaDataManager(
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package com.android.systemui.media
|
||||
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
|
||||
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
|
||||
@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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
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)
|
||||
|
||||
@@ -74,8 +74,9 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -96,11 +96,24 @@ class MediaDataFilterTest : SysuiTestCase() {
|
||||
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)
|
||||
|
||||
@@ -11,12 +11,15 @@ import android.media.MediaDescription
|
||||
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 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
@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 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
@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 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
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 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
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 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
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 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +102,24 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -163,8 +163,27 @@ public class MediaPlayerDataTest : SysuiTestCase() {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -95,13 +95,26 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user