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..bfed7b395954ea104fa225c7ac1d0bdfc56bbd78 |
--- /dev/null |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/ui/NotificationMediaPlaybackControls.java |
@@ -0,0 +1,362 @@ |
+// 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.Handler; |
+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 java.util.Set; |
+import java.util.concurrent.CopyOnWriteArraySet; |
+ |
+/** |
+ * 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 NotificationMediaPlaybackControls.Listener} calls for all registered listeners. |
+ */ |
+public class NotificationMediaPlaybackControls { |
+ /** |
+ * Base interface for classes that need to listen to transport control events. |
mlamouri (slow - plz ping)
2015/06/24 16:15:02
Not sure if that comment is linked to the interfac
whywhat
2015/06/24 18:36:15
Done.
|
+ */ |
+ public static interface Listener { |
+ /** |
+ * Called when the user wants to resume the playback. |
+ */ |
+ void onPlay(); |
+ |
+ /** |
+ * Called when the user wants to pause the playback. |
+ */ |
+ void onPause(); |
+ } |
+ |
+ /** |
+ * 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_PREFIX = |
+ "NotificationMediaPlaybackControls.ListenerService."; |
+ |
+ // Constants used by intent actions |
+ public static final int ACTION_ID_PLAY = 0; |
+ public static final int ACTION_ID_PAUSE = 1; |
+ |
+ // Must be kept in sync with the ACTION_ID_XXX constants above |
+ private static final String[] ACTION_VERBS = { "PLAY", "PAUSE" }; |
+ |
+ private PendingIntent[] mPendingIntents; |
+ |
+ PendingIntent getPendingIntent(int id) { |
+ return mPendingIntents[id]; |
+ } |
+ |
+ @Override |
+ public IBinder onBind(Intent intent) { |
+ return null; |
+ } |
+ |
+ @Override |
+ public void onCreate() { |
+ super.onCreate(); |
+ |
+ // Create all the PendingIntents |
+ int actionCount = ACTION_VERBS.length; |
+ mPendingIntents = new PendingIntent[actionCount]; |
+ for (int i = 0; i < actionCount; ++i) { |
+ Intent intent = new Intent(this, ListenerService.class); |
+ intent.setAction(ACTION_PREFIX + ACTION_VERBS[i]); |
+ mPendingIntents[i] = PendingIntent.getService(this, 0, intent, |
+ PendingIntent.FLAG_CANCEL_CURRENT); |
+ } |
+ onServiceStarted(this); |
+ } |
+ |
+ @Override |
+ public void onDestroy() { |
+ onServiceDestroyed(); |
+ } |
+ |
+ @Override |
+ public int onStartCommand(Intent intent, int flags, int startId) { |
+ if (intent == null) { |
+ stopSelf(); |
+ return START_NOT_STICKY; |
+ } |
+ |
+ String action = intent.getAction(); |
+ if (action != null && action.startsWith(ACTION_PREFIX)) { |
+ Set<Listener> listeners = sInstance.getListeners(); |
+ |
+ // Strip the prefix for matching the verb |
+ action = action.substring(ACTION_PREFIX.length()); |
+ if (ACTION_VERBS[ACTION_ID_PLAY].equals(action)) { |
+ for (Listener listener : listeners) listener.onPlay(); |
+ getOrCreate(getApplicationContext()).onPlaybackStateChanged(false); |
+ } else if (ACTION_VERBS[ACTION_ID_PAUSE].equals(action)) { |
+ for (Listener listener : listeners) listener.onPause(); |
+ getOrCreate(getApplicationContext()).onPlaybackStateChanged(true); |
+ } |
+ } |
+ |
+ return START_STICKY; |
+ } |
+ } |
+ |
+ private static NotificationMediaPlaybackControls sInstance = null; |
+ private static final Object LOCK = new Object(); |
+ private static final int MSG_UPDATE_NOTIFICATION = 100; |
+ |
+ /** |
+ * Returns the singleton NotificationMediaPlaybackControls object. |
+ * |
+ * @param context The Context that the notification service needs to be created in. |
+ * @return A {@code NotificationMediaPlaybackControls} object. |
+ */ |
+ public static NotificationMediaPlaybackControls getOrCreate(Context context) { |
+ synchronized (LOCK) { |
+ if (sInstance == null) { |
+ sInstance = new NotificationMediaPlaybackControls(context); |
+ } |
+ |
+ return sInstance; |
+ } |
+ } |
+ |
+ private static void onServiceDestroyed() { |
+ sInstance.destroyNotification(); |
+ sInstance.mService = null; |
+ } |
+ |
+ private static void onServiceStarted(ListenerService service) { |
+ sInstance.mService = service; |
+ sInstance.createNotification(); |
+ } |
+ |
+ 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 Handler mHandler; |
+ private Set<Listener> mListeners; |
+ private MediaInfo mMediaInfo; |
+ |
+ private NotificationMediaPlaybackControls(Context context) { |
+ this.mContext = context; |
+ mHandler = new Handler(context.getMainLooper()) { |
+ @Override |
+ public void handleMessage(android.os.Message msg) { |
+ if (msg.what == MSG_UPDATE_NOTIFICATION) { |
+ mHandler.removeMessages(MSG_UPDATE_NOTIFICATION); // Only one update is needed. |
+ updateNotificationInternal(); |
+ } |
+ } |
+ }; |
+ |
+ mPlayDescription = context.getResources().getString(R.string.accessibility_play); |
+ mPauseDescription = context.getResources().getString(R.string.accessibility_pause); |
+ } |
+ |
+ public void hide(int tabId) { |
+ if (getMediaInfo() == null || tabId != getMediaInfo().tabId) return; |
+ |
+ mContext.stopService(new Intent(mContext, ListenerService.class)); |
+ } |
+ |
+ /** |
+ * @return true if the notification is currently visible to the user. |
+ */ |
+ public boolean isShowing() { |
+ return mService != null; |
+ } |
+ |
+ public void onPlaybackStateChanged(boolean isPaused) { |
+ MediaInfo mediaInfo = new MediaInfo(getMediaInfo()); |
+ mediaInfo.isPaused = isPaused; |
+ setMediaInfo(mediaInfo); |
+ } |
+ |
+ public void show(MediaInfo mediaInfo) { |
+ setMediaInfo(mediaInfo); |
+ mContext.startService(new Intent(mContext, ListenerService.class)); |
+ } |
+ |
+ private void updateNotification() { |
+ // Defer the call to updateNotificationInternal() so it can be cancelled in |
+ // destroyNotification(). This is done to avoid the OS bug b/8798662. |
+ mHandler.sendEmptyMessage(MSG_UPDATE_NOTIFICATION); |
+ } |
+ |
+ Notification getNotification() { |
+ return mNotification; |
+ } |
+ |
+ final ListenerService getService() { |
+ return mService; |
+ } |
+ |
+ private void onMediaInfoChanged() { |
+ if (isShowing()) updateNotification(); |
+ } |
+ |
+ private RemoteViews createContentView() { |
+ RemoteViews contentView = |
+ new RemoteViews(getContext().getPackageName(), R.layout.playback_notification_bar); |
+ return contentView; |
+ } |
+ |
+ private void createNotification() { |
+ NotificationCompat.Builder notificationBuilder = |
+ 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() { |
+ // Cancel any pending updates - we're about to tear down the notification. |
+ mHandler.removeMessages(MSG_UPDATE_NOTIFICATION); |
+ |
+ NotificationManagerCompat manager = NotificationManagerCompat.from(getContext()); |
+ |
+ manager.cancel(R.id.media_playback_notification); |
+ mNotification = null; |
+ } |
+ |
+ private final Context getContext() { |
+ return mContext; |
+ } |
+ |
+ private String getStatus() { |
+ Context context = getContext(); |
+ MediaInfo mediaInfo = getMediaInfo(); |
+ return context.getString(R.string.media_notification_link_text, |
+ mediaInfo.origin != null ? mediaInfo.origin : ""); |
+ } |
+ |
+ private String getTitle() { |
+ Context context = getContext(); |
+ MediaInfo mediaInfo = getMediaInfo(); |
+ String mediaTitle = mediaInfo.title; |
+ if (mediaInfo.isPaused) { |
+ return context.getString( |
+ R.string.media_playback_notification_paused_for_media, mediaTitle); |
+ } else { |
+ return context.getString( |
+ R.string.media_playback_notification_playing_for_media, mediaTitle); |
+ } |
+ } |
+ |
+ private PendingIntent createContentIntent() { |
+ int tabId = getMediaInfo().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 updateNotificationInternal() { |
+ RemoteViews contentView = createContentView(); |
+ |
+ contentView.setTextViewText(R.id.title, getTitle()); |
+ contentView.setTextViewText(R.id.status, getStatus()); |
+ contentView.setImageViewResource(R.id.icon, R.drawable.audio_playing); |
+ |
+ MediaInfo mediaInfo = getMediaInfo(); |
+ if (mediaInfo == null) return; |
+ |
+ if (mediaInfo.isPaused) { |
+ contentView.setImageViewResource(R.id.playpause, R.drawable.ic_vidcontrol_play); |
mlamouri (slow - plz ping)
2015/06/24 16:15:02
What's the difference between ic_media_play and ic
whywhat
2015/06/24 18:36:15
Maybe there's none. ic_vidcontrol_play is what our
|
+ contentView.setContentDescription(R.id.playpause, mPlayDescription); |
+ contentView.setOnClickPendingIntent(R.id.playpause, |
+ getService().getPendingIntent(ListenerService.ACTION_ID_PLAY)); |
+ } else { |
+ contentView.setImageViewResource(R.id.playpause, R.drawable.ic_vidcontrol_pause); |
mlamouri (slow - plz ping)
2015/06/24 16:15:02
ditto for pause.
whywhat
2015/06/24 18:36:15
Ditto
|
+ contentView.setContentDescription(R.id.playpause, mPauseDescription); |
+ contentView.setOnClickPendingIntent(R.id.playpause, |
+ getService().getPendingIntent(ListenerService.ACTION_ID_PAUSE)); |
+ } |
+ |
+ mNotification.contentView = contentView; |
+ |
+ NotificationManagerCompat manager = NotificationManagerCompat.from(getContext()); |
+ manager.notify(R.id.media_playback_notification, mNotification); |
+ |
+ getService().startForeground(R.id.media_playback_notification, mNotification); |
+ } |
+ |
+ /** |
+ * @return the media information previously assigned with |
+ * {@link #setMediaInfo(MediaInfo)}, or {@code null} if the {@link MediaInfo} |
+ * has not yet been assigned. |
+ */ |
+ public final MediaInfo getMediaInfo() { |
+ return mMediaInfo; |
+ } |
+ |
+ /** |
+ * Sets the media information to display on the MediaPlaybackControls. |
+ * @param mediaInfo the media information to use. |
+ */ |
+ public final void setMediaInfo(MediaInfo mediaInfo) { |
+ if (equal(mMediaInfo, mediaInfo)) return; |
+ |
+ mMediaInfo = mediaInfo; |
+ onMediaInfoChanged(); |
+ } |
+ |
+ /** |
+ * Registers a {@link Listener} with the MediaPlaybackControls. |
+ * @param listener the Listener to be registered. |
+ */ |
+ public void addListener(Listener listener) { |
+ getListeners().add(listener); |
+ } |
+ |
+ /** |
+ * Unregisters a {@link Listener} previously registered with {@link #addListener(Listener)}. |
+ * @param listener the Listener to be removed. |
+ */ |
+ public void removeListener(Listener listener) { |
+ getListeners().remove(listener); |
+ } |
+ |
+ /** |
+ * @return the current list of listeners. |
+ */ |
+ private final Set<Listener> getListeners() { |
+ if (mListeners == null) mListeners = new CopyOnWriteArraySet<Listener>(); |
+ return mListeners; |
+ } |
+ |
+ private static boolean equal(Object a, Object b) { |
+ return (a == null) ? (b == null) : a.equals(b); |
+ } |
+} |