Chromium Code Reviews| 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 * 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 } | |
| OLD | NEW |