From 7b6c1789a6109384f777e1862e9ffa9ad86e084e Mon Sep 17 00:00:00 2001 From: Beth Thibodeau Date: Thu, 5 Mar 2020 11:43:51 -0500 Subject: [PATCH] Reset when playback state is NONE, and refactoring If a notification is cleared it won't always send an update first and call onSessionDestroyed. NotificationMediaManager already checks if the current media notification is being removed and sends an event, so we can listen for that. This fixes the bug with cast notifications. It can also be tested by playing media, pausing, and then clearing all notifications - the controls will now switch to the resumption state. Also refactored to put common code for QQS and QS players in a single class, and removed the long-press menu in QSMediaPlayer (which was just for testing) Fixes: 150437753 Fixes: 150742919 Bug: 150854549 Test: manual; atest com.android.systemui.qs com.android.systemui.statusbar.notification Change-Id: I0ecada16967a6d21b82dc86d05544cf78027bcd9 --- .../SystemUI/res/layout/qs_media_panel.xml | 3 +- .../systemui/media/MediaControlPanel.java | 425 ++++++++++++++++++ .../android/systemui/qs/QSMediaPlayer.java | 362 ++------------- .../src/com/android/systemui/qs/QSPanel.java | 20 +- .../systemui/qs/QuickQSMediaPlayer.java | 202 ++------- .../com/android/systemui/qs/QuickQSPanel.java | 13 +- .../com/android/systemui/qs/QSPanelTest.java | 8 +- 7 files changed, 515 insertions(+), 518 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java 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);