Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1040)

Unified Diff: chrome/android/java/src/org/chromium/chrome/browser/media/ui/NotificationMediaPlaybackControls.java

Issue 1159113006: [Android] A prototype of the interactive media notification. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Addressed the comments Created 5 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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..32c039f45e15e5019bddc869770adb2949f2eca2
--- /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 {
+ /**
+ * Interface for classes that need to be notified about play/pause events.
+ */
+ 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 {
qinmin 2015/06/24 19:10:20 no need for else
whywhat 2015/06/25 10:10:52 Indeed. Done.
+ 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);
+ 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);
+ 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) {
qinmin 2015/06/24 19:10:20 use Object.equals() instead
whywhat 2015/06/25 10:10:52 Object.equals() doesn't work for nullable objects.
qinmin 2015/06/25 15:08:43 Sorry, I mean Objects.equals(obj1, obj2)
whywhat 2015/06/25 16:25:07 I suspected so. Unfortunately, Objects is only sup
+ return (a == null) ? (b == null) : a.equals(b);
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698