OLD | NEW |
---|---|
(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 } | |
OLD | NEW |