Merge "Add media browser for resumption" into rvc-dev

This commit is contained in:
Beth Thibodeau
2020-04-20 23:47:44 +00:00
committed by Android (Google) Code Review
6 changed files with 666 additions and 118 deletions

View File

@@ -21,20 +21,21 @@ 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.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.Icon;
import android.graphics.drawable.RippleDrawable;
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.service.media.MediaBrowserService;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
@@ -55,6 +56,7 @@ import com.android.settingslib.widget.AdaptiveIcon;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.qs.QSMediaBrowser;
import com.android.systemui.util.Assert;
import java.util.List;
@@ -67,7 +69,7 @@ public class MediaControlPanel {
private static final String TAG = "MediaControlPanel";
@Nullable private final LocalMediaManager mLocalMediaManager;
private final Executor mForegroundExecutor;
private final Executor mBackgroundExecutor;
protected final Executor mBackgroundExecutor;
private Context mContext;
protected LinearLayout mMediaNotifView;
@@ -76,13 +78,18 @@ public class MediaControlPanel {
private MediaController mController;
private int mForegroundColor;
private int mBackgroundColor;
protected ComponentName mRecvComponent;
private MediaDevice mDevice;
protected ComponentName mServiceComponent;
private boolean mIsRegistered = false;
private String mKey;
private final int[] mActionIds;
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;
// Button IDs used in notifications
protected static final int[] NOTIF_ACTION_IDS = {
com.android.internal.R.id.action0,
@@ -154,7 +161,6 @@ public class MediaControlPanel {
* Initialize a new control panel
* @param context
* @param parent
* @param manager
* @param routeManager Manager used to listen for device change events.
* @param layoutId layout resource to use for this control panel
* @param actionIds resource IDs for action buttons in the layout
@@ -198,47 +204,50 @@ public class MediaControlPanel {
/**
* Update the media panel view for the given media session
* @param token
* @param icon
* @param iconDrawable
* @param iconColor
* @param bgColor
* @param contentIntent
* @param appNameString
* @param key
*/
public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor,
public void setMediaSession(MediaSession.Token token, Drawable iconDrawable, int iconColor,
int bgColor, PendingIntent contentIntent, String appNameString, String key) {
mToken = token;
// Ensure that component names are updated if token has changed
if (mToken == null || !mToken.equals(token)) {
mToken = token;
mServiceComponent = null;
mCheckedForResumption = false;
}
mForegroundColor = iconColor;
mBackgroundColor = bgColor;
mController = new MediaController(mContext, mToken);
mKey = key;
MediaMetadata mediaMetadata = mController.getMetadata();
// Try to find a receiver for the media button that matches this app
PackageManager pm = mContext.getPackageManager();
Intent it = new Intent(Intent.ACTION_MEDIA_BUTTON);
List<ResolveInfo> info = pm.queryBroadcastReceiversAsUser(it, 0, mContext.getUser());
if (info != null) {
for (ResolveInfo inf : info) {
if (inf.activityInfo.packageName.equals(mController.getPackageName())) {
mRecvComponent = inf.getComponentInfo().getComponentName();
// 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
String pkgName = mController.getPackageName();
if (mServiceComponent == null && !mCheckedForResumption) {
Log.d(TAG, "Checking for service component");
PackageManager pm = mContext.getPackageManager();
Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
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);
if (mediaMetadata == null) {
Log.e(TAG, "Media metadata was null");
return;
}
ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
if (albumView != null) {
// Resize art in a background thread
mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, albumView));
}
mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
// Click action
@@ -256,32 +265,9 @@ public class MediaControlPanel {
// App icon
ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
Drawable iconDrawable = icon.loadDrawable(mContext);
iconDrawable.setTint(mForegroundColor);
appIcon.setImageDrawable(iconDrawable);
// Song name
TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
titleText.setText(songName);
titleText.setTextColor(mForegroundColor);
// Not in mini player:
// App title
TextView appName = mMediaNotifView.findViewById(R.id.app_name);
if (appName != null) {
appName.setText(appNameString);
appName.setTextColor(mForegroundColor);
}
// Artist name
TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
if (artistText != null) {
String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
artistText.setText(artistName);
artistText.setTextColor(mForegroundColor);
}
// Transfer chip
mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
if (mSeamless != null && mLocalMediaManager != null) {
@@ -300,6 +286,39 @@ public class MediaControlPanel {
}
makeActive();
// App title (not in mini player)
TextView appName = mMediaNotifView.findViewById(R.id.app_name);
if (appName != null) {
appName.setText(appNameString);
appName.setTextColor(mForegroundColor);
}
MediaMetadata mediaMetadata = mController.getMetadata();
if (mediaMetadata == null) {
Log.e(TAG, "Media metadata was null");
return;
}
ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
if (albumView != null) {
// Resize art in a background thread
mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, albumView));
}
// Song name
TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
titleText.setText(songName);
titleText.setTextColor(mForegroundColor);
// Artist name (not in mini player)
TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
if (artistText != null) {
String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
artistText.setText(artistName);
artistText.setTextColor(mForegroundColor);
}
}
/**
@@ -320,9 +339,12 @@ public class MediaControlPanel {
/**
* Get the name of the package associated with the current media controller
* @return the package name
* @return the package name, or null if no controller
*/
public String getMediaPlayerPackage() {
if (mController == null) {
return null;
}
return mController.getPackageName();
}
@@ -368,6 +390,17 @@ public class MediaControlPanel {
return (state.getState() == PlaybackState.STATE_PLAYING);
}
/**
* Process album art for layout
* @param description media description
* @param albumView view to hold the album art
*/
protected void processAlbumArt(MediaDescription description, ImageView albumView) {
Bitmap albumArt = description.getIconBitmap();
//TODO check other fields (b/151054111, b/152067055)
processAlbumArtInternal(albumArt, albumView);
}
/**
* Process album art for layout
* @param metadata media metadata
@@ -375,6 +408,11 @@ public class MediaControlPanel {
*/
private void processAlbumArt(MediaMetadata metadata, ImageView albumView) {
Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
//TODO check other fields (b/151054111, b/152067055)
processAlbumArtInternal(albumArt, albumView);
}
private void processAlbumArtInternal(Bitmap albumArt, ImageView albumView) {
float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
RoundedBitmapDrawable roundedDrawable = null;
if (albumArt != null) {
@@ -449,10 +487,24 @@ public class MediaControlPanel {
}
/**
* Put controls into a resumption state
* 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() {
// Hide all the old buttons
for (int i = 0; i < mActionIds.length; i++) {
ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]);
@@ -465,27 +517,8 @@ public class MediaControlPanel {
ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]);
btn.setOnClickListener(v -> {
Log.d(TAG, "Attempting to restart session");
// Send a media button event to previously found receiver
if (mRecvComponent != null) {
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
intent.setComponent(mRecvComponent);
int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
intent.putExtra(
Intent.EXTRA_KEY_EVENT,
new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
mContext.sendBroadcast(intent);
} else {
// If we don't have a receiver, try relaunching the activity instead
if (mController.getSessionActivity() != null) {
try {
mController.getSessionActivity().send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Pending intent was canceled", e);
}
} else {
Log.e(TAG, "No receiver or activity to restart");
}
}
QSMediaBrowser browser = new QSMediaBrowser(mContext, null, mServiceComponent);
browser.restart();
});
btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
@@ -514,4 +547,65 @@ public class MediaControlPanel {
}
}
/**
* 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);
QSMediaBrowser.testConnection(mContext,
new QSMediaBrowser.Callback() {
@Override
public void onConnected() {
Log.d(TAG, "yes we can resume with " + componentName);
mServiceComponent = componentName;
updateResumptionList(componentName);
}
@Override
public void onError() {
Log.d(TAG, "Cannot resume with " + componentName);
mServiceComponent = null;
clearControls();
// remove
}
},
componentName);
}
/**
* 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() { }
}

View File

@@ -0,0 +1,259 @@
/*
* 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.qs;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.MediaDescription;
import android.media.browse.MediaBrowser;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.os.Bundle;
import android.service.media.MediaBrowserService;
import android.util.Log;
import java.util.List;
/**
* Media browser for managing resumption in QS media controls
*/
public class QSMediaBrowser {
/** Maximum number of controls to show on boot */
public static final int MAX_RESUMPTION_CONTROLS = 5;
/** Delimiter for saved component names */
public static final String DELIMITER = ":";
private static final String TAG = "QSMediaBrowser";
private final Context mContext;
private final Callback mCallback;
private MediaBrowser mMediaBrowser;
private ComponentName mComponentName;
/**
* Initialize a new media browser
* @param context the context
* @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) {
mContext = context;
mCallback = callback;
mComponentName = componentName;
Bundle rootHints = new Bundle();
rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
mMediaBrowser = new MediaBrowser(mContext,
mComponentName,
mConnectionCallback,
rootHints);
}
/**
* 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
*/
public void findRecentMedia() {
Log.d(TAG, "Connecting to " + mComponentName);
mMediaBrowser.connect();
}
private final MediaBrowser.SubscriptionCallback mSubscriptionCallback =
new MediaBrowser.SubscriptionCallback() {
@Override
public void onChildrenLoaded(String parentId,
List<MediaBrowser.MediaItem> children) {
if (children.size() == 0) {
Log.e(TAG, "No children found");
return;
}
// We ask apps to return a playable item as the first child when sending
// a request with EXTRA_RECENT; if they don't, no resume controls
MediaBrowser.MediaItem child = children.get(0);
MediaDescription desc = child.getDescription();
if (child.isPlayable()) {
mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), QSMediaBrowser.this);
} else {
Log.e(TAG, "Child found but not playable for " + mComponentName);
}
mMediaBrowser.disconnect();
}
@Override
public void onError(String parentId) {
Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId);
mMediaBrowser.disconnect();
}
@Override
public void onError(String parentId, Bundle options) {
Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId
+ ", options: " + options);
mMediaBrowser.disconnect();
}
};
private final MediaBrowser.ConnectionCallback mConnectionCallback =
new MediaBrowser.ConnectionCallback() {
/**
* Invoked after {@link MediaBrowser#connect()} when the request has successfully completed.
* For resumption controls, apps are expected to return a playable media item as the first
* child. If there are no children or it isn't playable it will be ignored.
*/
@Override
public void onConnected() {
if (mMediaBrowser.isConnected()) {
mCallback.onConnected();
Log.d(TAG, "Service connected for " + mComponentName);
String root = mMediaBrowser.getRoot();
mMediaBrowser.subscribe(root, mSubscriptionCallback);
}
}
/**
* Invoked when the client is disconnected from the media browser.
*/
@Override
public void onConnectionSuspended() {
Log.d(TAG, "Connection suspended for " + mComponentName);
}
/**
* Invoked when the connection to the media browser failed.
*/
@Override
public void onConnectionFailed() {
Log.e(TAG, "Connection failed for " + mComponentName);
mCallback.onError();
}
};
/**
* Connects to the MediaBrowserService and starts playback
*/
public void restart() {
if (mMediaBrowser.isConnected()) {
mMediaBrowser.disconnect();
}
Bundle rootHints = new Bundle();
rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
mMediaBrowser = new MediaBrowser(mContext, mComponentName,
new MediaBrowser.ConnectionCallback() {
@Override
public void onConnected() {
Log.d(TAG, "Connected for restart " + mMediaBrowser.isConnected());
MediaSession.Token token = mMediaBrowser.getSessionToken();
MediaController controller = new MediaController(mContext, token);
controller.getTransportControls();
controller.getTransportControls().prepare();
controller.getTransportControls().play();
}
}, rootHints);
mMediaBrowser.connect();
}
/**
* Get the media session token
* @return the token, or null if the MediaBrowser is null or disconnected
*/
public MediaSession.Token getToken() {
if (mMediaBrowser == null || !mMediaBrowser.isConnected()) {
return null;
}
return mMediaBrowser.getSessionToken();
}
/**
* Get an intent to launch the app associated with this browser service
* @return
*/
public PendingIntent getAppIntent() {
PackageManager pm = mContext.getPackageManager();
Intent launchIntent = pm.getLaunchIntentForPackage(mComponentName.getPackageName());
return PendingIntent.getActivity(mContext, 0, launchIntent, 0);
}
/**
* Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser
* @param mContext the context
* @param callback methods onConnected or onError will be called to indicate whether the
* connection was successful or not
* @param mComponentName Component name of the MediaBrowserService this browser will connect to
*/
public static MediaBrowser testConnection(Context mContext, Callback callback,
ComponentName mComponentName) {
final MediaBrowser.ConnectionCallback mConnectionCallback =
new MediaBrowser.ConnectionCallback() {
@Override
public void onConnected() {
Log.d(TAG, "connected");
callback.onConnected();
}
@Override
public void onConnectionSuspended() {
Log.d(TAG, "suspended");
callback.onError();
}
@Override
public void onConnectionFailed() {
Log.d(TAG, "failed");
callback.onError();
}
};
Bundle rootHints = new Bundle();
rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
MediaBrowser browser = new MediaBrowser(mContext,
mComponentName,
mConnectionCallback,
rootHints);
browser.connect();
return browser;
}
/**
* Interface to handle results from QSMediaBrowser
*/
public static class Callback {
/**
* Called when the browser has successfully connected to the service
*/
public void onConnected() {
}
/**
* Called when the browser encountered an error connecting to the service
*/
public void onError() {
}
/**
* Called when the browser finds a suitable track to add to the media carousel
* @param track media info for the item
* @param component component of the MediaBrowserService which returned this
* @param browser reference to the browser
*/
public void addTrack(MediaDescription track, ComponentName component,
QSMediaBrowser browser) {
}
}
}

View File

@@ -18,11 +18,12 @@ package com.android.systemui.qs;
import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.media.MediaDescription;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.util.Log;
@@ -60,9 +61,11 @@ public class QSMediaPlayer extends MediaControlPanel {
};
private final QSPanel mParent;
private final Executor mForegroundExecutor;
private final DelayableExecutor mBackgroundExecutor;
private final SeekBarViewModel mSeekBarViewModel;
private final SeekBarObserver mSeekBarObserver;
private String mPackageName;
/**
* Initialize quick shade version of player
@@ -77,6 +80,7 @@ public class QSMediaPlayer extends MediaControlPanel {
super(context, parent, routeManager, R.layout.qs_media_panel, QS_ACTION_IDS,
foregroundExecutor, backgroundExecutor);
mParent = (QSPanel) parent;
mForegroundExecutor = foregroundExecutor;
mBackgroundExecutor = backgroundExecutor;
mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
mSeekBarObserver = new SeekBarObserver(getView());
@@ -89,6 +93,58 @@ public class QSMediaPlayer extends MediaControlPanel {
bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
}
/**
* Add a media panel view based on a media description. Used for resumption
* @param description
* @param iconColor
* @param bgColor
* @param contentIntent
* @param pkgName
*/
public void setMediaSession(MediaSession.Token token, MediaDescription description,
int iconColor, int bgColor, PendingIntent contentIntent, String pkgName) {
mPackageName = pkgName;
PackageManager pm = getContext().getPackageManager();
Drawable icon = null;
CharSequence appName = pkgName.substring(pkgName.lastIndexOf("."));
try {
icon = pm.getApplicationIcon(pkgName);
appName = pm.getApplicationLabel(pm.getApplicationInfo(pkgName, 0));
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Error getting package information", e);
}
// Set what we can normally
super.setMediaSession(token, icon, iconColor, bgColor, contentIntent, appName.toString(),
null);
// Then add info from MediaDescription
ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
if (albumView != null) {
// Resize art in a background thread
mBackgroundExecutor.execute(() -> processAlbumArt(description, albumView));
}
// Song name
TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
CharSequence songName = description.getTitle();
titleText.setText(songName);
titleText.setTextColor(iconColor);
// Artist name (not in mini player)
TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
if (artistText != null) {
CharSequence artistName = description.getSubtitle();
artistText.setText(artistName);
artistText.setTextColor(iconColor);
}
initLongPressMenu(iconColor);
// Set buttons to resume state
resetButtons();
}
/**
* Update media panel view for the given media session
* @param token token for this media session
@@ -96,41 +152,43 @@ public class QSMediaPlayer extends MediaControlPanel {
* @param iconColor foreground color (for text, icons)
* @param bgColor background color
* @param actionsContainer a LinearLayout containing the media action buttons
* @param notif reference to original notification
* @param contentIntent Intent to send when user taps on player
* @param appName Application title
* @param key original notification's key
*/
public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor,
int bgColor, View actionsContainer, Notification notif, String key) {
public void setMediaSession(MediaSession.Token token, Drawable icon, int iconColor,
int bgColor, View actionsContainer, PendingIntent contentIntent, String appName,
String key) {
String appName = Notification.Builder.recoverBuilder(getContext(), notif)
.loadHeaderAppName();
super.setMediaSession(token, icon, iconColor, bgColor, notif.contentIntent, appName, key);
super.setMediaSession(token, icon, iconColor, bgColor, contentIntent, appName, key);
// Media controls
LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
int i = 0;
for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) {
ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]);
ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]);
if (thatBtn == null || thatBtn.getDrawable() == null
|| thatBtn.getVisibility() != View.VISIBLE) {
thisBtn.setVisibility(View.GONE);
continue;
if (actionsContainer != null) {
LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
int i = 0;
for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) {
ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]);
ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]);
if (thatBtn == null || thatBtn.getDrawable() == null
|| thatBtn.getVisibility() != View.VISIBLE) {
thisBtn.setVisibility(View.GONE);
continue;
}
Drawable thatIcon = thatBtn.getDrawable();
thisBtn.setImageDrawable(thatIcon.mutate());
thisBtn.setVisibility(View.VISIBLE);
thisBtn.setOnClickListener(v -> {
Log.d(TAG, "clicking on other button");
thatBtn.performClick();
});
}
Drawable thatIcon = thatBtn.getDrawable();
thisBtn.setImageDrawable(thatIcon.mutate());
thisBtn.setVisibility(View.VISIBLE);
thisBtn.setOnClickListener(v -> {
Log.d(TAG, "clicking on other button");
thatBtn.performClick();
});
}
// Hide any unused buttons
for (; i < QS_ACTION_IDS.length; i++) {
ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]);
thisBtn.setVisibility(View.GONE);
// Hide any unused buttons
for (; i < QS_ACTION_IDS.length; i++) {
ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]);
thisBtn.setVisibility(View.GONE);
}
}
// Seek Bar
@@ -138,6 +196,10 @@ public class QSMediaPlayer extends MediaControlPanel {
mBackgroundExecutor.execute(
() -> mSeekBarViewModel.updateController(controller, iconColor));
initLongPressMenu(iconColor);
}
private void initLongPressMenu(int iconColor) {
// Set up long press menu
View guts = mMediaNotifView.findViewById(R.id.media_guts);
View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
@@ -145,7 +207,7 @@ public class QSMediaPlayer extends MediaControlPanel {
View clearView = options.findViewById(R.id.remove);
clearView.setOnClickListener(b -> {
mParent.removeMediaPlayer(QSMediaPlayer.this);
removePlayer();
});
ImageView removeIcon = options.findViewById(R.id.remove_icon);
removeIcon.setImageTintList(ColorStateList.valueOf(iconColor));
@@ -165,11 +227,9 @@ public class QSMediaPlayer extends MediaControlPanel {
}
@Override
public void clearControls() {
super.clearControls();
protected void resetButtons() {
super.resetButtons();
mSeekBarViewModel.clearController();
View guts = mMediaNotifView.findViewById(R.id.media_guts);
View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
@@ -192,4 +252,19 @@ public class QSMediaPlayer extends MediaControlPanel {
public void setListening(boolean listening) {
mSeekBarViewModel.setListening(listening);
}
@Override
public void removePlayer() {
Log.d(TAG, "removing player from parent: " + mParent);
// Ensure this happens on the main thread (could happen in QSMediaBrowser callback)
mForegroundExecutor.execute(() -> mParent.removeMediaPlayer(QSMediaPlayer.this));
}
@Override
public String getMediaPlayerPackage() {
if (getController() == null) {
return mPackageName;
}
return super.getMediaPlayerPackage();
}
}

View File

@@ -21,16 +21,25 @@ import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEX
import static com.android.systemui.util.Utils.useQsMediaPlayer;
import android.annotation.Nullable;
import android.app.Notification;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.drawable.Icon;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.media.MediaDescription;
import android.media.session.MediaSession;
import android.metrics.LogMaker;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.UserHandle;
import android.os.UserManager;
import android.service.notification.StatusBarNotification;
import android.service.quicksettings.Tile;
import android.util.AttributeSet;
@@ -54,6 +63,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.media.MediaControlPanel;
import com.android.systemui.plugins.qs.DetailAdapter;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.qs.QSTileView;
@@ -90,6 +100,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
protected final Context mContext;
protected final ArrayList<TileRecord> mRecords = new ArrayList<>();
private final BroadcastDispatcher mBroadcastDispatcher;
private String mCachedSpecs = "";
protected final View mBrightnessView;
private final H mHandler = new H();
@@ -123,6 +134,19 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
private BrightnessMirrorController mBrightnessMirrorController;
private View mDivider;
private boolean mHasLoadedMediaControls;
private final BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
if (!mHasLoadedMediaControls) {
loadMediaResumptionControls();
}
}
}
};
@Inject
public QSPanel(
@@ -142,6 +166,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
mForegroundExecutor = foregroundExecutor;
mBackgroundExecutor = backgroundExecutor;
mLocalBluetoothManager = localBluetoothManager;
mBroadcastDispatcher = broadcastDispatcher;
setOrientation(VERTICAL);
@@ -176,7 +201,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
updateResources();
mBrightnessController = new BrightnessController(getContext(),
findViewById(R.id.brightness_slider), broadcastDispatcher);
findViewById(R.id.brightness_slider), mBroadcastDispatcher);
}
@Override
@@ -206,7 +231,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
* @param notif
* @param key
*/
public void addMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor,
public void addMediaSession(MediaSession.Token token, Drawable icon, int iconColor, int bgColor,
View actionsContainer, StatusBarNotification notif, String key) {
if (!useQsMediaPlayer(mContext)) {
// Shouldn't happen, but just in case
@@ -221,7 +246,14 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
QSMediaPlayer player = null;
String packageName = notif.getPackageName();
for (QSMediaPlayer p : mMediaPlayers) {
if (p.getMediaSessionToken().equals(token)) {
if (p.getKey() == null) {
// No notification key = loaded via mediabrowser, so just match on package
if (packageName.equals(p.getMediaPlayerPackage())) {
Log.d(TAG, "Found matching resume player by package: " + packageName);
player = p;
break;
}
} else if (p.getMediaSessionToken().equals(token)) {
Log.d(TAG, "Found matching player by token " + packageName);
player = p;
break;
@@ -262,8 +294,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
}
Log.d(TAG, "setting player session");
String appName = Notification.Builder.recoverBuilder(getContext(), notif.getNotification())
.loadHeaderAppName();
player.setMediaSession(token, icon, iconColor, bgColor, actionsContainer,
notif.getNotification(), key);
notif.getNotification().contentIntent, appName, key);
if (mMediaPlayers.size() > 0) {
((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE);
@@ -293,6 +327,74 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
return true;
}
private final QSMediaBrowser.Callback mMediaBrowserCallback = new QSMediaBrowser.Callback() {
@Override
public void addTrack(MediaDescription desc, ComponentName component,
QSMediaBrowser browser) {
if (component == null) {
Log.e(TAG, "Component cannot be null");
return;
}
Log.d(TAG, "adding track from browser: " + desc + ", " + component);
QSMediaPlayer player = new QSMediaPlayer(mContext, QSPanel.this,
null, mForegroundExecutor, mBackgroundExecutor);
String pkgName = component.getPackageName();
// Add controls to carousel
int playerWidth = (int) getResources().getDimension(R.dimen.qs_media_width);
int padding = (int) getResources().getDimension(R.dimen.qs_media_padding);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(playerWidth,
LayoutParams.MATCH_PARENT);
lp.setMarginStart(padding);
lp.setMarginEnd(padding);
mMediaCarousel.addView(player.getView(), lp);
((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE);
mMediaPlayers.add(player);
int iconColor = Color.DKGRAY;
int bgColor = Color.LTGRAY;
MediaSession.Token token = browser.getToken();
player.setMediaSession(token, desc, iconColor, bgColor, browser.getAppIntent(),
pkgName);
}
};
/**
* Load controls for resuming media, if available
*/
private void loadMediaResumptionControls() {
if (!useQsMediaPlayer(mContext)) {
return;
}
Log.d(TAG, "Loading resumption controls");
// Look up saved components to resume
Context userContext = mContext.createContextAsUser(mContext.getUser(), 0);
SharedPreferences prefs = userContext.getSharedPreferences(
MediaControlPanel.MEDIA_PREFERENCES, Context.MODE_PRIVATE);
String listString = prefs.getString(MediaControlPanel.MEDIA_PREFERENCE_KEY, null);
if (listString == null) {
Log.d(TAG, "No saved media components");
return;
}
String[] components = listString.split(QSMediaBrowser.DELIMITER);
Log.d(TAG, "components are: " + listString + " count " + components.length);
for (int i = 0; i < components.length && i < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
String[] info = components[i].split("/");
String packageName = info[0];
String className = info[1];
ComponentName component = new ComponentName(packageName, className);
QSMediaBrowser browser = new QSMediaBrowser(mContext, mMediaBrowserCallback,
component);
browser.findRecentMedia();
}
mHasLoadedMediaControls = true;
}
protected void addDivider() {
mDivider = LayoutInflater.from(mContext).inflate(R.layout.qs_divider, this, false);
mDivider.setBackgroundColor(Utils.applyAlpha(mDivider.getAlpha(),
@@ -343,6 +445,22 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
mBrightnessMirrorController.addCallback(this);
}
mDumpManager.registerDumpable(getDumpableTag(), this);
if (getClass() == QSPanel.class) {
//TODO(ethibodeau) remove class check after media refactor in ag/11059751
// Only run this in QSPanel proper, not QQS
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_USER_UNLOCKED);
mBroadcastDispatcher.registerReceiver(mUserChangeReceiver, filter, null,
UserHandle.ALL);
mHasLoadedMediaControls = false;
UserManager userManager = mContext.getSystemService(UserManager.class);
if (userManager.isUserUnlocked(mContext.getUserId())) {
// If it's already unlocked (like if dark theme was toggled), we can load now
loadMediaResumptionControls();
}
}
}
@Override
@@ -358,6 +476,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
mBrightnessMirrorController.removeCallback(this);
}
mDumpManager.unregisterDumpable(getDumpableTag());
mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver);
super.onDetachedFromWindow();
}

View File

@@ -19,7 +19,6 @@ package com.android.systemui.qs;
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.view.View;
@@ -67,7 +66,7 @@ public class QuickQSMediaPlayer extends MediaControlPanel {
* @param contentIntent Intent to send when user taps on the view
* @param key original notification's key
*/
public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor,
public void setMediaSession(MediaSession.Token token, Drawable icon, int iconColor, int bgColor,
View actionsContainer, int[] actionsToShow, PendingIntent contentIntent, String key) {
// Only update if this is a different session and currently playing
String oldPackage = "";

View File

@@ -22,6 +22,7 @@ import android.annotation.Nullable;
import android.app.Notification;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaSession;
@@ -187,8 +188,9 @@ public class NotificationMediaTemplateViewWrapper extends NotificationTemplateVi
com.android.systemui.R.id.quick_qs_panel);
StatusBarNotification sbn = mRow.getEntry().getSbn();
Notification notif = sbn.getNotification();
Drawable iconDrawable = notif.getSmallIcon().loadDrawable(mContext);
panel.getMediaPlayer().setMediaSession(token,
notif.getSmallIcon(),
iconDrawable,
tintColor,
mBackgroundColor,
mActions,
@@ -198,7 +200,7 @@ public class NotificationMediaTemplateViewWrapper extends NotificationTemplateVi
QSPanel bigPanel = ctrl.getNotificationShadeView().findViewById(
com.android.systemui.R.id.quick_settings_panel);
bigPanel.addMediaSession(token,
notif.getSmallIcon(),
iconDrawable,
tintColor,
mBackgroundColor,
mActions,