diff --git a/packages/SystemUI/res/drawable/ic_music_note.xml b/packages/SystemUI/res/drawable/ic_music_note.xml
new file mode 100644
index 0000000000000..30959a870a02b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_music_note.xml
@@ -0,0 +1,24 @@
+
+
+
+
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 39237ac246eb1..0a6498b9f0c11 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2781,6 +2781,8 @@
Close this media session
+
+ Resume
Inactive, check app
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 5595201a670fb..b8c1842a17808 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -17,12 +17,8 @@
package com.android.systemui.media;
import android.app.PendingIntent;
-import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.Canvas;
@@ -35,7 +31,6 @@ import android.graphics.drawable.RippleDrawable;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
-import android.service.media.MediaBrowserService;
import android.util.Log;
import android.view.View;
import android.widget.ImageButton;
@@ -55,7 +50,6 @@ import com.android.settingslib.media.MediaOutputSliceConstants;
import com.android.settingslib.widget.AdaptiveIcon;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.qs.QSMediaBrowser;
import com.android.systemui.util.animation.TransitionLayout;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -81,7 +75,6 @@ public class MediaControlPanel {
private final SeekBarViewModel mSeekBarViewModel;
private SeekBarObserver mSeekBarObserver;
- private final Executor mForegroundExecutor;
protected final Executor mBackgroundExecutor;
private final ActivityStarter mActivityStarter;
@@ -91,48 +84,18 @@ public class MediaControlPanel {
private MediaSession.Token mToken;
private MediaController mController;
private int mBackgroundColor;
- protected ComponentName mServiceComponent;
- private boolean mIsRegistered = false;
- private String mKey;
private int mAlbumArtSize;
private int mAlbumArtRadius;
- private int mViewWidth;
-
- public static final String MEDIA_PREFERENCES = "media_control_prefs";
- public static final String MEDIA_PREFERENCE_KEY = "browser_components";
- private SharedPreferences mSharedPrefs;
- private boolean mCheckedForResumption = false;
- private QSMediaBrowser mQSMediaBrowser;
-
- private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
- @Override
- public void onSessionDestroyed() {
- Log.d(TAG, "session destroyed");
- mController.unregisterCallback(mSessionCallback);
- clearControls();
- }
- @Override
- public void onPlaybackStateChanged(PlaybackState state) {
- final int s = state != null ? state.getState() : PlaybackState.STATE_NONE;
- if (s == PlaybackState.STATE_NONE) {
- Log.d(TAG, "playback state change will trigger resumption, state=" + state);
- clearControls();
- }
- }
- };
/**
* Initialize a new control panel
* @param context
- * @param foregroundExecutor foreground executor
* @param backgroundExecutor background executor, used for processing artwork
* @param activityStarter activity starter
*/
- public MediaControlPanel(Context context, Executor foregroundExecutor,
- DelayableExecutor backgroundExecutor, ActivityStarter activityStarter,
- MediaHostStatesManager mediaHostStatesManager) {
+ public MediaControlPanel(Context context, DelayableExecutor backgroundExecutor,
+ ActivityStarter activityStarter, MediaHostStatesManager mediaHostStatesManager) {
mContext = context;
- mForegroundExecutor = foregroundExecutor;
mBackgroundExecutor = backgroundExecutor;
mActivityStarter = activityStarter;
mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
@@ -214,45 +177,18 @@ public class MediaControlPanel {
MediaSession.Token token = data.getToken();
mBackgroundColor = data.getBackgroundColor();
if (mToken == null || !mToken.equals(token)) {
- if (mQSMediaBrowser != null) {
- Log.d(TAG, "Disconnecting old media browser");
- mQSMediaBrowser.disconnect();
- mQSMediaBrowser = null;
- }
mToken = token;
- mServiceComponent = null;
- mCheckedForResumption = false;
}
- mController = new MediaController(mContext, mToken);
+ if (mToken != null) {
+ mController = new MediaController(mContext, mToken);
+ } else {
+ mController = null;
+ }
ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
- // Try to find a browser service component for this app
- // TODO also check for a media button receiver intended for restarting (b/154127084)
- // Only check if we haven't tried yet or the session token changed
- final String pkgName = data.getPackageName();
- if (mServiceComponent == null && !mCheckedForResumption) {
- Log.d(TAG, "Checking for service component");
- PackageManager pm = mContext.getPackageManager();
- Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
- List resumeInfo = pm.queryIntentServices(resumeIntent, 0);
- // TODO: look into this resumption
- if (resumeInfo != null) {
- for (ResolveInfo inf : resumeInfo) {
- if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
- mBackgroundExecutor.execute(() ->
- tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
- break;
- }
- }
- }
- mCheckedForResumption = true;
- }
-
- mController.registerCallback(mSessionCallback);
-
mViewHolder.getPlayer().setBackgroundTintList(
ColorStateList.valueOf(mBackgroundColor));
@@ -267,12 +203,22 @@ public class MediaControlPanel {
ImageView albumView = mViewHolder.getAlbumView();
// TODO: migrate this to a view with rounded corners instead of baking the rounding
// into the bitmap
- Drawable artwork = createRoundedBitmap(data.getArtwork());
- albumView.setImageDrawable(artwork);
+ boolean hasArtwork = data.getArtwork() != null;
+ if (hasArtwork) {
+ Drawable artwork = createRoundedBitmap(data.getArtwork());
+ albumView.setImageDrawable(artwork);
+ }
+ setVisibleAndAlpha(collapsedSet, R.id.album_art, hasArtwork);
+ setVisibleAndAlpha(expandedSet, R.id.album_art, hasArtwork);
// App icon
ImageView appIcon = mViewHolder.getAppIcon();
- appIcon.setImageDrawable(data.getAppIcon());
+ if (data.getAppIcon() != null) {
+ appIcon.setImageDrawable(data.getAppIcon());
+ } else {
+ Drawable iconDrawable = mContext.getDrawable(R.drawable.ic_music_note);
+ appIcon.setImageDrawable(iconDrawable);
+ }
// Song name
TextView titleText = mViewHolder.getTitleText();
@@ -294,7 +240,7 @@ public class MediaControlPanel {
final Intent intent = new Intent()
.setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
.putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
- mController.getPackageName())
+ data.getPackageName())
.putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
mActivityStarter.startActivity(intent, false, true /* dismissShade */,
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
@@ -350,15 +296,11 @@ public class MediaControlPanel {
MediaAction mediaAction = actionIcons.get(i);
button.setImageDrawable(mediaAction.getDrawable());
button.setContentDescription(mediaAction.getContentDescription());
- PendingIntent actionIntent = mediaAction.getIntent();
+ Runnable action = mediaAction.getAction();
button.setOnClickListener(v -> {
- if (actionIntent != null) {
- try {
- actionIntent.send();
- } catch (PendingIntent.CanceledException e) {
- e.printStackTrace();
- }
+ if (action != null) {
+ action.run();
}
});
boolean visibleInCompat = actionsWhenCollapsed.contains(i);
@@ -443,14 +385,6 @@ public class MediaControlPanel {
return mController.getPackageName();
}
- /**
- * Return the original notification's key
- * @return The notification key
- */
- public String getKey() {
- return mKey;
- }
-
/**
* Check whether this player has an attached media session.
* @return whether there is a controller with a current media session.
@@ -485,150 +419,8 @@ public class MediaControlPanel {
return (state.getState() == PlaybackState.STATE_PLAYING);
}
- /**
- * Puts controls into a resumption state if possible, or calls removePlayer if no component was
- * found that could resume playback
- */
- public void clearControls() {
- Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
- if (mServiceComponent == null) {
- // If we don't have a way to resume, just remove the player altogether
- Log.d(TAG, "Removing unresumable controls");
- removePlayer();
- return;
- }
- resetButtons();
- }
-
- /**
- * Hide the media buttons and show only a restart button
- */
- protected void resetButtons() {
- if (mViewHolder == null) {
- return;
- }
- // Hide all the old buttons
-
- ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
- ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
- for (int i = 1; i < ACTION_IDS.length; i++) {
- setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */);
- setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
- }
-
- // Add a restart button
- ImageButton btn = mViewHolder.getAction0();
- btn.setOnClickListener(v -> {
- Log.d(TAG, "Attempting to restart session");
- if (mQSMediaBrowser != null) {
- mQSMediaBrowser.disconnect();
- }
- mQSMediaBrowser = new QSMediaBrowser(mContext, new QSMediaBrowser.Callback(){
- @Override
- public void onConnected() {
- Log.d(TAG, "Successfully restarted");
- }
- @Override
- public void onError() {
- Log.e(TAG, "Error restarting");
- mQSMediaBrowser.disconnect();
- mQSMediaBrowser = null;
- }
- }, mServiceComponent);
- mQSMediaBrowser.restart();
- });
- btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
- setVisibleAndAlpha(expandedSet, ACTION_IDS[0], true /*visible */);
- setVisibleAndAlpha(collapsedSet, ACTION_IDS[0], true /*visible */);
-
- mSeekBarViewModel.clearController();
- // TODO: fix guts
- // View guts = mMediaNotifView.findViewById(R.id.media_guts);
- View options = mViewHolder.getOptions();
-
- mViewHolder.getPlayer().setOnLongClickListener(v -> {
- // Replace player view with close/cancel view
-// guts.setVisibility(View.GONE);
- options.setVisibility(View.VISIBLE);
- return true; // consumed click
- });
- mMediaViewController.refreshState();
- }
-
private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
set.setVisibility(actionId, visible? ConstraintSet.VISIBLE : ConstraintSet.GONE);
set.setAlpha(actionId, visible ? 1.0f : 0.0f);
}
-
- /**
- * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
- * component to the list of resumption components
- */
- private void tryUpdateResumptionList(ComponentName componentName) {
- Log.d(TAG, "Testing if we can connect to " + componentName);
- if (mQSMediaBrowser != null) {
- mQSMediaBrowser.disconnect();
- }
- mQSMediaBrowser = new QSMediaBrowser(mContext,
- new QSMediaBrowser.Callback() {
- @Override
- public void onConnected() {
- Log.d(TAG, "yes we can resume with " + componentName);
- mServiceComponent = componentName;
- updateResumptionList(componentName);
- mQSMediaBrowser.disconnect();
- mQSMediaBrowser = null;
- }
-
- @Override
- public void onError() {
- Log.d(TAG, "Cannot resume with " + componentName);
- mServiceComponent = null;
- if (!hasMediaSession()) {
- // If it's not active and we can't resume, remove
- removePlayer();
- }
- mQSMediaBrowser.disconnect();
- mQSMediaBrowser = null;
- }
- },
- componentName);
- mQSMediaBrowser.testConnection();
- }
-
- /**
- * Add the component to the saved list of media browser services, checking for duplicates and
- * removing older components that exceed the maximum limit
- * @param componentName
- */
- private synchronized void updateResumptionList(ComponentName componentName) {
- // Add to front of saved list
- if (mSharedPrefs == null) {
- mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
- }
- String componentString = componentName.flattenToString();
- String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
- if (listString == null) {
- listString = componentString;
- } else {
- String[] components = listString.split(QSMediaBrowser.DELIMITER);
- StringBuilder updated = new StringBuilder(componentString);
- int nBrowsers = 1;
- for (int i = 0; i < components.length
- && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
- if (componentString.equals(components[i])) {
- continue;
- }
- updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
- nBrowsers++;
- }
- listString = updated.toString();
- }
- mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
- }
-
- /**
- * Called when a player can't be resumed to give it an opportunity to hide or remove itself
- */
- protected void removePlayer() { }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
index a94f6a87d58a5..5d28178a3b1b0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
@@ -32,17 +32,19 @@ data class MediaData(
val artwork: Icon?,
val actions: List,
val actionsToShowInCompact: List,
- val packageName: String?,
+ val packageName: String,
val token: MediaSession.Token?,
val clickIntent: PendingIntent?,
val device: MediaDeviceData?,
- val notificationKey: String = "INVALID"
+ var resumeAction: Runnable?,
+ val notificationKey: String = "INVALID",
+ var hasCheckedForResume: Boolean = false
)
/** State of a media action. */
data class MediaAction(
val drawable: Drawable?,
- val intent: PendingIntent?,
+ val action: Runnable?,
val contentDescription: CharSequence?
)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
index 67cf21ae10b90..11cbc482459ad 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
@@ -32,9 +32,15 @@ class MediaDataCombineLatest @Inject constructor(
init {
dataSource.addListener(object : MediaDataManager.Listener {
- override fun onMediaDataLoaded(key: String, data: MediaData) {
- entries[key] = data to entries[key]?.second
- update(key)
+ override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
+ if (oldKey != null && !oldKey.equals(key)) {
+ val s = entries[oldKey]?.second
+ entries[key] = data to entries[oldKey]?.second
+ entries.remove(oldKey)
+ } else {
+ entries[key] = data to entries[key]?.second
+ }
+ update(key, oldKey)
}
override fun onMediaDataRemoved(key: String) {
remove(key)
@@ -43,7 +49,7 @@ class MediaDataCombineLatest @Inject constructor(
deviceSource.addListener(object : MediaDeviceManager.Listener {
override fun onMediaDeviceChanged(key: String, data: MediaDeviceData?) {
entries[key] = entries[key]?.first to data
- update(key)
+ update(key, key)
}
override fun onKeyRemoved(key: String) {
remove(key)
@@ -61,13 +67,13 @@ class MediaDataCombineLatest @Inject constructor(
*/
fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
- private fun update(key: String) {
+ private fun update(key: String, oldKey: String?) {
val (entry, device) = entries[key] ?: null to null
if (entry != null && device != null) {
val data = entry.copy(device = device)
val listenersCopy = listeners.toSet()
listenersCopy.forEach {
- it.onMediaDataLoaded(key, data)
+ it.onMediaDataLoaded(key, oldKey, data)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index d949857030832..094c5bef3c18d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -17,6 +17,7 @@
package com.android.systemui.media
import android.app.Notification
+import android.app.PendingIntent
import android.content.ContentResolver
import android.content.Context
import android.graphics.Bitmap
@@ -25,6 +26,7 @@ import android.graphics.Color
import android.graphics.ImageDecoder
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
+import android.media.MediaDescription
import android.media.MediaMetadata
import android.media.session.MediaSession
import android.net.Uri
@@ -32,8 +34,10 @@ import android.service.notification.StatusBarNotification
import android.text.TextUtils
import android.util.Log
import com.android.internal.graphics.ColorUtils
+import com.android.systemui.R
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.statusbar.NotificationMediaManager
import com.android.systemui.statusbar.notification.MediaNotificationProcessor
import com.android.systemui.statusbar.notification.NotificationEntryManager
import com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON
@@ -58,7 +62,7 @@ private const val LUMINOSITY_THRESHOLD = 0.05f
private const val SATURATION_MULTIPLIER = 0.8f
private val LOADING = MediaData(false, 0, null, null, null, null, null,
- emptyList(), emptyList(), null, null, null, null)
+ emptyList(), emptyList(), "INVALID", null, null, null, null)
fun isMediaNotification(sbn: StatusBarNotification): Boolean {
if (!sbn.notification.hasMediaSession()) {
@@ -81,34 +85,92 @@ class MediaDataManager @Inject constructor(
private val mediaControllerFactory: MediaControllerFactory,
private val mediaTimeoutListener: MediaTimeoutListener,
private val notificationEntryManager: NotificationEntryManager,
+ private val mediaResumeListener: MediaResumeListener,
@Background private val backgroundExecutor: Executor,
@Main private val foregroundExecutor: Executor
) {
private val listeners: MutableSet = mutableSetOf()
private val mediaEntries: LinkedHashMap = LinkedHashMap()
+ private val useMediaResumption: Boolean = Utils.useMediaResumption(context)
init {
mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean ->
setTimedOut(token, timedOut) }
addListener(mediaTimeoutListener)
+
+ if (useMediaResumption) {
+ mediaResumeListener.addTrackToResumeCallback = { desc: MediaDescription,
+ resumeAction: Runnable, token: MediaSession.Token, appName: String,
+ appIntent: PendingIntent, packageName: String ->
+ addResumptionControls(desc, resumeAction, token, appName, appIntent, packageName)
+ }
+ mediaResumeListener.resumeComponentFoundCallback = { key: String, action: Runnable? ->
+ mediaEntries.get(key)?.resumeAction = action
+ mediaEntries.get(key)?.hasCheckedForResume = true
+ }
+ addListener(mediaResumeListener)
+ }
}
fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
if (Utils.useQsMediaPlayer(context) && isMediaNotification(sbn)) {
Assert.isMainThread()
- if (!mediaEntries.containsKey(key)) {
- mediaEntries.put(key, LOADING)
+ val oldKey = findExistingEntry(key, sbn.packageName)
+ if (oldKey == null) {
+ val temp = LOADING.copy(packageName = sbn.packageName)
+ mediaEntries.put(key, temp)
+ } else if (oldKey != key) {
+ // Move to new key
+ val oldData = mediaEntries.remove(oldKey)!!
+ mediaEntries.put(key, oldData)
}
- loadMediaData(key, sbn)
+ loadMediaData(key, sbn, oldKey)
} else {
onNotificationRemoved(key)
}
}
- private fun loadMediaData(key: String, sbn: StatusBarNotification) {
+ private fun addResumptionControls(
+ desc: MediaDescription,
+ action: Runnable,
+ token: MediaSession.Token,
+ appName: String,
+ appIntent: PendingIntent,
+ packageName: String
+ ) {
+ // Resume controls don't have a notification key, so store by package name instead
+ if (!mediaEntries.containsKey(packageName)) {
+ val resumeData = LOADING.copy(packageName = packageName, resumeAction = action)
+ mediaEntries.put(packageName, resumeData)
+ }
backgroundExecutor.execute {
- loadMediaDataInBg(key, sbn)
+ loadMediaDataInBg(desc, action, token, appName, appIntent, packageName)
+ }
+ }
+
+ /**
+ * Check if there is an existing entry that matches the key or package name.
+ * Returns the key that matches, or null if not found.
+ */
+ private fun findExistingEntry(key: String, packageName: String): String? {
+ if (mediaEntries.containsKey(key)) {
+ return key
+ }
+ // Check if we already had a resume player
+ if (mediaEntries.containsKey(packageName)) {
+ return packageName
+ }
+ return null
+ }
+
+ private fun loadMediaData(
+ key: String,
+ sbn: StatusBarNotification,
+ oldKey: String?
+ ) {
+ backgroundExecutor.execute {
+ loadMediaDataInBg(key, sbn, oldKey)
}
}
@@ -132,7 +194,50 @@ class MediaDataManager @Inject constructor(
}
}
- private fun loadMediaDataInBg(key: String, sbn: StatusBarNotification) {
+ private fun loadMediaDataInBg(
+ desc: MediaDescription,
+ resumeAction: Runnable,
+ token: MediaSession.Token,
+ appName: String,
+ appIntent: PendingIntent,
+ packageName: String
+ ) {
+ if (resumeAction == null) {
+ Log.e(TAG, "Resume action cannot be null")
+ return
+ }
+
+ if (TextUtils.isEmpty(desc.title)) {
+ Log.e(TAG, "Description incomplete")
+ return
+ }
+
+ Log.d(TAG, "adding track from browser: $desc")
+
+ // Album art
+ var artworkBitmap = desc.iconBitmap
+ if (artworkBitmap == null && desc.iconUri != null) {
+ artworkBitmap = loadBitmapFromUri(desc.iconUri!!)
+ }
+ val artworkIcon = if (artworkBitmap != null) {
+ Icon.createWithBitmap(artworkBitmap)
+ } else {
+ null
+ }
+
+ val mediaAction = getResumeMediaAction(resumeAction)
+ foregroundExecutor.execute {
+ onMediaDataLoaded(packageName, null, MediaData(true, Color.DKGRAY, appName,
+ null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
+ packageName, token, appIntent, null, resumeAction, packageName))
+ }
+ }
+
+ private fun loadMediaDataInBg(
+ key: String,
+ sbn: StatusBarNotification,
+ oldKey: String?
+ ) {
val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
as MediaSession.Token?
val metadata = mediaControllerFactory.create(token).metadata
@@ -234,16 +339,23 @@ class MediaDataManager @Inject constructor(
}
val mediaAction = MediaAction(
action.getIcon().loadDrawable(packageContext),
- action.actionIntent,
+ Runnable {
+ try {
+ action.actionIntent.send()
+ } catch (e: PendingIntent.CanceledException) {
+ Log.d(TAG, "Intent canceled", e)
+ }
+ },
action.title)
actionIcons.add(mediaAction)
}
}
+ val resumeAction: Runnable? = mediaEntries.get(key)?.resumeAction
foregroundExecutor.execute {
- onMediaDataLoaded(key, MediaData(true, bgColor, app, smallIconDrawable, artist, song,
- artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
- notif.contentIntent, null, key))
+ onMediaDataLoaded(key, oldKey, MediaData(true, bgColor, app, smallIconDrawable, artist,
+ song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
+ notif.contentIntent, null, resumeAction, key))
}
}
@@ -257,7 +369,7 @@ class MediaDataManager @Inject constructor(
val albumArt = loadBitmapFromUri(Uri.parse(uriString))
if (albumArt != null) {
Log.d(TAG, "loaded art from $uri")
- break
+ return albumArt
}
}
}
@@ -283,27 +395,52 @@ class MediaDataManager @Inject constructor(
val source = ImageDecoder.createSource(context.getContentResolver(), uri)
return try {
- ImageDecoder.decodeBitmap(source)
+ ImageDecoder.decodeBitmap(source) {
+ decoder, info, source -> decoder.isMutableRequired = true
+ }
} catch (e: IOException) {
e.printStackTrace()
null
}
}
- fun onMediaDataLoaded(key: String, data: MediaData) {
+ private fun getResumeMediaAction(action: Runnable): MediaAction {
+ return MediaAction(
+ context.getDrawable(R.drawable.lb_ic_play),
+ action,
+ context.getString(R.string.controls_media_resume)
+ )
+ }
+
+ fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
Assert.isMainThread()
if (mediaEntries.containsKey(key)) {
// Otherwise this was removed already
mediaEntries.put(key, data)
val listenersCopy = listeners.toSet()
listenersCopy.forEach {
- it.onMediaDataLoaded(key, data)
+ it.onMediaDataLoaded(key, oldKey, data)
}
}
}
fun onNotificationRemoved(key: String) {
Assert.isMainThread()
+ if (useMediaResumption && mediaEntries.get(key)?.resumeAction != null) {
+ Log.d(TAG, "Not removing $key because resumable")
+ // Move to resume key aka package name
+ val data = mediaEntries.remove(key)!!
+ val resumeAction = getResumeMediaAction(data.resumeAction!!)
+ val updated = data.copy(token = null, actions = listOf(resumeAction),
+ actionsToShowInCompact = listOf(0))
+ mediaEntries.put(data.packageName, updated)
+ // Notify listeners of "new" controls
+ val listenersCopy = listeners.toSet()
+ listenersCopy.forEach {
+ it.onMediaDataLoaded(data.packageName, key, updated)
+ }
+ return
+ }
val removed = mediaEntries.remove(key)
if (removed != null) {
val listenersCopy = listeners.toSet()
@@ -316,19 +453,32 @@ class MediaDataManager @Inject constructor(
/**
* Are there any media notifications active?
*/
- fun hasActiveMedia() = mediaEntries.isNotEmpty()
+ fun hasActiveMedia() = mediaEntries.any({ isActive(it.value) })
- fun hasAnyMedia(): Boolean {
- // TODO: implement this when we implemented resumption
- return hasActiveMedia()
+ fun isActive(data: MediaData): Boolean {
+ if (data.token == null) {
+ return false
+ }
+ val controller = mediaControllerFactory.create(data.token)
+ val state = controller?.playbackState?.state
+ return state != null && NotificationMediaManager.isActiveState(state)
}
+ /**
+ * Are there any media entries, including resume controls?
+ */
+ fun hasAnyMedia() = mediaEntries.isNotEmpty()
+
interface Listener {
/**
- * Called whenever there's new MediaData Loaded for the consumption in views
+ * Called whenever there's new MediaData Loaded for the consumption in views.
+ *
+ * oldKey is provided to check whether the view has changed keys, which can happen when a
+ * player has gone from resume state (key is package name) to active state (key is
+ * notification key) or vice versa.
*/
- fun onMediaDataLoaded(key: String, data: MediaData) {}
+ fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {}
/**
* Called whenever a previously existing Media notification was removed
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
index 552fea63a278a..2f521ea392429 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
@@ -16,11 +16,8 @@
package com.android.systemui.media
-import android.app.Notification
import android.content.Context
-import android.service.notification.StatusBarNotification
import android.media.MediaRouter2Manager
-import android.media.session.MediaSession
import android.media.session.MediaController
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
@@ -38,11 +35,16 @@ class MediaDeviceManager @Inject constructor(
private val localMediaManagerFactory: LocalMediaManagerFactory,
private val mr2manager: MediaRouter2Manager,
private val featureFlag: MediaFeatureFlag,
- @Main private val fgExecutor: Executor
-) {
+ @Main private val fgExecutor: Executor,
+ private val mediaDataManager: MediaDataManager
+) : MediaDataManager.Listener {
private val listeners: MutableSet = mutableSetOf()
private val entries: MutableMap = mutableMapOf()
+ init {
+ mediaDataManager.addListener(this)
+ }
+
/**
* Add a listener for changes to the media route (ie. device).
*/
@@ -53,23 +55,25 @@ class MediaDeviceManager @Inject constructor(
*/
fun removeListener(listener: Listener) = listeners.remove(listener)
- fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
- if (featureFlag.enabled && isMediaNotification(sbn)) {
+ override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
+ if (featureFlag.enabled) {
+ if (oldKey != null && oldKey != key) {
+ val oldToken = entries.remove(oldKey)
+ oldToken?.stop()
+ }
var tok = entries[key]
- if (tok == null) {
- val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
- as MediaSession.Token?
- val controller = MediaController(context, token)
- tok = Token(key, controller, localMediaManagerFactory.create(sbn.packageName))
+ if (tok == null && data.token != null) {
+ val controller = MediaController(context, data.token!!)
+ tok = Token(key, controller, localMediaManagerFactory.create(data.packageName))
entries[key] = tok
tok.start()
}
} else {
- onNotificationRemoved(key)
+ onMediaDataRemoved(key)
}
}
- fun onNotificationRemoved(key: String) {
+ override fun onMediaDataRemoved(key: String) {
val token = entries.remove(key)
token?.stop()
token?.let {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
index e904e935b0e05..2bd8c0cbeab24 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
@@ -50,7 +50,7 @@ class MediaHost @Inject constructor(
}
private val listener = object : MediaDataManager.Listener {
- override fun onMediaDataLoaded(key: String, data: MediaData) {
+ override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
updateViewVisibility()
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
new file mode 100644
index 0000000000000..6bbe0d1651ddf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2020 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 android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager
+import android.media.MediaDescription
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import android.os.UserHandle
+import android.service.media.MediaBrowserService
+import android.util.Log
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.Utils
+import java.util.concurrent.ConcurrentLinkedQueue
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import javax.inject.Singleton
+
+private const val TAG = "MediaResumeListener"
+
+private const val MEDIA_PREFERENCES = "media_control_prefs"
+private const val MEDIA_PREFERENCE_KEY = "browser_components"
+
+@Singleton
+class MediaResumeListener @Inject constructor(
+ private val context: Context,
+ private val broadcastDispatcher: BroadcastDispatcher,
+ @Background private val backgroundExecutor: Executor
+) : MediaDataManager.Listener {
+
+ private val useMediaResumption: Boolean = Utils.useMediaResumption(context)
+ private val resumeComponents: ConcurrentLinkedQueue = ConcurrentLinkedQueue()
+
+ lateinit var addTrackToResumeCallback: (
+ MediaDescription,
+ Runnable,
+ MediaSession.Token,
+ String,
+ PendingIntent,
+ String
+ ) -> Unit
+ lateinit var resumeComponentFoundCallback: (String, Runnable?) -> Unit
+
+ private var mediaBrowser: ResumeMediaBrowser? = null
+
+ private val unlockReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (Intent.ACTION_USER_UNLOCKED == intent.action) {
+ loadMediaResumptionControls()
+ }
+ }
+ }
+
+ private val mediaBrowserCallback = object : ResumeMediaBrowser.Callback() {
+ override fun addTrack(
+ desc: MediaDescription,
+ component: ComponentName,
+ browser: ResumeMediaBrowser
+ ) {
+ val token = browser.token
+ val appIntent = browser.appIntent
+ val pm = context.getPackageManager()
+ var appName: CharSequence = component.packageName
+ val resumeAction = getResumeAction(component)
+ try {
+ appName = pm.getApplicationLabel(
+ pm.getApplicationInfo(component.packageName, 0))
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.e(TAG, "Error getting package information", e)
+ }
+
+ Log.d(TAG, "Adding resume controls $desc")
+ addTrackToResumeCallback(desc, resumeAction, token, appName.toString(), appIntent,
+ component.packageName)
+ }
+ }
+
+ init {
+ if (useMediaResumption) {
+ val unlockFilter = IntentFilter()
+ unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED)
+ broadcastDispatcher.registerReceiver(unlockReceiver, unlockFilter, null, UserHandle.ALL)
+ loadSavedComponents()
+ }
+ }
+
+ private fun loadSavedComponents() {
+ val userContext = context.createContextAsUser(context.getUser(), 0)
+ val prefs = userContext.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
+ val listString = prefs.getString(MEDIA_PREFERENCE_KEY, null)
+ val components = listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())
+ ?.dropLastWhile { it.isEmpty() }
+ components?.forEach {
+ val info = it.split("/")
+ val packageName = info[0]
+ val className = info[1]
+ val component = ComponentName(packageName, className)
+ resumeComponents.add(component)
+ }
+ Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}")
+ }
+
+ /**
+ * Load controls for resuming media, if available
+ */
+ private fun loadMediaResumptionControls() {
+ if (!useMediaResumption) {
+ return
+ }
+
+ resumeComponents.forEach {
+ val browser = ResumeMediaBrowser(context, mediaBrowserCallback, it)
+ browser.findRecentMedia()
+ }
+ broadcastDispatcher.unregisterReceiver(unlockReceiver) // only need to load once
+ }
+
+ override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
+ if (useMediaResumption) {
+ // If this had been started from a resume state, disconnect now that it's live
+ mediaBrowser?.disconnect()
+ // If we don't have a resume action, check if we haven't already
+ if (data.resumeAction == null && !data.hasCheckedForResume) {
+ // TODO also check for a media button receiver intended for restarting (b/154127084)
+ Log.d(TAG, "Checking for service component for " + data.packageName)
+ val pm = context.packageManager
+ val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE)
+ val resumeInfo = pm.queryIntentServices(serviceIntent, 0)
+
+ val inf = resumeInfo?.filter {
+ it.serviceInfo.packageName == data.packageName
+ }
+ if (inf != null && inf.size > 0) {
+ backgroundExecutor.execute {
+ tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName)
+ }
+ } else {
+ // No service found
+ resumeComponentFoundCallback(key, null)
+ }
+ }
+ }
+ }
+
+ /**
+ * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
+ * component to the list of resumption components
+ */
+ private fun tryUpdateResumptionList(key: String, componentName: ComponentName) {
+ Log.d(TAG, "Testing if we can connect to $componentName")
+ mediaBrowser?.disconnect()
+ mediaBrowser = ResumeMediaBrowser(context,
+ object : ResumeMediaBrowser.Callback() {
+ override fun onConnected() {
+ Log.d(TAG, "yes we can resume with $componentName")
+ resumeComponentFoundCallback(key, getResumeAction(componentName))
+ updateResumptionList(componentName)
+ mediaBrowser?.disconnect()
+ mediaBrowser = null
+ }
+
+ override fun onError() {
+ Log.e(TAG, "Cannot resume with $componentName")
+ resumeComponentFoundCallback(key, null)
+ mediaBrowser?.disconnect()
+ mediaBrowser = null
+ }
+ },
+ componentName)
+ mediaBrowser?.testConnection()
+ }
+
+ /**
+ * Add the component to the saved list of media browser services, checking for duplicates and
+ * removing older components that exceed the maximum limit
+ * @param componentName
+ */
+ private fun updateResumptionList(componentName: ComponentName) {
+ // Remove if exists
+ resumeComponents.remove(componentName)
+ // Insert at front of queue
+ resumeComponents.add(componentName)
+ // Remove old components if over the limit
+ if (resumeComponents.size > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
+ resumeComponents.remove()
+ }
+
+ // Save changes
+ val sb = StringBuilder()
+ resumeComponents.forEach {
+ sb.append(it.flattenToString())
+ sb.append(ResumeMediaBrowser.DELIMITER)
+ }
+ val userContext = context.createContextAsUser(context.getUser(), 0)
+ val prefs = userContext.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
+ prefs.edit().putString(MEDIA_PREFERENCE_KEY, sb.toString()).apply()
+ }
+
+ /**
+ * Get a runnable which will resume media playback
+ */
+ private fun getResumeAction(componentName: ComponentName): Runnable {
+ return Runnable {
+ mediaBrowser?.disconnect()
+ mediaBrowser = ResumeMediaBrowser(context,
+ object : ResumeMediaBrowser.Callback() {
+ override fun onConnected() {
+ if (mediaBrowser?.token == null) {
+ Log.e(TAG, "Error after connect")
+ mediaBrowser?.disconnect()
+ mediaBrowser = null
+ return
+ }
+ Log.d(TAG, "Connected for restart $componentName")
+ val controller = MediaController(context, mediaBrowser!!.token)
+ val controls = controller.transportControls
+ controls.prepare()
+ controls.play()
+ }
+
+ override fun onError() {
+ Log.e(TAG, "Resume failed for $componentName")
+ mediaBrowser?.disconnect()
+ mediaBrowser = null
+ }
+ },
+ componentName)
+ mediaBrowser?.restart()
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
index 92a1ab1b1871c..359c2f5e297cc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
@@ -45,7 +45,7 @@ class MediaTimeoutListener @Inject constructor(
lateinit var timeoutCallback: (String, Boolean) -> Unit
- override fun onMediaDataLoaded(key: String, data: MediaData) {
+ override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
if (mediaListeners.containsKey(key)) {
return
}
@@ -67,15 +67,20 @@ class MediaTimeoutListener @Inject constructor(
var timedOut = false
- private val mediaController = mediaControllerFactory.create(data.token)
+ // Resume controls may have null token
+ private val mediaController = if (data.token != null) {
+ mediaControllerFactory.create(data.token)
+ } else {
+ null
+ }
private var cancellation: Runnable? = null
init {
- mediaController.registerCallback(this)
+ mediaController?.registerCallback(this)
}
fun destroy() {
- mediaController.unregisterCallback(this)
+ mediaController?.unregisterCallback(this)
}
override fun onPlaybackStateChanged(state: PlaybackState?) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
index 8ab30c75c7eb2..3557b04a57bc0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
@@ -12,14 +12,12 @@ import android.widget.LinearLayout
import androidx.core.view.GestureDetectorCompat
import com.android.systemui.R
import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.PageIndicator
import com.android.systemui.statusbar.notification.VisualStabilityManager
import com.android.systemui.util.animation.UniqueObjectHostView
import com.android.systemui.util.animation.requiresRemeasuring
import com.android.systemui.util.concurrency.DelayableExecutor
-import java.util.concurrent.Executor
import javax.inject.Inject
import javax.inject.Singleton
@@ -32,7 +30,6 @@ private const val FLING_SLOP = 1000000
@Singleton
class MediaViewManager @Inject constructor(
private val context: Context,
- @Main private val foregroundExecutor: Executor,
@Background private val backgroundExecutor: DelayableExecutor,
private val visualStabilityManager: VisualStabilityManager,
private val activityStarter: ActivityStarter,
@@ -147,8 +144,8 @@ class MediaViewManager @Inject constructor(
visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback,
true /* persistent */)
mediaManager.addListener(object : MediaDataManager.Listener {
- override fun onMediaDataLoaded(key: String, data: MediaData) {
- updateView(key, data)
+ override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
+ updateView(key, oldKey, data)
updatePlayerVisibilities()
mediaCarousel.requiresRemeasuring = true
}
@@ -259,11 +256,17 @@ class MediaViewManager @Inject constructor(
}
}
- private fun updateView(key: String, data: MediaData) {
+ private fun updateView(key: String, oldKey: String?, data: MediaData) {
+ // If the key was changed, update entry
+ val oldData = mediaPlayers[oldKey]
+ if (oldData != null) {
+ val oldData = mediaPlayers.remove(oldKey)
+ mediaPlayers.put(key, oldData!!)
+ }
var existingPlayer = mediaPlayers[key]
if (existingPlayer == null) {
- existingPlayer = MediaControlPanel(context, foregroundExecutor, backgroundExecutor,
- activityStarter, mediaHostStatesManager)
+ existingPlayer = MediaControlPanel(context, backgroundExecutor, activityStarter,
+ mediaHostStatesManager)
existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
mediaContent))
mediaPlayers[key] = existingPlayer
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
similarity index 80%
rename from packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java
rename to packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
index a5b73dcbd289a..1e9a303646079 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java
+++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.qs;
+package com.android.systemui.media;
import android.app.PendingIntent;
import android.content.ComponentName;
@@ -27,14 +27,17 @@ import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.os.Bundle;
import android.service.media.MediaBrowserService;
+import android.text.TextUtils;
import android.util.Log;
+import com.android.systemui.util.Utils;
+
import java.util.List;
/**
- * Media browser for managing resumption in QS media controls
+ * Media browser for managing resumption in media controls
*/
-public class QSMediaBrowser {
+public class ResumeMediaBrowser {
/** Maximum number of controls to show on boot */
public static final int MAX_RESUMPTION_CONTROLS = 5;
@@ -42,7 +45,8 @@ public class QSMediaBrowser {
/** Delimiter for saved component names */
public static final String DELIMITER = ":";
- private static final String TAG = "QSMediaBrowser";
+ private static final String TAG = "ResumeMediaBrowser";
+ private boolean mIsEnabled = false;
private final Context mContext;
private final Callback mCallback;
private MediaBrowser mMediaBrowser;
@@ -54,21 +58,25 @@ public class QSMediaBrowser {
* @param callback used to report media items found
* @param componentName Component name of the MediaBrowserService this browser will connect to
*/
- public QSMediaBrowser(Context context, Callback callback, ComponentName componentName) {
+ public ResumeMediaBrowser(Context context, Callback callback, ComponentName componentName) {
+ mIsEnabled = Utils.useMediaResumption(context);
mContext = context;
mCallback = callback;
mComponentName = componentName;
}
/**
- * Connects to the MediaBrowserService and looks for valid media. If a media item is returned
- * by the service, QSMediaBrowser.Callback#addTrack will be called with its MediaDescription.
- * QSMediaBrowser.Callback#onConnected and QSMediaBrowser.Callback#onError will also be called
- * when the initial connection is successful, or an error occurs. Note that it is possible for
- * the service to connect but for no playable tracks to be found later.
- * QSMediaBrowser#disconnect will be called automatically with this function.
+ * Connects to the MediaBrowserService and looks for valid media. If a media item is returned,
+ * ResumeMediaBrowser.Callback#addTrack will be called with the MediaDescription.
+ * ResumeMediaBrowser.Callback#onConnected and ResumeMediaBrowser.Callback#onError will also be
+ * called when the initial connection is successful, or an error occurs.
+ * Note that it is possible for the service to connect but for no playable tracks to be found.
+ * ResumeMediaBrowser#disconnect will be called automatically with this function.
*/
public void findRecentMedia() {
+ if (!mIsEnabled) {
+ return;
+ }
Log.d(TAG, "Connecting to " + mComponentName);
disconnect();
Bundle rootHints = new Bundle();
@@ -86,7 +94,7 @@ public class QSMediaBrowser {
public void onChildrenLoaded(String parentId,
List children) {
if (children.size() == 0) {
- Log.e(TAG, "No children found for " + mComponentName);
+ Log.d(TAG, "No children found for " + mComponentName);
return;
}
// We ask apps to return a playable item as the first child when sending
@@ -94,23 +102,24 @@ public class QSMediaBrowser {
MediaBrowser.MediaItem child = children.get(0);
MediaDescription desc = child.getDescription();
if (child.isPlayable()) {
- mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), QSMediaBrowser.this);
+ mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(),
+ ResumeMediaBrowser.this);
} else {
- Log.e(TAG, "Child found but not playable for " + mComponentName);
+ Log.d(TAG, "Child found but not playable for " + mComponentName);
}
disconnect();
}
@Override
public void onError(String parentId) {
- Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId);
+ Log.d(TAG, "Subscribe error for " + mComponentName + ": " + parentId);
mCallback.onError();
disconnect();
}
@Override
public void onError(String parentId, Bundle options) {
- Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId
+ Log.d(TAG, "Subscribe error for " + mComponentName + ": " + parentId
+ ", options: " + options);
mCallback.onError();
disconnect();
@@ -149,7 +158,7 @@ public class QSMediaBrowser {
*/
@Override
public void onConnectionFailed() {
- Log.e(TAG, "Connection failed for " + mComponentName);
+ Log.d(TAG, "Connection failed for " + mComponentName);
mCallback.onError();
disconnect();
}
@@ -167,11 +176,15 @@ public class QSMediaBrowser {
}
/**
- * Connects to the MediaBrowserService and starts playback. QSMediaBrowser.Callback#onError or
- * QSMediaBrowser.Callback#onConnected will be called depending on whether it was successful.
- * QSMediaBrowser#disconnect should be called after this to ensure the connection is closed.
+ * Connects to the MediaBrowserService and starts playback.
+ * ResumeMediaBrowser.Callback#onError or ResumeMediaBrowser.Callback#onConnected will be called
+ * depending on whether it was successful.
+ * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed.
*/
public void restart() {
+ if (!mIsEnabled) {
+ return;
+ }
disconnect();
Bundle rootHints = new Bundle();
rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
@@ -224,18 +237,21 @@ public class QSMediaBrowser {
/**
* Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser.
- * QSMediaBrowser.Callback#onError or QSMediaBrowser.Callback#onConnected will be called
+ * ResumeMediaBrowser.Callback#onError or ResumeMediaBrowser.Callback#onConnected will be called
* depending on whether it was successful.
- * QSMediaBrowser#disconnect should be called after this to ensure the connection is closed.
+ * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed.
*/
public void testConnection() {
+ if (!mIsEnabled) {
+ return;
+ }
disconnect();
final MediaBrowser.ConnectionCallback connectionCallback =
new MediaBrowser.ConnectionCallback() {
@Override
public void onConnected() {
Log.d(TAG, "connected");
- if (mMediaBrowser.getRoot() == null) {
+ if (TextUtils.isEmpty(mMediaBrowser.getRoot())) {
mCallback.onError();
} else {
mCallback.onConnected();
@@ -264,7 +280,7 @@ public class QSMediaBrowser {
}
/**
- * Interface to handle results from QSMediaBrowser
+ * Interface to handle results from ResumeMediaBrowser
*/
public static class Callback {
/**
@@ -286,7 +302,7 @@ public class QSMediaBrowser {
* @param browser reference to the browser
*/
public void addTrack(MediaDescription track, ComponentName component,
- QSMediaBrowser browser) {
+ ResumeMediaBrowser browser) {
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
index 06821cd615a56..75ad06962afe4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
@@ -91,8 +91,14 @@ class SeekBarViewModel(val bgExecutor: DelayableExecutor) {
playbackState = state
if (shouldPollPlaybackPosition()) {
checkPlaybackPosition()
+ } else if (PlaybackState.STATE_NONE.equals(playbackState)) {
+ clearController()
}
}
+
+ override fun onSessionDestroyed() {
+ clearController()
+ }
}
/** Listening state (QS open or closed) is used to control polling of progress. */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
index 191d4757258d5..94b4cee92965c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
@@ -41,7 +41,6 @@ import com.android.systemui.qs.customize.QSCustomizer;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.tuner.TunerService.Tunable;
-import com.android.systemui.util.Utils;
import java.util.ArrayList;
import java.util.Collection;
@@ -82,8 +81,7 @@ public class QuickQSPanel extends QSPanel {
MediaHost mediaHost,
UiEventLogger uiEventLogger
) {
- super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost,
- uiEventLogger);
+ super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost, uiEventLogger);
if (mFooter != null) {
removeView(mFooter.getView());
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index 217148df60e26..5628a24f40ef1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -52,7 +52,6 @@ import com.android.systemui.Interpolators;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.media.MediaDataManager;
-import com.android.systemui.media.MediaDeviceManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.dagger.StatusBarModule;
import com.android.systemui.statusbar.notification.NotificationEntryListener;
@@ -102,6 +101,12 @@ public class NotificationMediaManager implements Dumpable {
PAUSED_MEDIA_STATES.add(PlaybackState.STATE_PAUSED);
PAUSED_MEDIA_STATES.add(PlaybackState.STATE_ERROR);
}
+ private static final HashSet INACTIVE_MEDIA_STATES = new HashSet<>();
+ static {
+ INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_NONE);
+ INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_STOPPED);
+ INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_ERROR);
+ }
private final NotificationEntryManager mEntryManager;
private final MediaDataManager mMediaDataManager;
@@ -190,8 +195,7 @@ public class NotificationMediaManager implements Dumpable {
KeyguardBypassController keyguardBypassController,
@Main DelayableExecutor mainExecutor,
DeviceConfigProxy deviceConfig,
- MediaDataManager mediaDataManager,
- MediaDeviceManager mediaDeviceManager) {
+ MediaDataManager mediaDataManager) {
mContext = context;
mMediaArtworkProcessor = mediaArtworkProcessor;
mKeyguardBypassController = keyguardBypassController;
@@ -212,13 +216,11 @@ public class NotificationMediaManager implements Dumpable {
@Override
public void onPendingEntryAdded(NotificationEntry entry) {
mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn());
- mediaDeviceManager.onNotificationAdded(entry.getKey(), entry.getSbn());
}
@Override
public void onPreEntryUpdated(NotificationEntry entry) {
mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn());
- mediaDeviceManager.onNotificationAdded(entry.getKey(), entry.getSbn());
}
@Override
@@ -239,7 +241,6 @@ public class NotificationMediaManager implements Dumpable {
int reason) {
onNotificationRemoved(entry.getKey());
mediaDataManager.onNotificationRemoved(entry.getKey());
- mediaDeviceManager.onNotificationRemoved(entry.getKey());
}
});
@@ -252,10 +253,24 @@ public class NotificationMediaManager implements Dumpable {
mPropertiesChangedListener);
}
+ /**
+ * Check if a state should be considered actively playing
+ * @param state a PlaybackState
+ * @return true if playing
+ */
public static boolean isPlayingState(int state) {
return !PAUSED_MEDIA_STATES.contains(state);
}
+ /**
+ * Check if a state should be considered active (playing or paused)
+ * @param state a PlaybackState
+ * @return true if active
+ */
+ public static boolean isActiveState(int state) {
+ return !INACTIVE_MEDIA_STATES.contains(state);
+ }
+
public void setUpWithPresenter(NotificationPresenter presenter) {
mPresenter = presenter;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
index c988e1251d3f4..84c8db3218e7c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
@@ -24,7 +24,6 @@ import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.media.MediaDataManager;
-import com.android.systemui.media.MediaDeviceManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.ActionClickLogger;
import com.android.systemui.statusbar.CommandQueue;
@@ -51,8 +50,6 @@ import com.android.systemui.tracing.ProtoTracer;
import com.android.systemui.util.DeviceConfigProxy;
import com.android.systemui.util.concurrency.DelayableExecutor;
-import java.util.concurrent.Executor;
-
import javax.inject.Singleton;
import dagger.Lazy;
@@ -105,8 +102,7 @@ public interface StatusBarDependenciesModule {
KeyguardBypassController keyguardBypassController,
@Main DelayableExecutor mainExecutor,
DeviceConfigProxy deviceConfigProxy,
- MediaDataManager mediaDataManager,
- MediaDeviceManager mediaDeviceManager) {
+ MediaDataManager mediaDataManager) {
return new NotificationMediaManager(
context,
statusBarLazy,
@@ -116,8 +112,7 @@ public interface StatusBarDependenciesModule {
keyguardBypassController,
mainExecutor,
deviceConfigProxy,
- mediaDataManager,
- mediaDeviceManager);
+ mediaDataManager);
}
/** */
diff --git a/packages/SystemUI/src/com/android/systemui/util/Utils.java b/packages/SystemUI/src/com/android/systemui/util/Utils.java
index b1792d003290d..5c9db54a0f002 100644
--- a/packages/SystemUI/src/com/android/systemui/util/Utils.java
+++ b/packages/SystemUI/src/com/android/systemui/util/Utils.java
@@ -133,4 +133,13 @@ public class Utils {
Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1);
return flag > 0;
}
+
+ /**
+ * Allow media resumption controls. Requires {@link #useQsMediaPlayer(Context)} to be enabled.
+ * Off by default, but can be enabled by setting to 1
+ */
+ public static boolean useMediaResumption(Context context) {
+ int flag = Settings.System.getInt(context.getContentResolver(), "qs_media_resumption", 0);
+ return useQsMediaPlayer(context) && flag > 0;
+ }
}
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 9d2b6f4deb148..1ba36e19b404b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -67,7 +67,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
private lateinit var player: MediaControlPanel
- private lateinit var fgExecutor: FakeExecutor
private lateinit var bgExecutor: FakeExecutor
@Mock private lateinit var activityStarter: ActivityStarter
@@ -97,14 +96,12 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Before
fun setUp() {
- fgExecutor = FakeExecutor(FakeSystemClock())
bgExecutor = FakeExecutor(FakeSystemClock())
activityStarter = mock(ActivityStarter::class.java)
mediaHostStatesManager = mock(MediaHostStatesManager::class.java)
- player = MediaControlPanel(context, fgExecutor, bgExecutor, activityStarter,
- mediaHostStatesManager)
+ player = MediaControlPanel(context, bgExecutor, activityStarter, mediaHostStatesManager)
// Mock out a view holder for the player to attach to.
holder = mock(PlayerViewHolder::class.java)
@@ -171,7 +168,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun bindWhenUnattached() {
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, null, null, device)
+ emptyList(), PACKAGE, null, null, device, null)
player.bind(state)
assertThat(player.isPlaying()).isFalse()
}
@@ -180,7 +177,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindText() {
player.attach(holder)
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, session.getSessionToken(), null, device)
+ emptyList(), PACKAGE, session.getSessionToken(), null, device, null)
player.bind(state)
assertThat(appName.getText()).isEqualTo(APP)
assertThat(titleText.getText()).isEqualTo(TITLE)
@@ -191,7 +188,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindBackgroundColor() {
player.attach(holder)
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, session.getSessionToken(), null, device)
+ emptyList(), PACKAGE, session.getSessionToken(), null, device, null)
player.bind(state)
val list = ArgumentCaptor.forClass(ColorStateList::class.java)
verify(view).setBackgroundTintList(list.capture())
@@ -202,7 +199,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindDevice() {
player.attach(holder)
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, session.getSessionToken(), null, device)
+ emptyList(), PACKAGE, session.getSessionToken(), null, device, null)
player.bind(state)
assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
assertThat(seamless.isEnabled()).isTrue()
@@ -212,7 +209,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindDisabledDevice() {
player.attach(holder)
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, session.getSessionToken(), null, disabledDevice)
+ emptyList(), PACKAGE, session.getSessionToken(), null, disabledDevice, null)
player.bind(state)
assertThat(seamless.isEnabled()).isFalse()
assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString(
@@ -223,7 +220,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindNullDevice() {
player.attach(holder)
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, session.getSessionToken(), null, null)
+ emptyList(), PACKAGE, session.getSessionToken(), null, null, null)
player.bind(state)
assertThat(seamless.isEnabled()).isTrue()
assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString(
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 48e3b0a9d9932..bed5c9eb6df55 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
@@ -79,16 +79,16 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
mManager.addListener(mListener);
mMediaData = new MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null,
- new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, KEY);
+ new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, null, KEY, false);
mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME);
}
@Test
public void eventNotEmittedWithoutDevice() {
// WHEN data source emits an event without device data
- mDataListener.onMediaDataLoaded(KEY, mMediaData);
+ mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
// THEN an event isn't emitted
- verify(mListener, never()).onMediaDataLoaded(eq(KEY), any());
+ verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any());
}
@Test
@@ -96,7 +96,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
// WHEN device source emits an event without media data
mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
// THEN an event isn't emitted
- verify(mListener, never()).onMediaDataLoaded(eq(KEY), any());
+ verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any());
}
@Test
@@ -104,22 +104,22 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
// GIVEN that a device event has already been received
mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
// WHEN media event is received
- mDataListener.onMediaDataLoaded(KEY, mMediaData);
+ mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
// THEN the listener receives a combined event
ArgumentCaptor captor = ArgumentCaptor.forClass(MediaData.class);
- verify(mListener).onMediaDataLoaded(eq(KEY), captor.capture());
+ verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture());
assertThat(captor.getValue().getDevice()).isNotNull();
}
@Test
public void emitEventAfterMediaFirst() {
// GIVEN that media event has already been received
- mDataListener.onMediaDataLoaded(KEY, mMediaData);
+ mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
// WHEN device event is received
mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
// THEN the listener receives a combined event
ArgumentCaptor captor = ArgumentCaptor.forClass(MediaData.class);
- verify(mListener).onMediaDataLoaded(eq(KEY), captor.capture());
+ verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture());
assertThat(captor.getValue().getDevice()).isNotNull();
}
@@ -133,7 +133,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
@Test
public void mediaDataRemovedAfterMediaEvent() {
- mDataListener.onMediaDataLoaded(KEY, mMediaData);
+ mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
mDataListener.onMediaDataRemoved(KEY);
verify(mListener).onMediaDataRemoved(eq(KEY));
}
@@ -145,6 +145,18 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
verify(mListener).onMediaDataRemoved(eq(KEY));
}
+ @Test
+ public void mediaDataKeyUpdated() {
+ // GIVEN that device and media events have already been received
+ mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
+ mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
+ // WHEN the key is changed
+ mDataListener.onMediaDataLoaded("NEW_KEY", KEY, mMediaData);
+ // THEN the listener gets a load event with the correct keys
+ ArgumentCaptor captor = ArgumentCaptor.forClass(MediaData.class);
+ verify(mListener).onMediaDataLoaded(eq("NEW_KEY"), any(), captor.capture());
+ }
+
private MediaDataManager.Listener captureDataListener() {
ArgumentCaptor captor = ArgumentCaptor.forClass(
MediaDataManager.Listener.class);
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 c0aef8adc4af5..3a3140f2ff53e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
@@ -23,8 +23,6 @@ import android.media.MediaRouter2Manager
import android.media.RoutingSessionInfo
import android.media.session.MediaSession
import android.media.session.PlaybackState
-import android.os.Process
-import android.service.notification.StatusBarNotification
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
@@ -67,6 +65,7 @@ private fun eq(value: T): T = Mockito.eq(value) ?: value
public class MediaDeviceManagerTest : SysuiTestCase() {
private lateinit var manager: MediaDeviceManager
+ @Mock private lateinit var mediaDataManager: MediaDataManager
@Mock private lateinit var lmmFactory: LocalMediaManagerFactory
@Mock private lateinit var lmm: LocalMediaManager
@Mock private lateinit var mr2: MediaRouter2Manager
@@ -80,13 +79,14 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
private lateinit var metadataBuilder: MediaMetadata.Builder
private lateinit var playbackBuilder: PlaybackState.Builder
private lateinit var notifBuilder: Notification.Builder
- private lateinit var sbn: StatusBarNotification
+ private lateinit var mediaData: MediaData
@JvmField @Rule val mockito = MockitoJUnit.rule()
@Before
fun setUp() {
fakeExecutor = FakeExecutor(FakeSystemClock())
- manager = MediaDeviceManager(context, lmmFactory, mr2, featureFlag, fakeExecutor)
+ manager = MediaDeviceManager(context, lmmFactory, mr2, featureFlag, fakeExecutor,
+ mediaDataManager)
manager.addListener(listener)
// Configure mocks.
@@ -117,8 +117,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
setSmallIcon(android.R.drawable.ic_media_pause)
setStyle(Notification.MediaStyle().setMediaSession(session.getSessionToken()))
}
- sbn = StatusBarNotification(PACKAGE, PACKAGE, 0, "TAG", Process.myUid(), 0, 0,
- notifBuilder.build(), Process.myUserHandle(), 0)
+ mediaData = MediaData(true, 0, PACKAGE, null, null, SESSION_TITLE, null,
+ emptyList(), emptyList(), PACKAGE, session.sessionToken, null, null, null)
}
@After
@@ -128,33 +128,33 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
@Test
fun removeUnknown() {
- manager.onNotificationRemoved("unknown")
+ manager.onMediaDataRemoved("unknown")
}
@Test
fun addNotification() {
- manager.onNotificationAdded(KEY, sbn)
+ manager.onMediaDataLoaded(KEY, null, mediaData)
verify(lmmFactory).create(PACKAGE)
}
@Test
fun featureDisabled() {
whenever(featureFlag.enabled).thenReturn(false)
- manager.onNotificationAdded(KEY, sbn)
+ manager.onMediaDataLoaded(KEY, null, mediaData)
verify(lmmFactory, never()).create(PACKAGE)
}
@Test
fun addAndRemoveNotification() {
- manager.onNotificationAdded(KEY, sbn)
- manager.onNotificationRemoved(KEY)
+ manager.onMediaDataLoaded(KEY, null, mediaData)
+ manager.onMediaDataRemoved(KEY)
verify(lmm).unregisterCallback(any())
}
@Test
fun deviceEventOnAddNotification() {
// WHEN a notification is added
- manager.onNotificationAdded(KEY, sbn)
+ manager.onMediaDataLoaded(KEY, null, mediaData)
val deviceCallback = captureCallback()
// THEN the update is dispatched to the listener
val data = captureDeviceData(KEY)
@@ -165,7 +165,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
@Test
fun deviceListUpdate() {
- manager.onNotificationAdded(KEY, sbn)
+ manager.onMediaDataLoaded(KEY, null, mediaData)
val deviceCallback = captureCallback()
// WHEN the device list changes
deviceCallback.onDeviceListUpdate(mutableListOf(device))
@@ -179,7 +179,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
@Test
fun selectedDeviceStateChanged() {
- manager.onNotificationAdded(KEY, sbn)
+ manager.onMediaDataLoaded(KEY, null, mediaData)
val deviceCallback = captureCallback()
// WHEN the selected device changes state
deviceCallback.onSelectedDeviceStateChanged(device, 1)
@@ -193,9 +193,9 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
@Test
fun listenerReceivesKeyRemoved() {
- manager.onNotificationAdded(KEY, sbn)
+ manager.onMediaDataLoaded(KEY, null, mediaData)
// WHEN the notification is removed
- manager.onNotificationRemoved(KEY)
+ manager.onMediaDataRemoved(KEY)
// THEN the listener receives key removed event
verify(listener).onKeyRemoved(eq(KEY))
}
@@ -205,7 +205,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
// GIVEN that MR2Manager returns null for routing session
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
// WHEN a notification is added
- manager.onNotificationAdded(KEY, sbn)
+ manager.onMediaDataLoaded(KEY, null, mediaData)
// THEN the device is disabled
val data = captureDeviceData(KEY)
assertThat(data.enabled).isFalse()
@@ -216,7 +216,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
@Test
fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceChanged() {
// GIVEN a notif is added
- manager.onNotificationAdded(KEY, sbn)
+ manager.onMediaDataLoaded(KEY, null, mediaData)
reset(listener)
// AND MR2Manager returns null for routing session
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
@@ -234,7 +234,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
@Test
fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceListUpdate() {
// GIVEN a notif is added
- manager.onNotificationAdded(KEY, sbn)
+ manager.onMediaDataLoaded(KEY, null, mediaData)
reset(listener)
// GIVEN that MR2Manager returns null for routing session
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
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 c21343cb5423b..643a3352c30ce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
@@ -16,7 +16,9 @@
package com.android.systemui.media
+import android.media.MediaMetadata
import android.media.session.MediaController
+import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
@@ -41,6 +43,10 @@ import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
private const val KEY = "KEY"
+private const val PACKAGE = "PKG"
+private const val SESSION_KEY = "SESSION_KEY"
+private const val SESSION_ARTIST = "SESSION_ARTIST"
+private const val SESSION_TITLE = "SESSION_TITLE"
private fun eq(value: T): T = Mockito.eq(value) ?: value
private fun anyObject(): T {
@@ -54,12 +60,15 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Mock private lateinit var mediaControllerFactory: MediaControllerFactory
@Mock private lateinit var mediaController: MediaController
@Mock private lateinit var executor: DelayableExecutor
- @Mock private lateinit var mediaData: MediaData
@Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
@Mock private lateinit var cancellationRunnable: Runnable
@Captor private lateinit var timeoutCaptor: ArgumentCaptor
@Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor
@JvmField @Rule val mockito = MockitoJUnit.rule()
+ private lateinit var metadataBuilder: MediaMetadata.Builder
+ private lateinit var playbackBuilder: PlaybackState.Builder
+ private lateinit var session: MediaSession
+ private lateinit var mediaData: MediaData
private lateinit var mediaTimeoutListener: MediaTimeoutListener
@Before
@@ -68,22 +77,39 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
`when`(executor.executeDelayed(any(), anyLong())).thenReturn(cancellationRunnable)
mediaTimeoutListener = MediaTimeoutListener(mediaControllerFactory, executor)
mediaTimeoutListener.timeoutCallback = timeoutCallback
+
+ // Create a media session and notification for testing.
+ metadataBuilder = MediaMetadata.Builder().apply {
+ putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
+ putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
+ }
+ playbackBuilder = PlaybackState.Builder().apply {
+ setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
+ setActions(PlaybackState.ACTION_PLAY)
+ }
+ session = MediaSession(context, SESSION_KEY).apply {
+ setMetadata(metadataBuilder.build())
+ setPlaybackState(playbackBuilder.build())
+ }
+ session.setActive(true)
+ mediaData = MediaData(true, 0, PACKAGE, null, null, SESSION_TITLE, null,
+ emptyList(), emptyList(), PACKAGE, session.sessionToken, null, null, null)
}
@Test
fun testOnMediaDataLoaded_registersPlaybackListener() {
- mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData)
+ mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
// Ignores is same key
clearInvocations(mediaController)
- mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData)
+ mediaTimeoutListener.onMediaDataLoaded(KEY, KEY, mediaData)
verify(mediaController, never()).registerCallback(anyObject())
}
@Test
fun testOnMediaDataRemoved_unregistersPlaybackListener() {
- mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData)
+ mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
mediaTimeoutListener.onMediaDataRemoved(KEY)
verify(mediaController).unregisterCallback(anyObject())
@@ -124,7 +150,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test
fun testIsTimedOut() {
- mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData)
+ mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
assertThat(mediaTimeoutListener.isTimedOut(KEY)).isFalse()
}
}
\ No newline at end of file