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

Side by Side 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: Fixed Mounir's 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 unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package org.chromium.chrome.browser.media.ui;
6
7 import android.app.Notification;
8 import android.app.PendingIntent;
9 import android.app.Service;
10 import android.content.Context;
11 import android.content.Intent;
12 import android.os.Handler;
13 import android.os.IBinder;
14 import android.provider.Browser;
15 import android.support.v4.app.NotificationCompat;
16 import android.support.v4.app.NotificationManagerCompat;
17 import android.widget.RemoteViews;
18
19 import org.chromium.chrome.R;
20 import org.chromium.chrome.browser.IntentHandler.TabOpenType;
21
22 import java.util.Set;
23 import java.util.concurrent.CopyOnWriteArraySet;
24
25 /**
26 * A class for notifications that provide information and optional media control s for a given media.
27 * Internally implements a Service for transforming notification Intents into
28 * {@link NotificationMediaPlaybackControls.Listener} calls for all registered l isteners.
29 */
30 public class NotificationMediaPlaybackControls {
31 /**
32 * Base interface for classes that need to listen to transport control event s.
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.
33 */
34 public static interface Listener {
35 /**
36 * Called when the user wants to resume the playback.
37 */
38 void onPlay();
39
40 /**
41 * Called when the user wants to pause the playback.
42 */
43 void onPause();
44 }
45
46 /**
47 * Service used to transform intent requests triggered from the notification into
48 * {@code Listener} callbacks. Ideally this class should be private, but pub lic is required to
49 * create as a service.
50 */
51 public static class ListenerService extends Service {
52 private static final String ACTION_PREFIX =
53 "NotificationMediaPlaybackControls.ListenerService.";
54
55 // Constants used by intent actions
56 public static final int ACTION_ID_PLAY = 0;
57 public static final int ACTION_ID_PAUSE = 1;
58
59 // Must be kept in sync with the ACTION_ID_XXX constants above
60 private static final String[] ACTION_VERBS = { "PLAY", "PAUSE" };
61
62 private PendingIntent[] mPendingIntents;
63
64 PendingIntent getPendingIntent(int id) {
65 return mPendingIntents[id];
66 }
67
68 @Override
69 public IBinder onBind(Intent intent) {
70 return null;
71 }
72
73 @Override
74 public void onCreate() {
75 super.onCreate();
76
77 // Create all the PendingIntents
78 int actionCount = ACTION_VERBS.length;
79 mPendingIntents = new PendingIntent[actionCount];
80 for (int i = 0; i < actionCount; ++i) {
81 Intent intent = new Intent(this, ListenerService.class);
82 intent.setAction(ACTION_PREFIX + ACTION_VERBS[i]);
83 mPendingIntents[i] = PendingIntent.getService(this, 0, intent,
84 PendingIntent.FLAG_CANCEL_CURRENT);
85 }
86 onServiceStarted(this);
87 }
88
89 @Override
90 public void onDestroy() {
91 onServiceDestroyed();
92 }
93
94 @Override
95 public int onStartCommand(Intent intent, int flags, int startId) {
96 if (intent == null) {
97 stopSelf();
98 return START_NOT_STICKY;
99 }
100
101 String action = intent.getAction();
102 if (action != null && action.startsWith(ACTION_PREFIX)) {
103 Set<Listener> listeners = sInstance.getListeners();
104
105 // Strip the prefix for matching the verb
106 action = action.substring(ACTION_PREFIX.length());
107 if (ACTION_VERBS[ACTION_ID_PLAY].equals(action)) {
108 for (Listener listener : listeners) listener.onPlay();
109 getOrCreate(getApplicationContext()).onPlaybackStateChanged( false);
110 } else if (ACTION_VERBS[ACTION_ID_PAUSE].equals(action)) {
111 for (Listener listener : listeners) listener.onPause();
112 getOrCreate(getApplicationContext()).onPlaybackStateChanged( true);
113 }
114 }
115
116 return START_STICKY;
117 }
118 }
119
120 private static NotificationMediaPlaybackControls sInstance = null;
121 private static final Object LOCK = new Object();
122 private static final int MSG_UPDATE_NOTIFICATION = 100;
123
124 /**
125 * Returns the singleton NotificationMediaPlaybackControls object.
126 *
127 * @param context The Context that the notification service needs to be crea ted in.
128 * @return A {@code NotificationMediaPlaybackControls} object.
129 */
130 public static NotificationMediaPlaybackControls getOrCreate(Context context) {
131 synchronized (LOCK) {
132 if (sInstance == null) {
133 sInstance = new NotificationMediaPlaybackControls(context);
134 }
135
136 return sInstance;
137 }
138 }
139
140 private static void onServiceDestroyed() {
141 sInstance.destroyNotification();
142 sInstance.mService = null;
143 }
144
145 private static void onServiceStarted(ListenerService service) {
146 sInstance.mService = service;
147 sInstance.createNotification();
148 }
149
150 private final Context mContext;
151
152 // ListenerService running for the notification. Only non-null when showing.
153 private ListenerService mService;
154
155 private final String mPlayDescription;
156
157 private final String mPauseDescription;
158
159 private Notification mNotification;
160
161 private Handler mHandler;
162 private Set<Listener> mListeners;
163 private MediaInfo mMediaInfo;
164
165 private NotificationMediaPlaybackControls(Context context) {
166 this.mContext = context;
167 mHandler = new Handler(context.getMainLooper()) {
168 @Override
169 public void handleMessage(android.os.Message msg) {
170 if (msg.what == MSG_UPDATE_NOTIFICATION) {
171 mHandler.removeMessages(MSG_UPDATE_NOTIFICATION); // Only on e update is needed.
172 updateNotificationInternal();
173 }
174 }
175 };
176
177 mPlayDescription = context.getResources().getString(R.string.accessibili ty_play);
178 mPauseDescription = context.getResources().getString(R.string.accessibil ity_pause);
179 }
180
181 public void hide(int tabId) {
182 if (getMediaInfo() == null || tabId != getMediaInfo().tabId) return;
183
184 mContext.stopService(new Intent(mContext, ListenerService.class));
185 }
186
187 /**
188 * @return true if the notification is currently visible to the user.
189 */
190 public boolean isShowing() {
191 return mService != null;
192 }
193
194 public void onPlaybackStateChanged(boolean isPaused) {
195 MediaInfo mediaInfo = new MediaInfo(getMediaInfo());
196 mediaInfo.isPaused = isPaused;
197 setMediaInfo(mediaInfo);
198 }
199
200 public void show(MediaInfo mediaInfo) {
201 setMediaInfo(mediaInfo);
202 mContext.startService(new Intent(mContext, ListenerService.class));
203 }
204
205 private void updateNotification() {
206 // Defer the call to updateNotificationInternal() so it can be cancelled in
207 // destroyNotification(). This is done to avoid the OS bug b/8798662.
208 mHandler.sendEmptyMessage(MSG_UPDATE_NOTIFICATION);
209 }
210
211 Notification getNotification() {
212 return mNotification;
213 }
214
215 final ListenerService getService() {
216 return mService;
217 }
218
219 private void onMediaInfoChanged() {
220 if (isShowing()) updateNotification();
221 }
222
223 private RemoteViews createContentView() {
224 RemoteViews contentView =
225 new RemoteViews(getContext().getPackageName(), R.layout.playback _notification_bar);
226 return contentView;
227 }
228
229 private void createNotification() {
230 NotificationCompat.Builder notificationBuilder =
231 new NotificationCompat.Builder(getContext())
232 .setSmallIcon(R.drawable.audio_playing)
233 .setAutoCancel(false)
234 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
235 .setOngoing(true)
236 .setContent(createContentView())
237 .setContentIntent(createContentIntent());
238 mNotification = notificationBuilder.build();
239 updateNotification();
240 }
241
242 private void destroyNotification() {
243 // Cancel any pending updates - we're about to tear down the notificatio n.
244 mHandler.removeMessages(MSG_UPDATE_NOTIFICATION);
245
246 NotificationManagerCompat manager = NotificationManagerCompat.from(getCo ntext());
247
248 manager.cancel(R.id.media_playback_notification);
249 mNotification = null;
250 }
251
252 private final Context getContext() {
253 return mContext;
254 }
255
256 private String getStatus() {
257 Context context = getContext();
258 MediaInfo mediaInfo = getMediaInfo();
259 return context.getString(R.string.media_notification_link_text,
260 mediaInfo.origin != null ? mediaInfo.origin : "");
261 }
262
263 private String getTitle() {
264 Context context = getContext();
265 MediaInfo mediaInfo = getMediaInfo();
266 String mediaTitle = mediaInfo.title;
267 if (mediaInfo.isPaused) {
268 return context.getString(
269 R.string.media_playback_notification_paused_for_media, media Title);
270 } else {
271 return context.getString(
272 R.string.media_playback_notification_playing_for_media, medi aTitle);
273 }
274 }
275
276 private PendingIntent createContentIntent() {
277 int tabId = getMediaInfo().tabId;
278 Intent intent = new Intent(Intent.ACTION_MAIN);
279 intent.putExtra(Browser.EXTRA_APPLICATION_ID, getContext().getPackageNam e());
280 intent.putExtra(TabOpenType.BRING_TAB_TO_FRONT.name(), tabId);
281 intent.setPackage(mContext.getPackageName());
282 return PendingIntent.getActivity(getContext(), tabId, intent, 0);
283 }
284
285 private void updateNotificationInternal() {
286 RemoteViews contentView = createContentView();
287
288 contentView.setTextViewText(R.id.title, getTitle());
289 contentView.setTextViewText(R.id.status, getStatus());
290 contentView.setImageViewResource(R.id.icon, R.drawable.audio_playing);
291
292 MediaInfo mediaInfo = getMediaInfo();
293 if (mediaInfo == null) return;
294
295 if (mediaInfo.isPaused) {
296 contentView.setImageViewResource(R.id.playpause, R.drawable.ic_vidco ntrol_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
297 contentView.setContentDescription(R.id.playpause, mPlayDescription);
298 contentView.setOnClickPendingIntent(R.id.playpause,
299 getService().getPendingIntent(ListenerService.ACTION_ID_PLAY ));
300 } else {
301 contentView.setImageViewResource(R.id.playpause, R.drawable.ic_vidco ntrol_pause);
mlamouri (slow - plz ping) 2015/06/24 16:15:02 ditto for pause.
whywhat 2015/06/24 18:36:15 Ditto
302 contentView.setContentDescription(R.id.playpause, mPauseDescription) ;
303 contentView.setOnClickPendingIntent(R.id.playpause,
304 getService().getPendingIntent(ListenerService.ACTION_ID_PAUS E));
305 }
306
307 mNotification.contentView = contentView;
308
309 NotificationManagerCompat manager = NotificationManagerCompat.from(getCo ntext());
310 manager.notify(R.id.media_playback_notification, mNotification);
311
312 getService().startForeground(R.id.media_playback_notification, mNotifica tion);
313 }
314
315 /**
316 * @return the media information previously assigned with
317 * {@link #setMediaInfo(MediaInfo)}, or {@code null} if the {@link MediaInfo }
318 * has not yet been assigned.
319 */
320 public final MediaInfo getMediaInfo() {
321 return mMediaInfo;
322 }
323
324 /**
325 * Sets the media information to display on the MediaPlaybackControls.
326 * @param mediaInfo the media information to use.
327 */
328 public final void setMediaInfo(MediaInfo mediaInfo) {
329 if (equal(mMediaInfo, mediaInfo)) return;
330
331 mMediaInfo = mediaInfo;
332 onMediaInfoChanged();
333 }
334
335 /**
336 * Registers a {@link Listener} with the MediaPlaybackControls.
337 * @param listener the Listener to be registered.
338 */
339 public void addListener(Listener listener) {
340 getListeners().add(listener);
341 }
342
343 /**
344 * Unregisters a {@link Listener} previously registered with {@link #addList ener(Listener)}.
345 * @param listener the Listener to be removed.
346 */
347 public void removeListener(Listener listener) {
348 getListeners().remove(listener);
349 }
350
351 /**
352 * @return the current list of listeners.
353 */
354 private final Set<Listener> getListeners() {
355 if (mListeners == null) mListeners = new CopyOnWriteArraySet<Listener>() ;
356 return mListeners;
357 }
358
359 private static boolean equal(Object a, Object b) {
360 return (a == null) ? (b == null) : a.equals(b);
361 }
362 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698