diff --git a/packages/SystemUI/res/layout/qs_media_panel.xml b/packages/SystemUI/res/layout/qs_media_panel.xml index 22303dc1d8d2e..34bd7035b5aea 100644 --- a/packages/SystemUI/res/layout/qs_media_panel.xml +++ b/packages/SystemUI/res/layout/qs_media_panel.xml @@ -32,7 +32,6 @@ android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" - android:id="@+id/header" android:layout_marginBottom="16dp" > @@ -73,7 +72,7 @@ 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(); + } + } + } + + 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 + mMediaNotifView.setOnClickListener(v -> { + try { + contentIntent.send(); + // Also close shade + mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + } catch (PendingIntent.CanceledException e) { + Log.e(TAG, "Pending intent was canceled", e); + } + }); + + // 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) { + mSeamless.setVisibility(View.VISIBLE); + updateDevice(device); + ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class); + mSeamless.setOnClickListener(v -> { + final Intent intent = new Intent() + .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT) + .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME, + mController.getPackageName()) + .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken); + mActivityStarter.startActivity(intent, false, true /* dismissShade */, + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + }); + } + + // Ensure is only added once + mMediaManager.removeCallback(this); + mMediaManager.addCallback(this); + } + + /** + * Return the token for the current media session + * @return the token + */ + public MediaSession.Token getMediaSessionToken() { + return mToken; + } + + /** + * Get the current media controller + * @return the controller + */ + public MediaController getController() { + return mController; + } + + /** + * Get the name of the package associated with the current media controller + * @return the package name + */ + public String getMediaPlayerPackage() { + return mController.getPackageName(); + } + + /** + * Check whether this player has an attached media session. + * @return whether there is a controller with a current media session. + */ + public boolean hasMediaSession() { + return mController != null && mController.getPlaybackState() != null; + } + + /** + * Check whether the media controlled by this player is currently playing + * @return whether it is playing, or false if no controller information + */ + public boolean isPlaying() { + return isPlaying(mController); + } + + /** + * Check whether the given controller is currently playing + * @param controller media controller to check + * @return whether it is playing, or false if no controller information + */ + protected boolean isPlaying(MediaController controller) { + if (controller == null) { + return false; + } + + PlaybackState state = controller.getPlaybackState(); + if (state == null) { + return false; + } + + return (state.getState() == PlaybackState.STATE_PLAYING); + } + + /** + * Process album art for layout + * @param metadata media metadata + * @param albumView view to hold the album art + */ + private void processAlbumArt(MediaMetadata metadata, ImageView albumView) { + Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); + float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius); + RoundedBitmapDrawable roundedDrawable = null; + if (albumArt != null) { + Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true); + int albumSize = (int) mContext.getResources().getDimension( + R.dimen.qs_media_album_size); + Bitmap scaled = Bitmap.createScaledBitmap(original, albumSize, albumSize, false); + roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled); + roundedDrawable.setCornerRadius(radius); + } else { + Log.e(TAG, "No album art available"); + } + + // Now that it's resized, update the UI + final RoundedBitmapDrawable result = roundedDrawable; + albumView.getHandler().post(() -> { + if (result != null) { + albumView.setImageDrawable(result); + albumView.setVisibility(View.VISIBLE); + } else { + albumView.setImageDrawable(null); + albumView.setVisibility(View.GONE); + } + }); + } + + /** + * Update the current device information + * @param device device information to display + */ + public void updateDevice(MediaDevice device) { + if (mSeamless == null) { + return; + } + Handler handler = mSeamless.getHandler(); + handler.post(() -> { + updateChipInternal(device); + }); + } + + private void updateChipInternal(MediaDevice device) { + ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor); + + // Update the outline color + LinearLayout viewLayout = (LinearLayout) mSeamless; + RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground(); + GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0); + rect.setStroke(2, mForegroundColor); + rect.setColor(mBackgroundColor); + + ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image); + TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text); + deviceName.setTextColor(fgTintList); + + if (device != null) { + Drawable icon = device.getIcon(); + iconView.setVisibility(View.VISIBLE); + iconView.setImageTintList(fgTintList); + + if (icon instanceof AdaptiveIcon) { + AdaptiveIcon aIcon = (AdaptiveIcon) icon; + aIcon.setBackgroundColor(mBackgroundColor); + iconView.setImageDrawable(aIcon); + } else { + iconView.setImageDrawable(icon); + } + deviceName.setText(device.getName()); + } else { + // Reset to default + iconView.setVisibility(View.GONE); + deviceName.setText(com.android.internal.R.string.ext_media_seamless_action); + } + } + + /** + * Put controls into a resumption state + */ + public void clearControls() { + // Hide all the old buttons + for (int i = 0; i < mActionIds.length; i++) { + ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]); + if (thisBtn != null) { + thisBtn.setVisibility(View.GONE); + } + } + + // Add a restart button + 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 { + Log.d(TAG, "No receiver to restart"); + // If we don't have a receiver, try relaunching the activity instead + try { + mController.getSessionActivity().send(); + } catch (PendingIntent.CanceledException e) { + Log.e(TAG, "Pending intent was canceled", e); + } + } + }); + btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play)); + btn.setImageTintList(ColorStateList.valueOf(mForegroundColor)); + btn.setVisibility(View.VISIBLE); + } + + @Override + public void onMetadataOrStateChanged(MediaMetadata metadata, int state) { + if (state == PlaybackState.STATE_NONE) { + clearControls(); + mMediaManager.removeCallback(this); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java index 011893d657237..837256bf40528 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java @@ -17,158 +17,53 @@ package com.android.systemui.qs; import android.app.Notification; -import android.app.PendingIntent; -import android.content.ComponentName; import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.ColorStateList; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.Paint; import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.Icon; -import android.graphics.drawable.RippleDrawable; -import android.media.MediaMetadata; -import android.media.session.MediaController; import android.media.session.MediaSession; -import android.media.session.PlaybackState; -import android.os.Handler; import android.util.Log; -import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; -import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.core.graphics.drawable.RoundedBitmapDrawable; -import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; import com.android.settingslib.media.MediaDevice; -import com.android.settingslib.media.MediaOutputSliceConstants; -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.media.MediaControlPanel; +import com.android.systemui.statusbar.NotificationMediaManager; -import java.util.List; +import java.util.concurrent.Executor; /** * Single media player for carousel in QSPanel */ -public class QSMediaPlayer { +public class QSMediaPlayer extends MediaControlPanel { private static final String TAG = "QSMediaPlayer"; - private Context mContext; - private LinearLayout mMediaNotifView; - private View mSeamless; - private MediaSession.Token mToken; - private MediaController mController; - private int mForegroundColor; - private int mBackgroundColor; - private ComponentName mRecvComponent; - private QSPanel mParent; - - private MediaController.Callback mSessionCallback = new MediaController.Callback() { - @Override - public void onSessionDestroyed() { - Log.d(TAG, "session destroyed"); - mController.unregisterCallback(mSessionCallback); - - // Hide all the old buttons - final int[] actionIds = { - R.id.action0, - R.id.action1, - R.id.action2, - R.id.action3, - R.id.action4 - }; - for (int i = 0; i < actionIds.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]); - if (thisBtn != null) { - thisBtn.setVisibility(View.GONE); - } - } - - // Add a restart button - ImageButton btn = mMediaNotifView.findViewById(actionIds[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 { - Log.d(TAG, "No receiver to restart"); - // If we don't have a receiver, try relaunching the activity instead - try { - mController.getSessionActivity().send(); - } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Pending intent was canceled"); - e.printStackTrace(); - } - } - }); - btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play)); - btn.setImageTintList(ColorStateList.valueOf(mForegroundColor)); - btn.setVisibility(View.VISIBLE); - - // Add long-click option to remove the player - ViewGroup mMediaCarousel = (ViewGroup) mMediaNotifView.getParent(); - mMediaNotifView.setOnLongClickListener(v -> { - // Replace player view with delete/cancel view - v.setVisibility(View.GONE); - - View options = LayoutInflater.from(mContext).inflate( - R.layout.qs_media_panel_options, null, false); - ImageButton btnDelete = options.findViewById(R.id.remove); - btnDelete.setOnClickListener(b -> { - mMediaCarousel.removeView(options); - mParent.removeMediaPlayer(QSMediaPlayer.this); - }); - ImageButton btnCancel = options.findViewById(R.id.cancel); - btnCancel.setOnClickListener(b -> { - mMediaCarousel.removeView(options); - v.setVisibility(View.VISIBLE); - }); - - int pos = mMediaCarousel.indexOfChild(v); - mMediaCarousel.addView(options, pos, v.getLayoutParams()); - return true; // consumed click - }); - } + // Button IDs for QS controls + static final int[] QS_ACTION_IDS = { + R.id.action0, + R.id.action1, + R.id.action2, + R.id.action3, + R.id.action4 }; /** - * + * Initialize quick shade version of player * @param context * @param parent + * @param manager + * @param backgroundExecutor */ - public QSMediaPlayer(Context context, ViewGroup parent) { - mContext = context; - LayoutInflater inflater = LayoutInflater.from(mContext); - mMediaNotifView = (LinearLayout) inflater.inflate(R.layout.qs_media_panel, parent, false); - } - - public View getView() { - return mMediaNotifView; + public QSMediaPlayer(Context context, ViewGroup parent, NotificationMediaManager manager, + Executor backgroundExecutor) { + super(context, parent, manager, R.layout.qs_media_panel, QS_ACTION_IDS, backgroundExecutor); } /** - * Create or update the player view for the given media session - * @param parent the parent QSPanel + * Update media panel view for the given media session * @param token token for this media session * @param icon app notification icon * @param iconColor foreground color (for text, icons) @@ -177,114 +72,20 @@ public class QSMediaPlayer { * @param notif reference to original notification * @param device current playback device */ - public void setMediaSession(QSPanel parent, MediaSession.Token token, Icon icon, int iconColor, + public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor, View actionsContainer, Notification notif, MediaDevice device) { - mParent = parent; - mToken = token; - mForegroundColor = iconColor; - mBackgroundColor = bgColor; - mController = new MediaController(mContext, token); - // 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 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(); - } - } - } - - // reset in case we had previously restarted the stream - mMediaNotifView.setOnLongClickListener(null); - mController.registerCallback(mSessionCallback); - MediaMetadata mMediaMetadata = mController.getMetadata(); - if (mMediaMetadata == null) { - Log.e(TAG, "Media metadata was null"); - return; - } - - Notification.Builder builder = Notification.Builder.recoverBuilder(mContext, notif); - - // Album art - addAlbumArt(mMediaMetadata, bgColor); - - LinearLayout headerView = mMediaNotifView.findViewById(R.id.header); - - // App icon - ImageView appIcon = headerView.findViewById(R.id.icon); - Drawable iconDrawable = icon.loadDrawable(mContext); - iconDrawable.setTint(iconColor); - appIcon.setImageDrawable(iconDrawable); - - // App title - TextView appName = headerView.findViewById(R.id.app_name); - String appNameString = builder.loadHeaderAppName(); - appName.setText(appNameString); - appName.setTextColor(iconColor); - - // Action - mMediaNotifView.setOnClickListener(v -> { - try { - notif.contentIntent.send(); - // Also close shade - mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); - } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Pending intent was canceled"); - e.printStackTrace(); - } - }); - - // Transfer chip - mSeamless = headerView.findViewById(R.id.media_seamless); - mSeamless.setVisibility(View.VISIBLE); - updateChip(device); - ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class); - mSeamless.setOnClickListener(v -> { - final Intent intent = new Intent() - .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT) - .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME, - mController.getPackageName()) - .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, token); - mActivityStarter.startActivity(intent, false, true /* dismissShade */, - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - }); - - // Artist name - TextView artistText = headerView.findViewById(R.id.header_artist); - String artistName = mMediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST); - artistText.setText(artistName); - artistText.setTextColor(iconColor); - - // Song name - TextView titleText = headerView.findViewById(R.id.header_text); - String songName = mMediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); - titleText.setText(songName); - titleText.setTextColor(iconColor); + String appName = Notification.Builder.recoverBuilder(getContext(), notif) + .loadHeaderAppName(); + super.setMediaSession(token, icon, iconColor, bgColor, notif.contentIntent, + appName, device); // Media controls LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; - final int[] actionIds = { - R.id.action0, - R.id.action1, - R.id.action2, - R.id.action3, - R.id.action4 - }; - final int[] notifActionIds = { - com.android.internal.R.id.action0, - com.android.internal.R.id.action1, - com.android.internal.R.id.action2, - com.android.internal.R.id.action3, - com.android.internal.R.id.action4 - }; - int i = 0; - for (; i < parentActionsLayout.getChildCount() && i < actionIds.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]); - ImageButton thatBtn = parentActionsLayout.findViewById(notifActionIds[i]); + 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); @@ -301,116 +102,9 @@ public class QSMediaPlayer { } // Hide any unused buttons - for (; i < actionIds.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]); + for (; i < QS_ACTION_IDS.length; i++) { + ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); thisBtn.setVisibility(View.GONE); } } - - public MediaSession.Token getMediaSessionToken() { - return mToken; - } - - public String getMediaPlayerPackage() { - return mController.getPackageName(); - } - - /** - * Check whether the media controlled by this player is currently playing - * @return whether it is playing, or false if no controller information - */ - public boolean isPlaying() { - if (mController == null) { - return false; - } - - PlaybackState state = mController.getPlaybackState(); - if (state == null) { - return false; - } - - return (state.getState() == PlaybackState.STATE_PLAYING); - } - - private void addAlbumArt(MediaMetadata metadata, int bgColor) { - Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); - float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius); - ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); - if (albumArt != null) { - Log.d(TAG, "updating album art"); - Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true); - int albumSize = (int) mContext.getResources().getDimension(R.dimen.qs_media_album_size); - Bitmap scaled = scaleBitmap(original, albumSize, albumSize); - RoundedBitmapDrawable roundedDrawable = RoundedBitmapDrawableFactory.create( - mContext.getResources(), scaled); - roundedDrawable.setCornerRadius(radius); - albumView.setImageDrawable(roundedDrawable); - } else { - Log.e(TAG, "No album art available"); - albumView.setImageDrawable(null); - } - - mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(bgColor)); - } - - private Bitmap scaleBitmap(Bitmap original, int width, int height) { - Bitmap cropped = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(cropped); - - float scale = (float) cropped.getWidth() / (float) original.getWidth(); - float dy = (cropped.getHeight() - original.getHeight() * scale) / 2.0f; - Matrix transformation = new Matrix(); - transformation.postTranslate(0, dy); - transformation.preScale(scale, scale); - - Paint paint = new Paint(); - paint.setFilterBitmap(true); - canvas.drawBitmap(original, transformation, paint); - - return cropped; - } - - protected void updateChip(MediaDevice device) { - if (mSeamless == null) { - return; - } - Handler handler = mSeamless.getHandler(); - handler.post(() -> { - updateChipInternal(device); - }); - } - - private void updateChipInternal(MediaDevice device) { - ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor); - - // Update the outline color - LinearLayout viewLayout = (LinearLayout) mSeamless; - RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground(); - GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0); - rect.setStroke(2, mForegroundColor); - rect.setColor(mBackgroundColor); - - ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image); - TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text); - deviceName.setTextColor(fgTintList); - - if (device != null) { - Drawable icon = device.getIcon(); - iconView.setVisibility(View.VISIBLE); - iconView.setImageTintList(fgTintList); - - if (icon instanceof AdaptiveIcon) { - AdaptiveIcon aIcon = (AdaptiveIcon) icon; - aIcon.setBackgroundColor(mBackgroundColor); - iconView.setImageDrawable(aIcon); - } else { - iconView.setImageDrawable(icon); - } - deviceName.setText(device.getName()); - } else { - // Reset to default - iconView.setVisibility(View.GONE); - deviceName.setText(com.android.internal.R.string.ext_media_seamless_action); - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index d2d90925cbe22..9ab47145f92d5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -50,6 +50,7 @@ import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.plugins.qs.QSTile; @@ -60,6 +61,7 @@ import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.settings.BrightnessController; import com.android.systemui.settings.ToggleSliderView; +import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener; import com.android.systemui.tuner.TunerService; @@ -70,6 +72,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.concurrent.Executor; import java.util.stream.Collectors; import javax.inject.Inject; @@ -94,6 +97,8 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne private final LinearLayout mMediaCarousel; private final ArrayList mMediaPlayers = new ArrayList<>(); + private final NotificationMediaManager mNotificationMediaManager; + private final Executor mBackgroundExecutor; private LocalMediaManager mLocalMediaManager; private MediaDevice mDevice; private boolean mUpdateCarousel = false; @@ -128,7 +133,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne if (mDevice == null || !mDevice.equals(currentDevice)) { mDevice = currentDevice; for (QSMediaPlayer p : mMediaPlayers) { - p.updateChip(mDevice); + p.updateDevice(mDevice); } } } @@ -138,7 +143,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne if (mDevice == null || !mDevice.equals(device)) { mDevice = device; for (QSMediaPlayer p : mMediaPlayers) { - p.updateChip(mDevice); + p.updateDevice(mDevice); } } } @@ -150,12 +155,16 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne AttributeSet attrs, DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, - QSLogger qsLogger + QSLogger qsLogger, + NotificationMediaManager notificationMediaManager, + @Background Executor backgroundExecutor ) { super(context, attrs); mContext = context; mQSLogger = qsLogger; mDumpManager = dumpManager; + mNotificationMediaManager = notificationMediaManager; + mBackgroundExecutor = backgroundExecutor; setOrientation(VERTICAL); @@ -255,7 +264,8 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne if (player == null) { Log.d(TAG, "creating new player"); - player = new QSMediaPlayer(mContext, this); + player = new QSMediaPlayer(mContext, this, mNotificationMediaManager, + mBackgroundExecutor); if (player.isPlaying()) { mMediaCarousel.addView(player.getView(), 0, lp); // add in front @@ -268,7 +278,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } Log.d(TAG, "setting player session"); - player.setMediaSession(this, token, icon, iconColor, bgColor, actionsContainer, + player.setMediaSession(token, icon, iconColor, bgColor, actionsContainer, notif.getNotification(), mDevice); if (mMediaPlayers.size() > 0) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java index 9018a375c365a..4512afb3c1789 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java @@ -17,108 +17,47 @@ 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.content.pm.ResolveInfo; -import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; -import android.media.MediaMetadata; import android.media.session.MediaController; import android.media.session.MediaSession; -import android.media.session.PlaybackState; -import android.util.Log; -import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; -import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.TextView; import com.android.systemui.R; +import com.android.systemui.media.MediaControlPanel; +import com.android.systemui.statusbar.NotificationMediaManager; -import java.util.List; +import java.util.concurrent.Executor; /** * QQS mini media player */ -public class QuickQSMediaPlayer { +public class QuickQSMediaPlayer extends MediaControlPanel { private static final String TAG = "QQSMediaPlayer"; - private Context mContext; - private LinearLayout mMediaNotifView; - private MediaSession.Token mToken; - private MediaController mController; - private int mForegroundColor; - private ComponentName mRecvComponent; - - private MediaController.Callback mSessionCallback = new MediaController.Callback() { - @Override - public void onSessionDestroyed() { - Log.d(TAG, "session destroyed"); - mController.unregisterCallback(mSessionCallback); - - // Hide all the old buttons - final int[] actionIds = {R.id.action0, R.id.action1, R.id.action2}; - for (int i = 0; i < actionIds.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]); - if (thisBtn != null) { - thisBtn.setVisibility(View.GONE); - } - } - - // Add a restart button - ImageButton btn = mMediaNotifView.findViewById(actionIds[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 { - Log.d(TAG, "No receiver to restart"); - // If we don't have a receiver, try relaunching the activity instead - try { - mController.getSessionActivity().send(); - } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Pending intent was canceled"); - e.printStackTrace(); - } - } - }); - btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play)); - btn.setImageTintList(ColorStateList.valueOf(mForegroundColor)); - btn.setVisibility(View.VISIBLE); - } - }; + // Button IDs for QS controls + private static final int[] QQS_ACTION_IDS = {R.id.action0, R.id.action1, R.id.action2}; /** - * + * Initialize mini media player for QQS * @param context * @param parent + * @param manager + * @param backgroundExecutor */ - public QuickQSMediaPlayer(Context context, ViewGroup parent) { - mContext = context; - LayoutInflater inflater = LayoutInflater.from(mContext); - mMediaNotifView = (LinearLayout) inflater.inflate(R.layout.qqs_media_panel, parent, false); - } - - public View getView() { - return mMediaNotifView; + public QuickQSMediaPlayer(Context context, ViewGroup parent, NotificationMediaManager manager, + Executor backgroundExecutor) { + super(context, parent, manager, R.layout.qqs_media_panel, QQS_ACTION_IDS, + backgroundExecutor); } /** - * + * Update media panel view for the given media session * @param token token for this media session * @param icon app notification icon * @param iconColor foreground color (for text, icons) @@ -130,84 +69,30 @@ public class QuickQSMediaPlayer { */ public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor, View actionsContainer, int[] actionsToShow, PendingIntent contentIntent) { - mToken = token; - mForegroundColor = iconColor; - + // Only update if this is a different session and currently playing String oldPackage = ""; - if (mController != null) { - oldPackage = mController.getPackageName(); + if (getController() != null) { + oldPackage = getController().getPackageName(); } - MediaController controller = new MediaController(mContext, token); - boolean samePlayer = mToken.equals(token) && oldPackage.equals(controller.getPackageName()); - if (mController != null && !samePlayer && !isPlaying(controller)) { - // Only update if this is a different session and currently playing - return; - } - mController = controller; - MediaMetadata mMediaMetadata = 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 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(); - } - } - } - mController.registerCallback(mSessionCallback); - - if (mMediaMetadata == null) { - Log.e(TAG, "Media metadata was null"); + MediaController controller = new MediaController(getContext(), token); + MediaSession.Token currentToken = getMediaSessionToken(); + boolean samePlayer = currentToken != null + && currentToken.equals(token) + && oldPackage.equals(controller.getPackageName()); + if (getController() != null && !samePlayer && !isPlaying(controller)) { return; } - // Action - mMediaNotifView.setOnClickListener(v -> { - try { - contentIntent.send(); - mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); - } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Pending intent was canceled: " + e.getMessage()); - } - }); + super.setMediaSession(token, icon, iconColor, bgColor, contentIntent, null, null); - mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(bgColor)); - - // 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 = mMediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); - titleText.setText(songName); - titleText.setTextColor(mForegroundColor); - - // Buttons we can display - final int[] actionIds = {R.id.action0, R.id.action1, R.id.action2}; - - // Existing buttons in the notification LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; - final int[] notifActionIds = { - com.android.internal.R.id.action0, - com.android.internal.R.id.action1, - com.android.internal.R.id.action2, - com.android.internal.R.id.action3, - com.android.internal.R.id.action4 - }; - int i = 0; if (actionsToShow != null) { int maxButtons = Math.min(actionsToShow.length, parentActionsLayout.getChildCount()); - maxButtons = Math.min(maxButtons, actionIds.length); + maxButtons = Math.min(maxButtons, QQS_ACTION_IDS.length); for (; i < maxButtons; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]); - int thatId = notifActionIds[actionsToShow[i]]; + ImageButton thisBtn = mMediaNotifView.findViewById(QQS_ACTION_IDS[i]); + int thatId = NOTIF_ACTION_IDS[actionsToShow[i]]; ImageButton thatBtn = parentActionsLayout.findViewById(thatId); if (thatBtn == null || thatBtn.getDrawable() == null || thatBtn.getVisibility() != View.VISIBLE) { @@ -225,38 +110,9 @@ public class QuickQSMediaPlayer { } // Hide any unused buttons - for (; i < actionIds.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]); + for (; i < QQS_ACTION_IDS.length; i++) { + ImageButton thisBtn = mMediaNotifView.findViewById(QQS_ACTION_IDS[i]); thisBtn.setVisibility(View.GONE); } } - - public MediaSession.Token getMediaSessionToken() { - return mToken; - } - - /** - * Check whether the media controlled by this player is currently playing - * @return whether it is playing, or false if no controller information - */ - public boolean isPlaying(MediaController controller) { - if (controller == null) { - return false; - } - - PlaybackState state = controller.getPlaybackState(); - if (state == null) { - return false; - } - - return (state.getState() == PlaybackState.STATE_PLAYING); - } - - /** - * Check whether this player has an attached media session. - * @return whether there is a controller with a current media session. - */ - public boolean hasMediaSession() { - return mController != null && mController.getPlaybackState() != null; - } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java index 20efbcb3cc425..3da767e51be0b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java @@ -29,18 +29,21 @@ import android.widget.LinearLayout; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile.SignalState; import com.android.systemui.plugins.qs.QSTile.State; import com.android.systemui.qs.customize.QSCustomizer; import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.statusbar.NotificationMediaManager; 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; +import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; @@ -72,9 +75,12 @@ public class QuickQSPanel extends QSPanel { AttributeSet attrs, DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, - QSLogger qsLogger + QSLogger qsLogger, + NotificationMediaManager notificationMediaManager, + @Background Executor backgroundExecutor ) { - super(context, attrs, dumpManager, broadcastDispatcher, qsLogger); + super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, notificationMediaManager, + backgroundExecutor); if (mFooter != null) { removeView(mFooter.getView()); } @@ -93,7 +99,8 @@ public class QuickQSPanel extends QSPanel { mHorizontalLinearLayout.setClipToPadding(false); int marginSize = (int) mContext.getResources().getDimension(R.dimen.qqs_media_spacing); - mMediaPlayer = new QuickQSMediaPlayer(mContext, mHorizontalLinearLayout); + mMediaPlayer = new QuickQSMediaPlayer(mContext, mHorizontalLinearLayout, + notificationMediaManager, backgroundExecutor); LayoutParams lp2 = new LayoutParams(0, LayoutParams.MATCH_PARENT, 1); lp2.setMarginEnd(marginSize); lp2.setMarginStart(0); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java index 45c0cdd6bfd27..616399afe7a43 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java @@ -42,6 +42,7 @@ import com.android.systemui.plugins.qs.QSTileView; import com.android.systemui.qs.customize.QSCustomizer; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.statusbar.NotificationMediaManager; import org.junit.Before; import org.junit.Test; @@ -53,6 +54,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Collections; +import java.util.concurrent.Executor; @RunWith(AndroidTestingRunner.class) @RunWithLooper @@ -79,6 +81,10 @@ public class QSPanelTest extends SysuiTestCase { private QSDetail.Callback mCallback; @Mock private QSTileView mQSTileView; + @Mock + private NotificationMediaManager mNotificationMediaManager; + @Mock + private Executor mBackgroundExecutor; @Before public void setup() throws Exception { @@ -88,7 +94,7 @@ public class QSPanelTest extends SysuiTestCase { mTestableLooper.runWithLooper(() -> { mMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class); mQsPanel = new QSPanel(mContext, null, mDumpManager, mBroadcastDispatcher, - mQSLogger); + mQSLogger, mNotificationMediaManager, mBackgroundExecutor); // Provides a parent with non-zero size for QSPanel mParentView = new FrameLayout(mContext); mParentView.addView(mQsPanel);