Chromium Code Reviews| Index: chrome/android/java/src/org/chromium/chrome/browser/media/ui/MediaNotificationManager.java |
| diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/ui/MediaNotificationManager.java b/chrome/android/java/src/org/chromium/chrome/browser/media/ui/MediaNotificationManager.java |
| index b36cc9faffc5b28a81de23e6c366eeab18b7ab75..ed0f484016f8fce8c08053063c48baadbf63e3e8 100644 |
| --- a/chrome/android/java/src/org/chromium/chrome/browser/media/ui/MediaNotificationManager.java |
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/media/ui/MediaNotificationManager.java |
| @@ -18,11 +18,11 @@ import android.graphics.drawable.Drawable; |
| import android.media.AudioManager; |
| import android.os.Build; |
| import android.os.IBinder; |
| -import android.support.v4.app.NotificationCompat; |
| import android.support.v4.app.NotificationManagerCompat; |
| import android.support.v4.media.MediaMetadataCompat; |
| import android.support.v4.media.session.MediaSessionCompat; |
| import android.support.v4.media.session.PlaybackStateCompat; |
| +import android.support.v7.app.NotificationCompat; |
| import android.support.v7.media.MediaRouter; |
| import android.text.TextUtils; |
| import android.util.SparseArray; |
| @@ -33,6 +33,7 @@ import android.widget.RemoteViews; |
| import org.chromium.base.ApiCompatibilityUtils; |
| import org.chromium.base.VisibleForTesting; |
| import org.chromium.chrome.R; |
| +import org.chromium.chrome.browser.ChromeFeatureList; |
| import javax.annotation.Nullable; |
| @@ -43,9 +44,13 @@ import javax.annotation.Nullable; |
| * There's one service started for a distinct notification id. |
| */ |
| public class MediaNotificationManager { |
| - |
| private static final String TAG = "MediaNotification"; |
| + // The background notification size on Android Wear. See: |
| + // http://developer.android.com/training/wearables/notifications/creating.html |
| + private static final int WEARABLE_NOTIFICATION_BACKGROUND_WIDTH = 400; |
| + private static final int WEARABLE_NOTIFICATION_BACKGROUND_HEIGHT = 400; |
| + |
| // We're always used on the UI thread but the LOCK is required by lint when creating the |
| // singleton. |
| private static final Object LOCK = new Object(); |
| @@ -482,61 +487,13 @@ public class MediaNotificationManager { |
| clearNotification(); |
| } |
| - private RemoteViews createContentView() { |
| - RemoteViews contentView = |
| - new RemoteViews(mContext.getPackageName(), R.layout.playback_notification_bar); |
| - |
| - // By default, play/pause button is the only one. |
| - int playPauseButtonId = R.id.button1; |
| - // On Android pre-L, dismissing the notification when the service is no longer in foreground |
| - // doesn't work. Instead, a STOP button is shown. |
| - if (mMediaNotificationInfo.supportsSwipeAway() |
| - && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP |
| - || mMediaNotificationInfo.supportsStop()) { |
| - contentView.setOnClickPendingIntent(R.id.button1, |
| - createPendingIntent(ListenerService.ACTION_STOP)); |
| - contentView.setContentDescription(R.id.button1, mStopDescription); |
| - |
| - // If the play/pause needs to be shown, it moves over to the second button from the end. |
| - playPauseButtonId = R.id.button2; |
| - } |
| - |
| - contentView.setTextViewText(R.id.title, mMediaNotificationInfo.metadata.getTitle()); |
| - contentView.setTextViewText(R.id.status, mMediaNotificationInfo.origin); |
| - if (mNotificationIcon != null) { |
| - contentView.setImageViewBitmap(R.id.icon, mNotificationIcon); |
| - } else { |
| - contentView.setImageViewResource(R.id.icon, mMediaNotificationInfo.icon); |
| - } |
| - |
| - if (mMediaNotificationInfo.supportsPlayPause()) { |
| - if (mMediaNotificationInfo.isPaused) { |
| - contentView.setImageViewResource(playPauseButtonId, R.drawable.ic_vidcontrol_play); |
| - contentView.setContentDescription(playPauseButtonId, mPlayDescription); |
| - contentView.setOnClickPendingIntent(playPauseButtonId, |
| - createPendingIntent(ListenerService.ACTION_PLAY)); |
| - } else { |
| - // If we're here, the notification supports play/pause button and is playing. |
| - contentView.setImageViewResource(playPauseButtonId, R.drawable.ic_vidcontrol_pause); |
| - contentView.setContentDescription(playPauseButtonId, mPauseDescription); |
| - contentView.setOnClickPendingIntent(playPauseButtonId, |
| - createPendingIntent(ListenerService.ACTION_PAUSE)); |
| - } |
| - |
| - contentView.setViewVisibility(playPauseButtonId, View.VISIBLE); |
| - } else { |
| - contentView.setViewVisibility(playPauseButtonId, View.GONE); |
| - } |
| - |
| - return contentView; |
| - } |
| - |
| private MediaMetadataCompat createMetadata() { |
| MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Builder(); |
| // Choose the image to use as the icon. |
| - Bitmap mediaSessionImage = mMediaNotificationInfo.image == null ? mDefaultMediaSessionImage |
| - : mMediaNotificationInfo.image; |
| + Bitmap mediaSessionImage = mMediaNotificationInfo.largeIcon == null |
| + ? mDefaultMediaSessionImage |
| + : mMediaNotificationInfo.largeIcon; |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { |
| metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, |
| @@ -544,12 +501,12 @@ public class MediaNotificationManager { |
| metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, |
| mMediaNotificationInfo.origin); |
| metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, |
| - mediaSessionImage); |
| + scaleBitmapForWearable(mediaSessionImage)); |
| // METADATA_KEY_ART is optional and should only be used if we can provide something |
| // better than the default image. |
| - if (mMediaNotificationInfo.image != null) { |
| - metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, |
| - mMediaNotificationInfo.image); |
| + if (mMediaNotificationInfo.largeIcon != null) { |
| + metadataBuilder.putBitmap( |
| + MediaMetadataCompat.METADATA_KEY_ART, mMediaNotificationInfo.largeIcon); |
| } |
| } else { |
| metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, |
| @@ -576,21 +533,19 @@ public class MediaNotificationManager { |
| if (mMediaNotificationInfo == null) return; |
| - // Android doesn't badge the icons for RemoteViews automatically when |
| - // running the app under the Work profile. |
| - if (mNotificationIcon == null) { |
| - Drawable notificationIconDrawable = ApiCompatibilityUtils.getUserBadgedIcon( |
| - mContext, mMediaNotificationInfo.icon); |
| - mNotificationIcon = drawableToBitmap(notificationIconDrawable); |
| - } |
| + updateMediaSession(); |
| - if (mNotificationBuilder == null) { |
| - mNotificationBuilder = new NotificationCompat.Builder(mContext) |
| - .setSmallIcon(mMediaNotificationInfo.icon) |
| - .setAutoCancel(false) |
| - .setLocalOnly(true) |
| - .setDeleteIntent(createPendingIntent(ListenerService.ACTION_STOP)); |
| + mNotificationBuilder = new NotificationCompat.Builder(mContext); |
| + if (ChromeFeatureList.isEnabled(ChromeFeatureList.MEDIA_STYLE_NOTIFICATION)) { |
| + setMediaStyleLayoutForNotificationBuilder(mNotificationBuilder); |
| + } else { |
| + setCustomLayoutForNotificationBuilder(mNotificationBuilder); |
| } |
| + // TODO: smallIcon set to Chrome's icon? |
|
mlamouri (slow - plz ping)
2016/04/11 14:37:38
I think you can remove the TODO because if we don'
Zhiqiang Zhang (Slow)
2016/04/11 20:44:55
Done.
|
| + mNotificationBuilder.setSmallIcon(mMediaNotificationInfo.icon); |
| + mNotificationBuilder.setAutoCancel(false); |
| + mNotificationBuilder.setLocalOnly(true); |
| + mNotificationBuilder.setDeleteIntent(createPendingIntent(ListenerService.ACTION_STOP)); |
| if (mMediaNotificationInfo.supportsSwipeAway()) { |
| mNotificationBuilder.setOngoing(!mMediaNotificationInfo.isPaused); |
| @@ -604,39 +559,10 @@ public class MediaNotificationManager { |
| mMediaNotificationInfo.contentIntent, 0)); |
| } |
| - mNotificationBuilder.setContent(createContentView()); |
| mNotificationBuilder.setVisibility( |
| mMediaNotificationInfo.isPrivate ? NotificationCompat.VISIBILITY_PRIVATE |
| : NotificationCompat.VISIBILITY_PUBLIC); |
| - |
| - if (mMediaNotificationInfo.supportsPlayPause()) { |
| - |
| - if (mMediaSession == null) mMediaSession = createMediaSession(); |
| - try { |
| - // Tell the MediaRouter about the session, so that Chrome can control the volume |
| - // on the remote cast device (if any). |
| - // Pre-MR1 versions of JB do not have the complete MediaRouter APIs, |
| - // so getting the MediaRouter instance will throw an exception. |
| - MediaRouter.getInstance(mContext).setMediaSessionCompat(mMediaSession); |
| - } catch (NoSuchMethodError e) { |
| - // Do nothing. Chrome can't be casting without a MediaRouter, so there is nothing |
| - // to do here. |
| - } |
| - mMediaSession.setMetadata(createMetadata()); |
| - |
| - PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder() |
| - .setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE); |
| - if (mMediaNotificationInfo.isPaused) { |
| - playbackStateBuilder.setState(PlaybackStateCompat.STATE_PAUSED, |
| - PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f); |
| - } else { |
| - playbackStateBuilder.setState(PlaybackStateCompat.STATE_PLAYING, |
| - PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f); |
| - } |
| - mMediaSession.setPlaybackState(playbackStateBuilder.build()); |
| - } |
| - |
| Notification notification = mNotificationBuilder.build(); |
| // We keep the service as a foreground service while the media is playing. When it is not, |
| @@ -653,6 +579,38 @@ public class MediaNotificationManager { |
| } |
| } |
| + private void updateMediaSession() { |
| + if (!mMediaNotificationInfo.supportsPlayPause()) return; |
| + |
| + if (mMediaSession == null) mMediaSession = createMediaSession(); |
| + |
| + try { |
| + // Tell the MediaRouter about the session, so that Chrome can control the volume |
| + // on the remote cast device (if any). |
| + // Pre-MR1 versions of JB do not have the complete MediaRouter APIs, |
| + // so getting the MediaRouter instance will throw an exception. |
| + MediaRouter.getInstance(mContext).setMediaSessionCompat(mMediaSession); |
| + } catch (NoSuchMethodError e) { |
| + // Do nothing. Chrome can't be casting without a MediaRouter, so there is nothing |
| + // to do here. |
| + } |
| + |
| + mMediaSession.setMetadata(createMetadata()); |
| + |
| + PlaybackStateCompat.Builder playbackStateBuilder = |
| + new PlaybackStateCompat.Builder().setActions( |
| + PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE); |
| + if (mMediaNotificationInfo.isPaused) { |
| + playbackStateBuilder.setState(PlaybackStateCompat.STATE_PAUSED, |
| + PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f); |
| + } else { |
| + // If notification only supports stop, still pretend |
| + playbackStateBuilder.setState(PlaybackStateCompat.STATE_PLAYING, |
| + PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f); |
| + } |
| + mMediaSession.setPlaybackState(playbackStateBuilder.build()); |
| + } |
| + |
| private MediaSessionCompat createMediaSession() { |
| MediaSessionCompat mediaSession = new MediaSessionCompat( |
| mContext, |
| @@ -680,6 +638,116 @@ public class MediaNotificationManager { |
| return mediaSession; |
| } |
| + private void setMediaStyleLayoutForNotificationBuilder(NotificationCompat.Builder builder) { |
| + // TODO(zqzhang): After we ship the new style, we should ses how to present the |
| + // metadata.artist and metadata.album. |
|
mlamouri (slow - plz ping)
2016/04/11 14:37:38
typo: "see"
nit: add a bug number :)
Zhiqiang Zhang (Slow)
2016/04/11 20:44:55
Done.
|
| + builder.setContentTitle(mMediaNotificationInfo.metadata.getTitle()); |
| + builder.setContentText(mMediaNotificationInfo.origin); |
| + // TODO: have our own "Media" icon so we can have large resolution in case of we have no |
| + // large icon? |
|
mlamouri (slow - plz ping)
2016/04/11 14:37:38
Maybe rephrase to "Update the default icon when a
Zhiqiang Zhang (Slow)
2016/04/11 20:44:55
Done.
|
| + if (mMediaNotificationInfo.largeIcon != null) { |
| + builder.setLargeIcon(mMediaNotificationInfo.largeIcon); |
| + } else { |
| + builder.setLargeIcon(mDefaultMediaSessionImage); |
| + } |
| + builder.setShowWhen(false); |
| + |
| + // Only apply MediaStyle when NotificationInfo supports play/pause. |
| + if (mMediaNotificationInfo.supportsPlayPause()) { |
| + NotificationCompat.MediaStyle style = new NotificationCompat.MediaStyle(); |
| + style.setMediaSession(mMediaSession.getSessionToken()); |
| + |
| + if (mMediaNotificationInfo.isPaused) { |
| + builder.addAction(R.drawable.ic_vidcontrol_play, mPlayDescription, |
| + createPendingIntent(ListenerService.ACTION_PLAY)); |
| + } else { |
| + // If we're here, the notification supports play/pause button and is playing. |
| + builder.addAction(R.drawable.ic_vidcontrol_pause, mPauseDescription, |
| + createPendingIntent(ListenerService.ACTION_PAUSE)); |
| + } |
| + style.setShowActionsInCompactView(0); |
| + |
| + builder.setStyle(style); |
|
mlamouri (slow - plz ping)
2016/04/11 14:37:38
Is there a "X" in the UI before L?
Zhiqiang Zhang (Slow)
2016/04/11 20:44:55
Fixed, finally :/
|
| + } |
| + |
| + if (mMediaNotificationInfo.supportsStop()) { |
| + builder.addAction(R.drawable.ic_vidcontrol_stop, mStopDescription, |
| + createPendingIntent(ListenerService.ACTION_STOP)); |
| + } |
| + } |
| + |
| + private void setCustomLayoutForNotificationBuilder(NotificationCompat.Builder builder) { |
| + builder.setContent(createContentView()); |
| + } |
| + |
| + private RemoteViews createContentView() { |
| + RemoteViews contentView = |
| + new RemoteViews(mContext.getPackageName(), R.layout.playback_notification_bar); |
| + |
| + // By default, play/pause button is the only one. |
| + int playPauseButtonId = R.id.button1; |
| + // On Android pre-L, dismissing the notification when the service is no longer in foreground |
| + // doesn't work. Instead, a STOP button is shown. |
| + if (mMediaNotificationInfo.supportsSwipeAway() |
| + && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP |
| + || mMediaNotificationInfo.supportsStop()) { |
| + contentView.setOnClickPendingIntent( |
| + R.id.button1, createPendingIntent(ListenerService.ACTION_STOP)); |
| + contentView.setContentDescription(R.id.button1, mStopDescription); |
| + |
| + // If the play/pause needs to be shown, it moves over to the second button from the end. |
| + playPauseButtonId = R.id.button2; |
| + } |
| + |
| + contentView.setTextViewText(R.id.title, mMediaNotificationInfo.metadata.getTitle()); |
| + contentView.setTextViewText(R.id.status, mMediaNotificationInfo.origin); |
| + |
| + // Android doesn't badge the icons for RemoteViews automatically when |
| + // running the app under the Work profile. |
| + if (mNotificationIcon == null) { |
| + Drawable notificationIconDrawable = |
| + ApiCompatibilityUtils.getUserBadgedIcon(mContext, mMediaNotificationInfo.icon); |
| + mNotificationIcon = drawableToBitmap(notificationIconDrawable); |
| + } |
| + |
| + if (mNotificationIcon != null) { |
| + contentView.setImageViewBitmap(R.id.icon, mNotificationIcon); |
| + } else { |
| + contentView.setImageViewResource(R.id.icon, mMediaNotificationInfo.icon); |
| + } |
| + |
| + if (mMediaNotificationInfo.supportsPlayPause()) { |
| + if (mMediaNotificationInfo.isPaused) { |
| + contentView.setImageViewResource(playPauseButtonId, R.drawable.ic_vidcontrol_play); |
| + contentView.setContentDescription(playPauseButtonId, mPlayDescription); |
| + contentView.setOnClickPendingIntent( |
| + playPauseButtonId, createPendingIntent(ListenerService.ACTION_PLAY)); |
| + } else { |
| + // If we're here, the notification supports play/pause button and is playing. |
| + contentView.setImageViewResource(playPauseButtonId, R.drawable.ic_vidcontrol_pause); |
| + contentView.setContentDescription(playPauseButtonId, mPauseDescription); |
| + contentView.setOnClickPendingIntent( |
| + playPauseButtonId, createPendingIntent(ListenerService.ACTION_PAUSE)); |
| + } |
| + |
| + contentView.setViewVisibility(playPauseButtonId, View.VISIBLE); |
| + } else { |
| + contentView.setViewVisibility(playPauseButtonId, View.GONE); |
| + } |
| + |
| + return contentView; |
| + } |
| + |
| + /** |
| + * Scales the Bitmap to have better looking on Wearable devices. |
|
mlamouri (slow - plz ping)
2016/04/11 14:37:38
"to have better looking" sounds odd. Also, should
Zhiqiang Zhang (Slow)
2016/04/11 20:44:55
Done.
|
| + * The returned Bitmap size will be exactly 400*400. |
| + */ |
| + private Bitmap scaleBitmapForWearable(Bitmap original) { |
| + Bitmap result = Bitmap.createScaledBitmap(original, WEARABLE_NOTIFICATION_BACKGROUND_WIDTH, |
| + WEARABLE_NOTIFICATION_BACKGROUND_HEIGHT, true); |
| + return result; |
| + } |
| + |
| private Bitmap drawableToBitmap(Drawable drawable) { |
| if (!(drawable instanceof BitmapDrawable)) return null; |