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 | |
24 /** | |
25 * A class for notifications that provide information and optional media control s for a given media. | |
26 * Internally implements a Service for transforming notification Intents into | |
27 * {@link MediaPlaybackControls.Listener} calls for all registered listeners. | |
28 * | |
mlamouri (slow - plz ping)
2015/06/23 14:58:57
nit: remove that line?
whywhat
2015/06/23 19:39:11
Done.
| |
29 */ | |
30 public class NotificationMediaPlaybackControls extends MediaPlaybackControls { | |
31 /** | |
32 * Service used to transform intent requests triggered from the notification into | |
33 * {@code Listener} callbacks. Ideally this class should be protected, but p ublic is required to | |
34 * create as a service. | |
35 */ | |
36 public static class ListenerService extends Service { | |
37 private static final String ACTION_PREFIX = | |
38 "NotificationMediaPlaybackControls.ListenerService."; | |
39 | |
40 // Constants used by intent actions | |
41 public static final int ACTION_ID_PLAY = 0; | |
42 public static final int ACTION_ID_PAUSE = 1; | |
43 | |
44 // Must be kept in sync with the ACTION_ID_XXX constants above | |
45 private static final String[] ACTION_VERBS = { "PLAY", "PAUSE" }; | |
46 | |
47 private PendingIntent[] mPendingIntents; | |
48 | |
49 PendingIntent getPendingIntent(int id) { | |
50 return mPendingIntents[id]; | |
51 } | |
52 | |
53 @Override | |
54 public IBinder onBind(Intent intent) { | |
55 return null; | |
56 } | |
57 | |
58 @Override | |
59 public void onCreate() { | |
60 super.onCreate(); | |
61 | |
62 // Create all the PendingIntents | |
63 int actionCount = ACTION_VERBS.length; | |
64 mPendingIntents = new PendingIntent[actionCount]; | |
65 for (int i = 0; i < actionCount; ++i) { | |
66 Intent intent = new Intent(this, ListenerService.class); | |
67 intent.setAction(ACTION_PREFIX + ACTION_VERBS[i]); | |
68 mPendingIntents[i] = PendingIntent.getService(this, 0, intent, | |
69 PendingIntent.FLAG_CANCEL_CURRENT); | |
70 } | |
71 onServiceStarted(this); | |
72 } | |
73 | |
74 @Override | |
75 public void onDestroy() { | |
76 onServiceDestroyed(); | |
77 } | |
78 | |
79 @Override | |
80 public int onStartCommand(Intent intent, int flags, int startId) { | |
81 if (intent == null) { | |
82 stopSelf(); | |
83 return START_NOT_STICKY; | |
84 } | |
85 | |
86 String action = intent.getAction(); | |
87 if (action != null && action.startsWith(ACTION_PREFIX)) { | |
88 Set<Listener> listeners = sInstance.getListeners(); | |
89 | |
90 // Strip the prefix for matching the verb | |
91 action = action.substring(ACTION_PREFIX.length()); | |
92 if (ACTION_VERBS[ACTION_ID_PLAY].equals(action)) { | |
93 for (Listener listener : listeners) listener.onPlay(); | |
94 } else if (ACTION_VERBS[ACTION_ID_PAUSE].equals(action)) { | |
95 for (Listener listener : listeners) listener.onPause(); | |
96 } | |
97 } | |
98 | |
99 return START_STICKY; | |
100 } | |
101 } | |
102 | |
103 private static NotificationMediaPlaybackControls sInstance = null; | |
104 private static final Object LOCK = new Object(); | |
105 private static final int MSG_UPDATE_NOTIFICATION = 100; | |
106 | |
107 /** | |
108 * Returns the singleton NotificationMediaPlaybackControls object. | |
109 * | |
110 * @param context The Context that the notification service needs to be crea ted in. | |
111 * @return A {@code NotificationMediaPlaybackControls} object. | |
112 */ | |
113 public static NotificationMediaPlaybackControls getOrCreate(Context context) { | |
114 synchronized (LOCK) { | |
115 if (sInstance == null) { | |
116 sInstance = new NotificationMediaPlaybackControls(context); | |
117 } | |
118 | |
119 return sInstance; | |
120 } | |
121 } | |
122 | |
123 private static void onServiceDestroyed() { | |
124 sInstance.destroyNotification(); | |
125 sInstance.mService = null; | |
126 } | |
127 | |
128 private static void onServiceStarted(ListenerService service) { | |
129 sInstance.mService = service; | |
130 sInstance.createNotification(); | |
131 } | |
132 | |
133 private final Context mContext; | |
134 | |
135 // ListenerService running for the notification. Only non-null when showing. | |
136 private ListenerService mService; | |
137 | |
138 private final String mPlayDescription; | |
139 | |
140 private final String mPauseDescription; | |
141 | |
142 private Notification mNotification; | |
143 | |
144 private Handler mHandler; | |
145 | |
146 private NotificationMediaPlaybackControls(Context context) { | |
147 this.mContext = context; | |
148 mHandler = new Handler(context.getMainLooper()) { | |
149 @Override | |
150 public void handleMessage(android.os.Message msg) { | |
151 if (msg.what == MSG_UPDATE_NOTIFICATION) { | |
152 mHandler.removeMessages(MSG_UPDATE_NOTIFICATION); // Only on e update is needed. | |
153 updateNotificationInternal(); | |
154 } | |
155 } | |
156 }; | |
157 | |
158 mPlayDescription = context.getResources().getString(R.string.accessibili ty_play); | |
159 mPauseDescription = context.getResources().getString(R.string.accessibil ity_pause); | |
160 } | |
161 | |
162 @Override | |
163 public void hide(int tabId) { | |
164 if (getMediaInfo() == null || tabId != getMediaInfo().tabId) return; | |
165 | |
166 mContext.stopService(new Intent(mContext, ListenerService.class)); | |
167 } | |
168 | |
169 /** | |
170 * @return true if the notification is currently visible to the user. | |
171 */ | |
172 public boolean isShowing() { | |
173 return mService != null; | |
174 } | |
175 | |
176 @Override | |
177 public void onPlaybackStateChanged(boolean isPaused) { | |
178 MediaInfo mediaInfo = new MediaInfo(getMediaInfo()); | |
179 mediaInfo.isPaused = isPaused; | |
180 setMediaInfo(mediaInfo); | |
181 } | |
182 | |
183 @Override | |
184 public void onTitleChanged(String title) { | |
185 MediaInfo mediaInfo = new MediaInfo(getMediaInfo()); | |
186 mediaInfo.title = title; | |
187 setMediaInfo(mediaInfo); | |
188 } | |
189 | |
190 @Override | |
191 public void onOriginChanged(String origin) { | |
192 MediaInfo mediaInfo = new MediaInfo(getMediaInfo()); | |
193 mediaInfo.origin = origin; | |
194 setMediaInfo(mediaInfo); | |
195 } | |
196 | |
197 @Override | |
198 public void onTabIdChanged(int tabId) { | |
199 MediaInfo mediaInfo = new MediaInfo(getMediaInfo()); | |
200 mediaInfo.tabId = tabId; | |
201 setMediaInfo(mediaInfo); | |
202 } | |
203 | |
204 @Override | |
205 public void show(MediaInfo mediaInfo) { | |
206 setMediaInfo(mediaInfo); | |
207 mContext.startService(new Intent(mContext, ListenerService.class)); | |
208 } | |
209 | |
210 private void updateNotification() { | |
211 // Defer the call to updateNotificationInternal() so it can be cancelled in | |
212 // destroyNotification(). This is done to avoid the OS bug b/8798662. | |
213 mHandler.sendEmptyMessage(MSG_UPDATE_NOTIFICATION); | |
214 } | |
215 | |
216 Notification getNotification() { | |
217 return mNotification; | |
218 } | |
219 | |
220 final ListenerService getService() { | |
221 return mService; | |
222 } | |
223 | |
224 @Override | |
225 protected void onMediaInfoChanged() { | |
226 if (isShowing()) updateNotification(); | |
227 } | |
228 | |
229 private RemoteViews createContentView() { | |
230 RemoteViews contentView = | |
231 new RemoteViews(getContext().getPackageName(), R.layout.playback _notification_bar); | |
232 return contentView; | |
233 } | |
234 | |
235 private void createNotification() { | |
236 NotificationCompat.Builder notificationBuilder = | |
237 new NotificationCompat.Builder(getContext()) | |
238 .setSmallIcon(R.drawable.audio_playing) | |
239 .setAutoCancel(false) | |
240 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) | |
241 .setOngoing(true) | |
242 .setContent(createContentView()) | |
mlamouri (slow - plz ping)
2015/06/23 14:58:57
Why not using setContentTitle() and setContentText
whywhat
2015/06/23 19:39:11
It's how we achieve the desired consistent layout
| |
243 .setContentIntent(createContentIntent()); | |
244 mNotification = notificationBuilder.build(); | |
245 updateNotification(); | |
246 } | |
247 | |
248 private void destroyNotification() { | |
249 // Cancel any pending updates - we're about to tear down the notificatio n. | |
250 mHandler.removeMessages(MSG_UPDATE_NOTIFICATION); | |
251 | |
252 NotificationManagerCompat manager = NotificationManagerCompat.from(getCo ntext()); | |
253 | |
254 manager.cancel(R.id.media_playback_notification); | |
255 mNotification = null; | |
256 } | |
257 | |
258 private final Context getContext() { | |
259 return mContext; | |
260 } | |
261 | |
262 private String getStatus() { | |
263 Context context = getContext(); | |
264 MediaInfo mediaInfo = getMediaInfo(); | |
265 return context.getString(R.string.media_notification_link_text, | |
266 mediaInfo.origin != null ? mediaInfo.origin : ""); | |
267 } | |
268 | |
269 private String getTitle() { | |
270 Context context = getContext(); | |
271 MediaInfo mediaInfo = getMediaInfo(); | |
272 String mediaTitle = mediaInfo.title; | |
273 if (mediaInfo.isPaused) { | |
274 return context.getString( | |
275 R.string.media_playback_notification_paused_for_media, media Title); | |
276 } else { | |
277 return context.getString( | |
278 R.string.media_playback_notification_playing_for_media, medi aTitle); | |
279 } | |
280 } | |
281 | |
282 private PendingIntent createContentIntent() { | |
283 int tabId = getMediaInfo().tabId; | |
284 Intent intent = new Intent(Intent.ACTION_MAIN); | |
285 intent.putExtra(Browser.EXTRA_APPLICATION_ID, getContext().getPackageNam e()); | |
286 intent.putExtra(TabOpenType.BRING_TAB_TO_FRONT.name(), tabId); | |
287 intent.setPackage(mContext.getPackageName()); | |
288 return PendingIntent.getActivity(getContext(), tabId, intent, 0); | |
289 } | |
290 | |
291 private void updateNotificationInternal() { | |
292 RemoteViews contentView = createContentView(); | |
293 | |
294 contentView.setTextViewText(R.id.title, getTitle()); | |
295 contentView.setTextViewText(R.id.status, getStatus()); | |
296 contentView.setImageViewResource(R.id.icon, R.drawable.audio_playing); | |
297 | |
298 MediaInfo mediaInfo = getMediaInfo(); | |
299 if (mediaInfo == null) return; | |
300 | |
301 if (mediaInfo.isPaused) { | |
302 contentView.setImageViewResource(R.id.playpause, R.drawable.ic_vidco ntrol_play); | |
303 contentView.setContentDescription(R.id.playpause, mPlayDescription); | |
304 contentView.setOnClickPendingIntent(R.id.playpause, | |
305 getService().getPendingIntent(ListenerService.ACTION_ID_PLAY )); | |
mlamouri (slow - plz ping)
2015/06/23 14:58:56
We might want to consider using addAction() in ord
whywhat
2015/06/23 19:39:11
Ack.
| |
306 } else { | |
307 contentView.setImageViewResource(R.id.playpause, R.drawable.ic_vidco ntrol_pause); | |
308 contentView.setContentDescription(R.id.playpause, mPauseDescription) ; | |
309 contentView.setOnClickPendingIntent(R.id.playpause, | |
310 getService().getPendingIntent(ListenerService.ACTION_ID_PAUS E)); | |
311 } | |
312 | |
313 mNotification.contentView = contentView; | |
314 | |
315 NotificationManagerCompat manager = NotificationManagerCompat.from(getCo ntext()); | |
316 manager.notify(R.id.media_playback_notification, mNotification); | |
317 | |
318 getService().startForeground(R.id.media_playback_notification, mNotifica tion); | |
319 } | |
320 } | |
OLD | NEW |