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 { | |
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 { | |
qinmin
2015/06/24 19:10:20
no need for else
whywhat
2015/06/25 10:10:52
Indeed. Done.
| |
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); | |
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); | |
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) { | |
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
| |
360 return (a == null) ? (b == null) : a.equals(b); | |
361 } | |
362 } | |
OLD | NEW |