1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.mediabrowserservice; 18 19 import android.app.Notification; 20 import android.app.NotificationManager; 21 import android.app.PendingIntent; 22 import android.content.BroadcastReceiver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentFilter; 26 import android.content.pm.ApplicationInfo; 27 import android.content.pm.PackageManager; 28 import android.content.res.Resources; 29 import android.content.res.TypedArray; 30 import android.graphics.Bitmap; 31 import android.graphics.BitmapFactory; 32 import android.graphics.Color; 33 import android.media.MediaDescription; 34 import android.media.MediaMetadata; 35 import android.media.session.MediaController; 36 import android.media.session.MediaSession; 37 import android.media.session.PlaybackState; 38 import android.os.AsyncTask; 39 import android.util.LruCache; 40 41 import com.example.android.mediabrowserservice.utils.BitmapHelper; 42 import com.example.android.mediabrowserservice.utils.LogHelper; 43 44 import java.io.IOException; 45 46 /** 47 * Keeps track of a notification and updates it automatically for a given 48 * MediaSession. Maintaining a visible notification (usually) guarantees that the music service 49 * won't be killed during playback. 50 */ 51 public class MediaNotificationManager extends BroadcastReceiver { 52 private static final String TAG = LogHelper.makeLogTag(MediaNotificationManager.class.getSimpleName()); 53 54 private static final int NOTIFICATION_ID = 412; 55 56 public static final String ACTION_PAUSE = "com.example.android.mediabrowserservice.pause"; 57 public static final String ACTION_PLAY = "com.example.android.mediabrowserservice.play"; 58 public static final String ACTION_PREV = "com.example.android.mediabrowserservice.prev"; 59 public static final String ACTION_NEXT = "com.example.android.mediabrowserservice.next"; 60 61 private static final int MAX_ALBUM_ART_CACHE_SIZE = 1024*1024; 62 63 private final MusicService mService; 64 private MediaSession.Token mSessionToken; 65 private MediaController mController; 66 private MediaController.TransportControls mTransportControls; 67 private final LruCache<String, Bitmap> mAlbumArtCache; 68 69 private PlaybackState mPlaybackState; 70 private MediaMetadata mMetadata; 71 72 private Notification.Builder mNotificationBuilder; 73 private NotificationManager mNotificationManager; 74 private Notification.Action mPlayPauseAction; 75 76 private PendingIntent mPauseIntent, mPlayIntent, mPreviousIntent, mNextIntent; 77 78 private int mNotificationColor; 79 80 private boolean mStarted = false; 81 82 public MediaNotificationManager(MusicService service) { 83 mService = service; 84 updateSessionToken(); 85 86 // simple album art cache that holds no more than 87 // MAX_ALBUM_ART_CACHE_SIZE bytes: 88 mAlbumArtCache = new LruCache<String, Bitmap>(MAX_ALBUM_ART_CACHE_SIZE) { 89 @Override 90 protected int sizeOf(String key, Bitmap value) { 91 return value.getByteCount(); 92 } 93 }; 94 95 mNotificationColor = getNotificationColor(); 96 97 mNotificationManager = (NotificationManager) mService 98 .getSystemService(Context.NOTIFICATION_SERVICE); 99 100 String pkg = mService.getPackageName(); 101 mPauseIntent = PendingIntent.getBroadcast(mService, 100, 102 new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); 103 mPlayIntent = PendingIntent.getBroadcast(mService, 100, 104 new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); 105 mPreviousIntent = PendingIntent.getBroadcast(mService, 100, 106 new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); 107 mNextIntent = PendingIntent.getBroadcast(mService, 100, 108 new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); 109 } 110 111 protected int getNotificationColor() { 112 int notificationColor = 0; 113 String packageName = mService.getPackageName(); 114 try { 115 Context packageContext = mService.createPackageContext(packageName, 0); 116 ApplicationInfo applicationInfo = 117 mService.getPackageManager().getApplicationInfo(packageName, 0); 118 packageContext.setTheme(applicationInfo.theme); 119 Resources.Theme theme = packageContext.getTheme(); 120 TypedArray ta = theme.obtainStyledAttributes( 121 new int[] {android.R.attr.colorPrimary}); 122 notificationColor = ta.getColor(0, Color.DKGRAY); 123 ta.recycle(); 124 } catch (PackageManager.NameNotFoundException e) { 125 e.printStackTrace(); 126 } 127 return notificationColor; 128 } 129 130 /** 131 * Posts the notification and starts tracking the session to keep it 132 * updated. The notification will automatically be removed if the session is 133 * destroyed before {@link #stopNotification} is called. 134 */ 135 public void startNotification() { 136 if (!mStarted) { 137 mController.registerCallback(mCb); 138 IntentFilter filter = new IntentFilter(); 139 filter.addAction(ACTION_NEXT); 140 filter.addAction(ACTION_PAUSE); 141 filter.addAction(ACTION_PLAY); 142 filter.addAction(ACTION_PREV); 143 mService.registerReceiver(this, filter); 144 145 mMetadata = mController.getMetadata(); 146 mPlaybackState = mController.getPlaybackState(); 147 148 mStarted = true; 149 // The notification must be updated after setting started to true 150 updateNotificationMetadata(); 151 } 152 } 153 154 /** 155 * Removes the notification and stops tracking the session. If the session 156 * was destroyed this has no effect. 157 */ 158 public void stopNotification() { 159 mStarted = false; 160 mController.unregisterCallback(mCb); 161 try { 162 mNotificationManager.cancel(NOTIFICATION_ID); 163 mService.unregisterReceiver(this); 164 } catch (IllegalArgumentException ex) { 165 // ignore if the receiver is not registered. 166 } 167 mService.stopForeground(true); 168 } 169 170 @Override 171 public void onReceive(Context context, Intent intent) { 172 final String action = intent.getAction(); 173 LogHelper.d(TAG, "Received intent with action " + action); 174 if (ACTION_PAUSE.equals(action)) { 175 mTransportControls.pause(); 176 } else if (ACTION_PLAY.equals(action)) { 177 mTransportControls.play(); 178 } else if (ACTION_NEXT.equals(action)) { 179 mTransportControls.skipToNext(); 180 } else if (ACTION_PREV.equals(action)) { 181 mTransportControls.skipToPrevious(); 182 } 183 } 184 185 /** 186 * Update the state based on a change on the session token. Called either when 187 * we are running for the first time or when the media session owner has destroyed the session 188 * (see {@link android.media.session.MediaController.Callback#onSessionDestroyed()}) 189 */ 190 private void updateSessionToken() { 191 MediaSession.Token freshToken = mService.getSessionToken(); 192 if (mSessionToken == null || !mSessionToken.equals(freshToken)) { 193 if (mController != null) { 194 mController.unregisterCallback(mCb); 195 } 196 mSessionToken = freshToken; 197 mController = new MediaController(mService, mSessionToken); 198 mTransportControls = mController.getTransportControls(); 199 if (mStarted) { 200 mController.registerCallback(mCb); 201 } 202 } 203 } 204 205 private final MediaController.Callback mCb = new MediaController.Callback() { 206 @Override 207 public void onPlaybackStateChanged(PlaybackState state) { 208 mPlaybackState = state; 209 LogHelper.d(TAG, "Received new playback state", state); 210 updateNotificationPlaybackState(); 211 } 212 213 @Override 214 public void onMetadataChanged(MediaMetadata metadata) { 215 mMetadata = metadata; 216 LogHelper.d(TAG, "Received new metadata ", metadata); 217 updateNotificationMetadata(); 218 } 219 220 @Override 221 public void onSessionDestroyed() { 222 super.onSessionDestroyed(); 223 LogHelper.d(TAG, "Session was destroyed, resetting to the new session token"); 224 updateSessionToken(); 225 } 226 }; 227 228 private void updateNotificationMetadata() { 229 LogHelper.d(TAG, "updateNotificationMetadata. mMetadata=" + mMetadata); 230 if (mMetadata == null || mPlaybackState == null) { 231 return; 232 } 233 234 updatePlayPauseAction(); 235 236 mNotificationBuilder = new Notification.Builder(mService); 237 int playPauseActionIndex = 0; 238 239 // If skip to previous action is enabled 240 if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) { 241 mNotificationBuilder 242 .addAction(R.drawable.ic_skip_previous_white_24dp, 243 mService.getString(R.string.label_previous), mPreviousIntent); 244 playPauseActionIndex = 1; 245 } 246 247 mNotificationBuilder.addAction(mPlayPauseAction); 248 249 // If skip to next action is enabled 250 if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) { 251 mNotificationBuilder.addAction(R.drawable.ic_skip_next_white_24dp, 252 mService.getString(R.string.label_next), mNextIntent); 253 } 254 255 MediaDescription description = mMetadata.getDescription(); 256 257 String fetchArtUrl = null; 258 Bitmap art = description.getIconBitmap(); 259 if (art == null && description.getIconUri() != null) { 260 // This sample assumes the iconUri will be a valid URL formatted String, but 261 // it can actually be any valid Android Uri formatted String. 262 // async fetch the album art icon 263 String artUrl = description.getIconUri().toString(); 264 art = mAlbumArtCache.get(artUrl); 265 if (art == null) { 266 fetchArtUrl = artUrl; 267 // use a placeholder art while the remote art is being downloaded 268 art = BitmapFactory.decodeResource(mService.getResources(), R.drawable.ic_default_art); 269 } 270 } 271 272 mNotificationBuilder 273 .setStyle(new Notification.MediaStyle() 274 .setShowActionsInCompactView(playPauseActionIndex) // only show play/pause in compact view 275 .setMediaSession(mSessionToken)) 276 .setColor(mNotificationColor) 277 .setSmallIcon(R.drawable.ic_notification) 278 .setVisibility(Notification.VISIBILITY_PUBLIC) 279 .setUsesChronometer(true) 280 .setContentTitle(description.getTitle()) 281 .setContentText(description.getSubtitle()) 282 .setLargeIcon(art); 283 284 updateNotificationPlaybackState(); 285 286 mService.startForeground(NOTIFICATION_ID, mNotificationBuilder.build()); 287 if (fetchArtUrl != null) { 288 fetchBitmapFromURLAsync(fetchArtUrl); 289 } 290 } 291 292 private void updatePlayPauseAction() { 293 LogHelper.d(TAG, "updatePlayPauseAction"); 294 String label; 295 int icon; 296 PendingIntent intent; 297 if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING) { 298 label = mService.getString(R.string.label_pause); 299 icon = R.drawable.ic_pause_white_24dp; 300 intent = mPauseIntent; 301 } else { 302 label = mService.getString(R.string.label_play); 303 icon = R.drawable.ic_play_arrow_white_24dp; 304 intent = mPlayIntent; 305 } 306 if (mPlayPauseAction == null) { 307 mPlayPauseAction = new Notification.Action(icon, label, intent); 308 } else { 309 mPlayPauseAction.icon = icon; 310 mPlayPauseAction.title = label; 311 mPlayPauseAction.actionIntent = intent; 312 } 313 } 314 315 private void updateNotificationPlaybackState() { 316 LogHelper.d(TAG, "updateNotificationPlaybackState. mPlaybackState=" + mPlaybackState); 317 if (mPlaybackState == null || !mStarted) { 318 LogHelper.d(TAG, "updateNotificationPlaybackState. cancelling notification!"); 319 mService.stopForeground(true); 320 return; 321 } 322 if (mNotificationBuilder == null) { 323 LogHelper.d(TAG, "updateNotificationPlaybackState. there is no notificationBuilder. Ignoring request to update state!"); 324 return; 325 } 326 if (mPlaybackState.getPosition() >= 0) { 327 LogHelper.d(TAG, "updateNotificationPlaybackState. updating playback position to ", 328 (System.currentTimeMillis() - mPlaybackState.getPosition()) / 1000, " seconds"); 329 mNotificationBuilder 330 .setWhen(System.currentTimeMillis() - mPlaybackState.getPosition()) 331 .setShowWhen(true) 332 .setUsesChronometer(true); 333 mNotificationBuilder.setShowWhen(true); 334 } else { 335 LogHelper.d(TAG, "updateNotificationPlaybackState. hiding playback position"); 336 mNotificationBuilder 337 .setWhen(0) 338 .setShowWhen(false) 339 .setUsesChronometer(false); 340 } 341 342 updatePlayPauseAction(); 343 344 // Make sure that the notification can be dismissed by the user when we are not playing: 345 mNotificationBuilder.setOngoing(mPlaybackState.getState() == PlaybackState.STATE_PLAYING); 346 347 mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); 348 } 349 350 public void fetchBitmapFromURLAsync(final String source) { 351 LogHelper.d(TAG, "getBitmapFromURLAsync: starting asynctask to fetch ", source); 352 new AsyncTask<Void, Void, Bitmap>() { 353 @Override 354 protected Bitmap doInBackground(Void[] objects) { 355 Bitmap bitmap = null; 356 try { 357 bitmap = BitmapHelper.fetchAndRescaleBitmap(source, 358 BitmapHelper.MEDIA_ART_BIG_WIDTH, BitmapHelper.MEDIA_ART_BIG_HEIGHT); 359 mAlbumArtCache.put(source, bitmap); 360 } catch (IOException e) { 361 LogHelper.e(TAG, e, "getBitmapFromURLAsync: " + source); 362 } 363 return bitmap; 364 } 365 366 @Override 367 protected void onPostExecute(Bitmap bitmap) { 368 if (bitmap != null && mMetadata != null && 369 mNotificationBuilder != null && mMetadata.getDescription() != null && 370 !source.equals(mMetadata.getDescription().getIconUri())) { 371 // If the media is still the same, update the notification: 372 LogHelper.d(TAG, "getBitmapFromURLAsync: set bitmap to ", source); 373 mNotificationBuilder.setLargeIcon(bitmap); 374 mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); 375 } 376 } 377 }.execute(); 378 } 379 380 } 381