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 MediaNotification extends BroadcastReceiver { 52 private static final String TAG = "MediaNotification"; 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 String mCurrentAlbumArt; 79 private int mNotificationColor; 80 81 private boolean mStarted = false; 82 MediaNotification(MusicService service)83 public MediaNotification(MusicService service) { 84 mService = service; 85 updateSessionToken(); 86 87 // simple album art cache that holds no more than 88 // MAX_ALBUM_ART_CACHE_SIZE bytes: 89 mAlbumArtCache = new LruCache<String, Bitmap>(MAX_ALBUM_ART_CACHE_SIZE) { 90 @Override 91 protected int sizeOf(String key, Bitmap value) { 92 return value.getByteCount(); 93 } 94 }; 95 96 mNotificationColor = getNotificationColor(); 97 98 mNotificationManager = (NotificationManager) mService 99 .getSystemService(Context.NOTIFICATION_SERVICE); 100 101 String pkg = mService.getPackageName(); 102 mPauseIntent = PendingIntent.getBroadcast(mService, 100, 103 new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); 104 mPlayIntent = PendingIntent.getBroadcast(mService, 100, 105 new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); 106 mPreviousIntent = PendingIntent.getBroadcast(mService, 100, 107 new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); 108 mNextIntent = PendingIntent.getBroadcast(mService, 100, 109 new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); 110 } 111 getNotificationColor()112 protected int getNotificationColor() { 113 int notificationColor = 0; 114 String packageName = mService.getPackageName(); 115 try { 116 Context packageContext = mService.createPackageContext(packageName, 0); 117 ApplicationInfo applicationInfo = 118 mService.getPackageManager().getApplicationInfo(packageName, 0); 119 packageContext.setTheme(applicationInfo.theme); 120 Resources.Theme theme = packageContext.getTheme(); 121 TypedArray ta = theme.obtainStyledAttributes( 122 new int[] {android.R.attr.colorPrimary}); 123 notificationColor = ta.getColor(0, Color.DKGRAY); 124 ta.recycle(); 125 } catch (PackageManager.NameNotFoundException e) { 126 e.printStackTrace(); 127 } 128 return notificationColor; 129 } 130 131 /** 132 * Posts the notification and starts tracking the session to keep it 133 * updated. The notification will automatically be removed if the session is 134 * destroyed before {@link #stopNotification} is called. 135 */ startNotification()136 public void startNotification() { 137 if (!mStarted) { 138 mController.registerCallback(mCb); 139 IntentFilter filter = new IntentFilter(); 140 filter.addAction(ACTION_NEXT); 141 filter.addAction(ACTION_PAUSE); 142 filter.addAction(ACTION_PLAY); 143 filter.addAction(ACTION_PREV); 144 mService.registerReceiver(this, filter); 145 146 mMetadata = mController.getMetadata(); 147 mPlaybackState = mController.getPlaybackState(); 148 149 mStarted = true; 150 // The notification must be updated after setting started to true 151 updateNotificationMetadata(); 152 } 153 } 154 155 /** 156 * Removes the notification and stops tracking the session. If the session 157 * was destroyed this has no effect. 158 */ stopNotification()159 public void stopNotification() { 160 mStarted = false; 161 mController.unregisterCallback(mCb); 162 try { 163 mNotificationManager.cancel(NOTIFICATION_ID); 164 mService.unregisterReceiver(this); 165 } catch (IllegalArgumentException ex) { 166 // ignore if the receiver is not registered. 167 } 168 mService.stopForeground(true); 169 } 170 171 @Override onReceive(Context context, Intent intent)172 public void onReceive(Context context, Intent intent) { 173 final String action = intent.getAction(); 174 LogHelper.d(TAG, "Received intent with action " + action); 175 if (ACTION_PAUSE.equals(action)) { 176 mTransportControls.pause(); 177 } else if (ACTION_PLAY.equals(action)) { 178 mTransportControls.play(); 179 } else if (ACTION_NEXT.equals(action)) { 180 mTransportControls.skipToNext(); 181 } else if (ACTION_PREV.equals(action)) { 182 mTransportControls.skipToPrevious(); 183 } 184 } 185 186 /** 187 * Update the state based on a change on the session token. Called either when 188 * we are running for the first time or when the media session owner has destroyed the session 189 * (see {@link android.media.session.MediaController.Callback#onSessionDestroyed()}) 190 */ updateSessionToken()191 private void updateSessionToken() { 192 MediaSession.Token freshToken = mService.getSessionToken(); 193 if (mSessionToken == null || !mSessionToken.equals(freshToken)) { 194 if (mController != null) { 195 mController.unregisterCallback(mCb); 196 } 197 mSessionToken = freshToken; 198 mController = new MediaController(mService, mSessionToken); 199 mTransportControls = mController.getTransportControls(); 200 if (mStarted) { 201 mController.registerCallback(mCb); 202 } 203 } 204 } 205 206 private final MediaController.Callback mCb = new MediaController.Callback() { 207 @Override 208 public void onPlaybackStateChanged(PlaybackState state) { 209 mPlaybackState = state; 210 LogHelper.d(TAG, "Received new playback state", state); 211 updateNotificationPlaybackState(); 212 } 213 214 @Override 215 public void onMetadataChanged(MediaMetadata metadata) { 216 mMetadata = metadata; 217 LogHelper.d(TAG, "Received new metadata ", metadata); 218 updateNotificationMetadata(); 219 } 220 221 @Override 222 public void onSessionDestroyed() { 223 super.onSessionDestroyed(); 224 LogHelper.d(TAG, "Session was destroyed, resetting to the new session token"); 225 updateSessionToken(); 226 } 227 }; 228 updateNotificationMetadata()229 private void updateNotificationMetadata() { 230 LogHelper.d(TAG, "updateNotificationMetadata. mMetadata=" + mMetadata); 231 if (mMetadata == null || mPlaybackState == null) { 232 return; 233 } 234 235 updatePlayPauseAction(); 236 237 mNotificationBuilder = new Notification.Builder(mService); 238 int playPauseActionIndex = 0; 239 240 // If skip to previous action is enabled 241 if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) { 242 mNotificationBuilder 243 .addAction(R.drawable.ic_skip_previous_white_24dp, 244 mService.getString(R.string.label_previous), mPreviousIntent); 245 playPauseActionIndex = 1; 246 } 247 248 mNotificationBuilder.addAction(mPlayPauseAction); 249 250 // If skip to next action is enabled 251 if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) { 252 mNotificationBuilder.addAction(R.drawable.ic_skip_next_white_24dp, 253 mService.getString(R.string.label_next), mNextIntent); 254 } 255 256 MediaDescription description = mMetadata.getDescription(); 257 258 String fetchArtUrl = null; 259 Bitmap art = description.getIconBitmap(); 260 if (art == null && description.getIconUri() != null) { 261 // This sample assumes the iconUri will be a valid URL formatted String, but 262 // it can actually be any valid Android Uri formatted String. 263 // async fetch the album art icon 264 String artUrl = description.getIconUri().toString(); 265 art = mAlbumArtCache.get(artUrl); 266 if (art == null) { 267 fetchArtUrl = artUrl; 268 // use a placeholder art while the remote art is being downloaded 269 art = BitmapFactory.decodeResource(mService.getResources(), R.drawable.ic_default_art); 270 } 271 } 272 273 mNotificationBuilder 274 .setStyle(new Notification.MediaStyle() 275 .setShowActionsInCompactView(playPauseActionIndex) // only show play/pause in compact view 276 .setMediaSession(mSessionToken)) 277 .setColor(mNotificationColor) 278 .setSmallIcon(R.drawable.ic_notification) 279 .setVisibility(Notification.VISIBILITY_PUBLIC) 280 .setUsesChronometer(true) 281 .setContentTitle(description.getTitle()) 282 .setContentText(description.getSubtitle()) 283 .setLargeIcon(art); 284 285 updateNotificationPlaybackState(); 286 287 mService.startForeground(NOTIFICATION_ID, mNotificationBuilder.build()); 288 if (fetchArtUrl != null) { 289 fetchBitmapFromURLAsync(fetchArtUrl); 290 } 291 } 292 updatePlayPauseAction()293 private void updatePlayPauseAction() { 294 LogHelper.d(TAG, "updatePlayPauseAction"); 295 String label; 296 int icon; 297 PendingIntent intent; 298 if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING) { 299 label = mService.getString(R.string.label_pause); 300 icon = R.drawable.ic_pause_white_24dp; 301 intent = mPauseIntent; 302 } else { 303 label = mService.getString(R.string.label_play); 304 icon = R.drawable.ic_play_arrow_white_24dp; 305 intent = mPlayIntent; 306 } 307 if (mPlayPauseAction == null) { 308 mPlayPauseAction = new Notification.Action(icon, label, intent); 309 } else { 310 mPlayPauseAction.icon = icon; 311 mPlayPauseAction.title = label; 312 mPlayPauseAction.actionIntent = intent; 313 } 314 } 315 updateNotificationPlaybackState()316 private void updateNotificationPlaybackState() { 317 LogHelper.d(TAG, "updateNotificationPlaybackState. mPlaybackState=" + mPlaybackState); 318 if (mPlaybackState == null || !mStarted) { 319 LogHelper.d(TAG, "updateNotificationPlaybackState. cancelling notification!"); 320 mService.stopForeground(true); 321 return; 322 } 323 if (mNotificationBuilder == null) { 324 LogHelper.d(TAG, "updateNotificationPlaybackState. there is no notificationBuilder. Ignoring request to update state!"); 325 return; 326 } 327 if (mPlaybackState.getPosition() >= 0) { 328 LogHelper.d(TAG, "updateNotificationPlaybackState. updating playback position to ", 329 (System.currentTimeMillis() - mPlaybackState.getPosition()) / 1000, " seconds"); 330 mNotificationBuilder 331 .setWhen(System.currentTimeMillis() - mPlaybackState.getPosition()) 332 .setShowWhen(true) 333 .setUsesChronometer(true); 334 mNotificationBuilder.setShowWhen(true); 335 } else { 336 LogHelper.d(TAG, "updateNotificationPlaybackState. hiding playback position"); 337 mNotificationBuilder 338 .setWhen(0) 339 .setShowWhen(false) 340 .setUsesChronometer(false); 341 } 342 343 updatePlayPauseAction(); 344 345 // Make sure that the notification can be dismissed by the user when we are not playing: 346 mNotificationBuilder.setOngoing(mPlaybackState.getState() == PlaybackState.STATE_PLAYING); 347 348 mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); 349 } 350 fetchBitmapFromURLAsync(final String source)351 public void fetchBitmapFromURLAsync(final String source) { 352 LogHelper.d(TAG, "getBitmapFromURLAsync: starting asynctask to fetch ", source); 353 new AsyncTask<Void, Void, Bitmap>() { 354 @Override 355 protected Bitmap doInBackground(Void[] objects) { 356 Bitmap bitmap = null; 357 try { 358 bitmap = BitmapHelper.fetchAndRescaleBitmap(source, 359 BitmapHelper.MEDIA_ART_BIG_WIDTH, BitmapHelper.MEDIA_ART_BIG_HEIGHT); 360 mAlbumArtCache.put(source, bitmap); 361 } catch (IOException e) { 362 LogHelper.e(TAG, e, "getBitmapFromURLAsync: " + source); 363 } 364 return bitmap; 365 } 366 367 @Override 368 protected void onPostExecute(Bitmap bitmap) { 369 if (bitmap != null && mMetadata != null && 370 mNotificationBuilder != null && mMetadata.getDescription() != null && 371 !source.equals(mMetadata.getDescription().getIconUri())) { 372 // If the media is still the same, update the notification: 373 LogHelper.d(TAG, "getBitmapFromURLAsync: set bitmap to ", source); 374 mNotificationBuilder.setLargeIcon(bitmap); 375 mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); 376 } 377 } 378 }.execute(); 379 } 380 381 } 382