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

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 tests and Min's nits Created 5 years, 5 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 * Interface for classes that need to be notified about play/pause events.
33 */
34 public static interface Listener {
Ted C 2015/06/26 00:43:53 I would call this MediaPlaybackListener so it is e
whywhat 2015/06/26 19:29:32 Done.
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;
Ted C 2015/06/26 00:43:54 I'm confused why there is this mapping? Why not j
whywhat 2015/06/26 19:29:31 The notification may be recreated every second or
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);
Ted C 2015/06/26 00:43:54 why getOrCreate here but you use sInstance above.
whywhat 2015/06/26 19:29:31 Done.
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;
Ted C 2015/06/26 00:43:54 null is the default, so don't need it.
whywhat 2015/06/26 19:29:32 Done.
121 private static final Object LOCK = new Object();
Ted C 2015/06/26 00:43:54 move the static finals above the plain ol' static.
whywhat 2015/06/26 19:29:31 Done.
122 private static final int MSG_UPDATE_NOTIFICATION = 100;
Ted C 2015/06/26 00:43:53 I would call this MSG_ID_UPDATE_...
whywhat 2015/06/26 19:29:32 Done.
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) {
Ted C 2015/06/26 00:43:54 Seems like a few places in the Tab logic would ben
whywhat 2015/06/26 19:29:31 Done.
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;
Ted C 2015/06/26 00:43:54 assert mService == null?
whywhat 2015/06/26 19:29:32 On 2015/06/26 at 00:43:54, Ted C wrote: > assert m
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;
Ted C 2015/06/26 00:43:54 Should this be an observerList?
whywhat 2015/06/26 19:29:31 Done.
163 private MediaInfo mMediaInfo;
Ted C 2015/06/26 00:43:54 Can there ever only be one media info shown? If s
whywhat 2015/06/26 19:29:32 Currently, yes and yes (one media info, one listen
164
165 private NotificationMediaPlaybackControls(Context context) {
166 this.mContext = context;
Ted C 2015/06/26 00:43:54 "this." is unnecessary
whywhat 2015/06/26 19:29:31 Done.
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
Ted C 2015/06/26 00:43:54 should you be setting mMediaInfo = null here?
whywhat 2015/06/26 19:29:31 done.
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) {
Ted C 2015/06/26 00:43:55 there seems to be a fair amount of public methods
whywhat 2015/06/26 19:29:31 Done.
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() {
Ted C 2015/06/26 00:43:54 why getNotification instead of just referencing mN
212 return mNotification;
213 }
214
215 final ListenerService getService() {
216 return mService;
217 }
218
219 private void onMediaInfoChanged() {
Ted C 2015/06/26 00:43:54 only called one place, inline it
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() {
Ted C 2015/06/26 00:43:54 For the MediaNotificationService, we found destroy
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 }
271 return context.getString(
272 R.string.media_playback_notification_playing_for_media, mediaTit le);
273 }
274
275 private PendingIntent createContentIntent() {
276 int tabId = getMediaInfo().tabId;
277 Intent intent = new Intent(Intent.ACTION_MAIN);
278 intent.putExtra(Browser.EXTRA_APPLICATION_ID, getContext().getPackageNam e());
279 intent.putExtra(TabOpenType.BRING_TAB_TO_FRONT.name(), tabId);
280 intent.setPackage(mContext.getPackageName());
281 return PendingIntent.getActivity(getContext(), tabId, intent, 0);
282 }
283
284 private void updateNotificationInternal() {
285 RemoteViews contentView = createContentView();
286
287 contentView.setTextViewText(R.id.title, getTitle());
288 contentView.setTextViewText(R.id.status, getStatus());
289 contentView.setImageViewResource(R.id.icon, R.drawable.audio_playing);
290
291 MediaInfo mediaInfo = getMediaInfo();
292 if (mediaInfo == null) return;
Ted C 2015/06/26 00:43:54 Should you be destroying the notification in this
whywhat 2015/06/26 19:29:32 Done.
293
294 if (mediaInfo.isPaused) {
295 contentView.setImageViewResource(R.id.playpause, R.drawable.ic_vidco ntrol_play);
296 contentView.setContentDescription(R.id.playpause, mPlayDescription);
297 contentView.setOnClickPendingIntent(R.id.playpause,
298 getService().getPendingIntent(ListenerService.ACTION_ID_PLAY ));
299 } else {
300 contentView.setImageViewResource(R.id.playpause, R.drawable.ic_vidco ntrol_pause);
301 contentView.setContentDescription(R.id.playpause, mPauseDescription) ;
302 contentView.setOnClickPendingIntent(R.id.playpause,
303 getService().getPendingIntent(ListenerService.ACTION_ID_PAUS E));
304 }
305
306 mNotification.contentView = contentView;
307
308 NotificationManagerCompat manager = NotificationManagerCompat.from(getCo ntext());
309 manager.notify(R.id.media_playback_notification, mNotification);
310
311 getService().startForeground(R.id.media_playback_notification, mNotifica tion);
312 }
313
314 /**
315 * @return the media information previously assigned with
316 * {@link #setMediaInfo(MediaInfo)}, or {@code null} if the {@link MediaInfo }
317 * has not yet been assigned.
318 */
319 public final MediaInfo getMediaInfo() {
320 return mMediaInfo;
321 }
322
323 /**
324 * Sets the media information to display on the MediaPlaybackControls.
325 * @param mediaInfo the media information to use.
326 */
327 public final void setMediaInfo(MediaInfo mediaInfo) {
328 if ((mediaInfo == null && mMediaInfo == null)
329 || (mMediaInfo != null && mMediaInfo.equals(mediaInfo))) {
330 return;
331 }
332
333 mMediaInfo = mediaInfo;
334 onMediaInfoChanged();
335 }
336
337 /**
338 * Registers a {@link Listener} with the MediaPlaybackControls.
339 * @param listener the Listener to be registered.
340 */
341 public void addListener(Listener listener) {
342 getListeners().add(listener);
343 }
344
345 /**
346 * Unregisters a {@link Listener} previously registered with {@link #addList ener(Listener)}.
347 * @param listener the Listener to be removed.
348 */
349 public void removeListener(Listener listener) {
350 getListeners().remove(listener);
351 }
352
353 /**
354 * @return the current list of listeners.
355 */
356 private final Set<Listener> getListeners() {
357 if (mListeners == null) mListeners = new CopyOnWriteArraySet<Listener>() ;
Ted C 2015/06/26 00:43:54 I would just initialize this in the constructor.
whywhat 2015/06/26 19:29:31 Done.
358 return mListeners;
359 }
360 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698