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.NotificationManager; |
| 9 import android.app.PendingIntent; |
| 10 import android.app.Service; |
| 11 import android.content.Context; |
| 12 import android.content.Intent; |
| 13 import android.os.Handler; |
| 14 import android.os.IBinder; |
| 15 import android.provider.Browser; |
| 16 import android.support.v4.app.NotificationCompat; |
| 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 * |
| 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(int oldState, int newState) { |
| 178 MediaInfo mediaInfo = new MediaInfo(getMediaInfo()); |
| 179 mediaInfo.state = newState; |
| 180 setMediaInfo(mediaInfo); |
| 181 |
| 182 if (newState == oldState) return; |
| 183 |
| 184 show(mediaInfo); |
| 185 } |
| 186 |
| 187 @Override |
| 188 public void onTitleChanged(String title) { |
| 189 MediaInfo mediaInfo = new MediaInfo(getMediaInfo()); |
| 190 mediaInfo.title = title; |
| 191 setMediaInfo(mediaInfo); |
| 192 } |
| 193 |
| 194 @Override |
| 195 public void onBroadDomainChanged(String broadDomain) { |
| 196 MediaInfo mediaInfo = new MediaInfo(getMediaInfo()); |
| 197 mediaInfo.broadDomain = broadDomain; |
| 198 setMediaInfo(mediaInfo); |
| 199 } |
| 200 |
| 201 @Override |
| 202 public void onTabIdChanged(int tabId) { |
| 203 MediaInfo mediaInfo = new MediaInfo(getMediaInfo()); |
| 204 mediaInfo.tabId = tabId; |
| 205 setMediaInfo(mediaInfo); |
| 206 } |
| 207 |
| 208 @Override |
| 209 public void show(MediaInfo mediaInfo) { |
| 210 setMediaInfo(mediaInfo); |
| 211 mContext.startService(new Intent(mContext, ListenerService.class)); |
| 212 } |
| 213 |
| 214 private void updateNotification() { |
| 215 // Defer the call to updateNotificationInternal() so it can be cancelled
in |
| 216 // destroyNotification(). This is done to avoid the OS bug b/8798662. |
| 217 mHandler.sendEmptyMessage(MSG_UPDATE_NOTIFICATION); |
| 218 } |
| 219 |
| 220 Notification getNotification() { |
| 221 return mNotification; |
| 222 } |
| 223 |
| 224 final ListenerService getService() { |
| 225 return mService; |
| 226 } |
| 227 |
| 228 @Override |
| 229 protected void onMediaInfoChanged() { |
| 230 if (isShowing()) updateNotification(); |
| 231 } |
| 232 |
| 233 private RemoteViews createContentView() { |
| 234 RemoteViews contentView = |
| 235 new RemoteViews(getContext().getPackageName(), R.layout.playback
_notification_bar); |
| 236 return contentView; |
| 237 } |
| 238 |
| 239 private void createNotification() { |
| 240 NotificationCompat.Builder notificationBuilder = |
| 241 new NotificationCompat.Builder(getContext()) |
| 242 .setSmallIcon(R.drawable.audio_playing) |
| 243 .setAutoCancel(false) |
| 244 .setLocalOnly(true) |
| 245 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) |
| 246 .setOngoing(true) |
| 247 .setContent(createContentView()) |
| 248 .setContentIntent(createContentIntent()); |
| 249 mNotification = notificationBuilder.build(); |
| 250 updateNotification(); |
| 251 } |
| 252 |
| 253 private void destroyNotification() { |
| 254 // Cancel any pending updates - we're about to tear down the notificatio
n. |
| 255 mHandler.removeMessages(MSG_UPDATE_NOTIFICATION); |
| 256 |
| 257 NotificationManager manager = |
| 258 (NotificationManager) getContext().getSystemService(Context.NOTI
FICATION_SERVICE); |
| 259 manager.cancel(R.id.media_playback_notification); |
| 260 mNotification = null; |
| 261 } |
| 262 |
| 263 private final Context getContext() { |
| 264 return mContext; |
| 265 } |
| 266 |
| 267 private String getStatus() { |
| 268 Context context = getContext(); |
| 269 MediaInfo mediaInfo = getMediaInfo(); |
| 270 return context.getString(R.string.media_notification_link_text, |
| 271 mediaInfo.broadDomain != null ? mediaInfo.broadDomain : ""); |
| 272 } |
| 273 |
| 274 private String getTitle() { |
| 275 Context context = getContext(); |
| 276 MediaInfo mediaInfo = getMediaInfo(); |
| 277 String mediaTitle = mediaInfo.title; |
| 278 switch (mediaInfo.state) { |
| 279 case MediaInfo.PLAYING: |
| 280 return context.getString( |
| 281 R.string.media_playback_notification_playing_for_media,
mediaTitle); |
| 282 case MediaInfo.PAUSED: |
| 283 return context.getString( |
| 284 R.string.media_playback_notification_paused_for_media, m
ediaTitle); |
| 285 default: |
| 286 return mediaTitle; |
| 287 } |
| 288 } |
| 289 |
| 290 private PendingIntent createContentIntent() { |
| 291 int tabId = getMediaInfo().tabId; |
| 292 Intent intent = new Intent(Intent.ACTION_MAIN); |
| 293 intent.putExtra(Browser.EXTRA_APPLICATION_ID, getContext().getPackageNam
e()); |
| 294 intent.putExtra(TabOpenType.BRING_TAB_TO_FRONT.name(), tabId); |
| 295 intent.setPackage(mContext.getPackageName()); |
| 296 return PendingIntent.getActivity(getContext(), tabId, intent, 0); |
| 297 } |
| 298 |
| 299 private void updateNotificationInternal() { |
| 300 RemoteViews contentView = createContentView(); |
| 301 |
| 302 contentView.setTextViewText(R.id.title, getTitle()); |
| 303 contentView.setTextViewText(R.id.status, getStatus()); |
| 304 contentView.setImageViewResource(R.id.icon, R.drawable.audio_playing); |
| 305 |
| 306 MediaInfo mediaInfo = getMediaInfo(); |
| 307 if (mediaInfo == null) return; |
| 308 |
| 309 switch (mediaInfo.state) { |
| 310 case MediaInfo.PLAYING: |
| 311 contentView.setImageViewResource(R.id.playpause, R.drawable.ic_v
idcontrol_pause); |
| 312 contentView.setContentDescription(R.id.playpause, mPauseDescript
ion); |
| 313 contentView.setOnClickPendingIntent(R.id.playpause, |
| 314 getService().getPendingIntent(ListenerService.ACTION_ID_
PAUSE)); |
| 315 break; |
| 316 case MediaInfo.PAUSED: |
| 317 contentView.setImageViewResource(R.id.playpause, R.drawable.ic_v
idcontrol_play); |
| 318 contentView.setContentDescription(R.id.playpause, mPlayDescripti
on); |
| 319 contentView.setOnClickPendingIntent(R.id.playpause, |
| 320 getService().getPendingIntent(ListenerService.ACTION_ID_
PLAY)); |
| 321 break; |
| 322 default: |
| 323 break; |
| 324 } |
| 325 |
| 326 mNotification.contentView = contentView; |
| 327 |
| 328 NotificationManager manager = (NotificationManager) getContext().getSyst
emService( |
| 329 Context.NOTIFICATION_SERVICE); |
| 330 manager.notify(R.id.media_playback_notification, mNotification); |
| 331 |
| 332 getService().startForeground(R.id.media_playback_notification, mNotifica
tion); |
| 333 } |
| 334 } |
OLD | NEW |