Chromium Code Reviews| Index: chrome/android/java/src/org/chromium/chrome/browser/media/ui/NotificationMediaPlaybackControls.java |
| diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/ui/NotificationMediaPlaybackControls.java b/chrome/android/java/src/org/chromium/chrome/browser/media/ui/NotificationMediaPlaybackControls.java |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..a47822296dc6f694efea674994d57ebaf0fe3827 |
| --- /dev/null |
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/media/ui/NotificationMediaPlaybackControls.java |
| @@ -0,0 +1,278 @@ |
| +// Copyright 2015 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +package org.chromium.chrome.browser.media.ui; |
| + |
| +import android.app.Notification; |
| +import android.app.PendingIntent; |
| +import android.app.Service; |
| +import android.content.Context; |
| +import android.content.Intent; |
| +import android.os.IBinder; |
| +import android.provider.Browser; |
| +import android.support.v4.app.NotificationCompat; |
| +import android.support.v4.app.NotificationManagerCompat; |
| +import android.widget.RemoteViews; |
| + |
| +import org.chromium.chrome.R; |
| +import org.chromium.chrome.browser.IntentHandler.TabOpenType; |
| +import org.chromium.chrome.browser.Tab; |
| + |
| +/** |
| + * A class for notifications that provide information and optional media controls for a given media. |
| + * Internally implements a Service for transforming notification Intents into |
| + * {@link MediaPlaybackListener} calls for all registered listeners. |
| + */ |
| +public class NotificationMediaPlaybackControls { |
| + private static final Object LOCK = new Object(); |
| + private static final int MSG_ID_UPDATE_NOTIFICATION = 100; |
|
Ted C
2015/07/06 22:18:28
not used
whywhat
2015/07/07 15:44:58
Done.
|
| + private static NotificationMediaPlaybackControls sInstance; |
| + |
| + /** |
| + * Service used to transform intent requests triggered from the notification into |
| + * {@code Listener} callbacks. Ideally this class should be private, but public is required to |
| + * create as a service. |
| + */ |
| + public static class ListenerService extends Service { |
| + private static final String ACTION_PLAY = |
| + "NotificationMediaPlaybackControls.ListenerService.PLAY"; |
| + private static final String ACTION_PAUSE = |
| + "NotificationMediaPlaybackControls.ListenerService.PAUSE"; |
| + |
| + private PendingIntent getPendingIntent(String action) { |
| + Intent intent = new Intent(this, ListenerService.class); |
| + intent.setAction(action); |
| + return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); |
| + } |
| + |
| + @Override |
| + public IBinder onBind(Intent intent) { |
| + return null; |
| + } |
| + |
| + @Override |
| + public void onCreate() { |
| + super.onCreate(); |
| + onServiceStarted(this); |
| + } |
| + |
| + @Override |
| + public void onDestroy() { |
|
Ted C
2015/07/06 22:18:29
should you call super.onDestroy() here as well?
whywhat
2015/07/07 15:44:58
Done.
|
| + onServiceDestroyed(); |
| + } |
| + |
| + @Override |
| + public int onStartCommand(Intent intent, int flags, int startId) { |
| + if (intent == null |
| + || sInstance == null |
| + || sInstance.mMediaInfo == null |
| + || sInstance.mMediaInfo.listener == null) { |
| + stopSelf(); |
| + return START_NOT_STICKY; |
| + } |
| + |
| + String action = intent.getAction(); |
| + if (ACTION_PLAY.equals(action)) { |
| + sInstance.mMediaInfo.listener.onPlay(); |
| + sInstance.onPlaybackStateChanged(false); |
| + } else if (ACTION_PAUSE.equals(action)) { |
| + sInstance.mMediaInfo.listener.onPause(); |
| + sInstance.onPlaybackStateChanged(true); |
| + } |
| + |
| + return START_NOT_STICKY; |
| + } |
| + } |
| + |
| + /** |
| + * Shows the notification with media controls with the specified media info. Replaces/updates |
| + * the current notification if already showing. Does nothing if |mediaInfo| hasn't changed from |
| + * the last one. |
| + * |
| + * @param applicationContext context to create the notification with |
| + * @param mediaInfo information to show in the notification |
| + */ |
| + public static void show(Context applicationContext, MediaInfo mediaInfo) { |
| + synchronized (LOCK) { |
| + if (sInstance == null) { |
| + sInstance = new NotificationMediaPlaybackControls(applicationContext); |
| + } |
| + } |
| + sInstance.showNotification(mediaInfo); |
| + } |
| + |
| + /** |
| + * Hides the notification for the specified tabId. If tabId equals to |
| + * {@link Tab#INVALID_TAB_ID}, hides any notification shown. |
| + * |
| + * @param tabId the id of the tab that showed the notification or invalid tab id. |
| + */ |
| + public static void hide(int tabId) { |
| + if (sInstance == null) return; |
| + sInstance.hideNotification(tabId); |
| + } |
| + |
| + /** |
| + * Registers the started {@link Service} with the singleton and creates the notification. |
| + * |
| + * @param service the service that was started |
| + */ |
| + private static void onServiceStarted(ListenerService service) { |
| + assert sInstance != null; |
| + assert sInstance.mService == null; |
| + sInstance.mService = service; |
| + sInstance.createNotification(); |
| + } |
| + |
| + /** |
| + * Handles the destruction |
| + */ |
| + private static void onServiceDestroyed() { |
| + assert sInstance != null; |
| + assert sInstance.mService != null; |
| + sInstance.destroyNotification(); |
| + sInstance.mService = null; |
| + } |
| + |
| + private final Context mContext; |
| + |
| + // ListenerService running for the notification. Only non-null when showing. |
| + private ListenerService mService; |
| + |
| + private final String mPlayDescription; |
| + |
| + private final String mPauseDescription; |
| + |
| + private Notification mNotification; |
| + |
| + private MediaInfo mMediaInfo; |
| + |
| + private NotificationMediaPlaybackControls(Context context) { |
| + mContext = context; |
| + mPlayDescription = context.getResources().getString(R.string.accessibility_play); |
| + mPauseDescription = context.getResources().getString(R.string.accessibility_pause); |
| + } |
| + |
| + private void showNotification(MediaInfo mediaInfo) { |
| + mContext.startService(new Intent(mContext, ListenerService.class)); |
| + |
| + assert mediaInfo != null; |
| + |
| + if (mediaInfo.equals(mMediaInfo)) return; |
| + |
| + mMediaInfo = mediaInfo; |
| + updateNotification(); |
| + } |
| + |
| + private void hideNotification(int tabId) { |
| + if (mMediaInfo == null || (tabId != Tab.INVALID_TAB_ID && tabId != mMediaInfo.tabId)) { |
|
Ted C
2015/07/06 22:18:28
If you have a problem with a dangling notification
whywhat
2015/07/07 15:44:58
Done.
|
| + return; |
| + } |
| + |
| + mMediaInfo = null; |
| + mContext.stopService(new Intent(mContext, ListenerService.class)); |
| + } |
| + |
| + private void onPlaybackStateChanged(boolean isPaused) { |
| + assert mMediaInfo != null; |
| + mMediaInfo = new MediaInfo( |
| + mMediaInfo.title, |
| + isPaused, |
| + mMediaInfo.origin, |
| + mMediaInfo.tabId, |
| + mMediaInfo.listener); |
| + updateNotification(); |
| + } |
| + |
| + private RemoteViews createContentView() { |
| + RemoteViews contentView = |
| + new RemoteViews(getContext().getPackageName(), R.layout.playback_notification_bar); |
| + return contentView; |
| + } |
| + |
| + private void createNotification() { |
| + NotificationCompat.Builder notificationBuilder = |
|
Ted C
2015/07/06 22:18:28
I would just inline this in updateNotification. Y
whywhat
2015/07/07 15:44:58
Done.
|
| + new NotificationCompat.Builder(getContext()) |
| + .setSmallIcon(R.drawable.audio_playing) |
| + .setAutoCancel(false) |
| + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) |
| + .setOngoing(true) |
| + .setContent(createContentView()) |
| + .setContentIntent(createContentIntent()); |
| + mNotification = notificationBuilder.build(); |
| + updateNotification(); |
| + } |
| + |
| + private void destroyNotification() { |
| + NotificationManagerCompat manager = NotificationManagerCompat.from(getContext()); |
| + manager.cancel(R.id.media_playback_notification); |
| + |
| + mNotification = null; |
| + } |
| + |
| + private final Context getContext() { |
|
Ted C
2015/07/06 22:18:28
remove this and just inline mContext everywhere.
whywhat
2015/07/07 15:44:57
Done.
|
| + return mContext; |
| + } |
| + |
| + private String getStatus() { |
| + Context context = getContext(); |
| + return context.getString(R.string.media_notification_link_text, |
| + mMediaInfo.origin != null ? mMediaInfo.origin : ""); |
|
Ted C
2015/07/06 22:18:28
You should probably have some Unknown URL or somet
whywhat
2015/07/07 15:44:57
Done.
|
| + } |
| + |
| + private String getTitle() { |
| + Context context = getContext(); |
| + String mediaTitle = mMediaInfo.title; |
| + if (mMediaInfo.isPaused) { |
| + return context.getString( |
| + R.string.media_playback_notification_paused_for_media, mediaTitle); |
| + } |
| + return context.getString( |
| + R.string.media_playback_notification_playing_for_media, mediaTitle); |
| + } |
| + |
| + private PendingIntent createContentIntent() { |
| + int tabId = mMediaInfo.tabId; |
| + Intent intent = new Intent(Intent.ACTION_MAIN); |
| + intent.putExtra(Browser.EXTRA_APPLICATION_ID, getContext().getPackageName()); |
| + intent.putExtra(TabOpenType.BRING_TAB_TO_FRONT.name(), tabId); |
| + intent.setPackage(mContext.getPackageName()); |
| + return PendingIntent.getActivity(getContext(), tabId, intent, 0); |
| + } |
| + |
| + private void updateNotification() { |
| + if (mService == null) return; |
| + |
| + if (mMediaInfo == null) { |
| + // Notification was hidden before we could update it. |
| + assert mNotification == null; |
| + return; |
| + } |
| + |
| + RemoteViews contentView = createContentView(); |
| + |
| + contentView.setTextViewText(R.id.title, getTitle()); |
| + contentView.setTextViewText(R.id.status, getStatus()); |
| + contentView.setImageViewResource(R.id.icon, R.drawable.audio_playing); |
| + |
| + if (mMediaInfo.isPaused) { |
| + contentView.setImageViewResource(R.id.playpause, R.drawable.ic_vidcontrol_play); |
| + contentView.setContentDescription(R.id.playpause, mPlayDescription); |
| + contentView.setOnClickPendingIntent(R.id.playpause, |
| + mService.getPendingIntent(ListenerService.ACTION_PLAY)); |
| + } else { |
| + contentView.setImageViewResource(R.id.playpause, R.drawable.ic_vidcontrol_pause); |
| + contentView.setContentDescription(R.id.playpause, mPauseDescription); |
| + contentView.setOnClickPendingIntent(R.id.playpause, |
| + mService.getPendingIntent(ListenerService.ACTION_PAUSE)); |
| + } |
| + |
| + mNotification.contentView = contentView; |
| + |
| + NotificationManagerCompat manager = NotificationManagerCompat.from(getContext()); |
| + manager.notify(R.id.media_playback_notification, mNotification); |
|
Ted C
2015/07/06 22:18:28
I would use a notification namespace here (and it'
whywhat
2015/07/07 15:44:58
So we actually don't need NotificationManager at a
|
| + |
| + mService.startForeground(R.id.media_playback_notification, mNotification); |
| + } |
| +} |