1 /* 2 * Copyright (C) 2014 Google Inc. All Rights Reserved. 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.musicservicedemo; 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 import android.util.SparseArray; 41 42 import com.example.android.musicservicedemo.utils.BitmapHelper; 43 import com.example.android.musicservicedemo.utils.LogHelper; 44 45 import java.io.IOException; 46 47 /** 48 * Keeps track of a notification and updates it automatically for a given 49 * MediaSession. Maintaining a visible notification (usually) guarantees that the music service 50 * won't be killed during playback. 51 */ 52 public class MediaNotification extends BroadcastReceiver { 53 private static final String TAG = "MediaNotification"; 54 55 private static final int NOTIFICATION_ID = 412; 56 57 public static final String ACTION_PAUSE = "com.example.android.musicservicedemo.pause"; 58 public static final String ACTION_PLAY = "com.example.android.musicservicedemo.play"; 59 public static final String ACTION_PREV = "com.example.android.musicservicedemo.prev"; 60 public static final String ACTION_NEXT = "com.example.android.musicservicedemo.next"; 61 62 private static final int MAX_ALBUM_ART_CACHE_SIZE = 1024*1024; 63 64 private final MusicService mService; 65 private MediaSession.Token mSessionToken; 66 private MediaController mController; 67 private MediaController.TransportControls mTransportControls; 68 private final SparseArray<PendingIntent> mIntents = new SparseArray<PendingIntent>(); 69 private final LruCache<String, Bitmap> mAlbumArtCache; 70 71 private PlaybackState mPlaybackState; 72 private MediaMetadata mMetadata; 73 74 private Notification.Builder mNotificationBuilder; 75 private NotificationManager mNotificationManager; 76 private Notification.Action mPlayPauseAction; 77 78 private String mCurrentAlbumArt; 79 private int mNotificationColor; 80 81 private boolean mStarted = false; 82 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 mIntents.put(R.drawable.ic_pause_white_24dp, PendingIntent.getBroadcast(mService, 100, 103 new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); 104 mIntents.put(R.drawable.ic_play_arrow_white_24dp, PendingIntent.getBroadcast(mService, 100, 105 new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); 106 mIntents.put(R.drawable.ic_skip_previous_white_24dp, PendingIntent.getBroadcast(mService, 100, 107 new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); 108 mIntents.put(R.drawable.ic_skip_next_white_24dp, PendingIntent.getBroadcast(mService, 100, 109 new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); 110 } 111 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 */ 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 */ 159 public void stopNotification() { 160 mStarted = false; 161 mController.unregisterCallback(mCb); 162 try { 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), 244 mIntents.get(R.drawable.ic_skip_previous_white_24dp)); 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), 254 mIntents.get(R.drawable.ic_skip_next_white_24dp)); 255 } 256 257 MediaDescription description = mMetadata.getDescription(); 258 259 String fetchArtUrl = null; 260 Bitmap art = description.getIconBitmap(); 261 if (art == null && description.getIconUri() != null) { 262 // This sample assumes the iconUri will be a valid URL formatted String, but 263 // it can actually be any valid Android Uri formatted String. 264 // async fetch the album art icon 265 String artUrl = description.getIconUri().toString(); 266 art = mAlbumArtCache.get(artUrl); 267 if (art == null) { 268 fetchArtUrl = artUrl; 269 // use a placeholder art while the remote art is being downloaded 270 art = BitmapFactory.decodeResource(mService.getResources(), R.drawable.ic_default_art); 271 } 272 } 273 274 mNotificationBuilder 275 .setStyle(new Notification.MediaStyle() 276 .setShowActionsInCompactView(playPauseActionIndex) // only show play/pause in compact view 277 .setMediaSession(mSessionToken)) 278 .setColor(mNotificationColor) 279 .setSmallIcon(R.drawable.ic_notification) 280 .setVisibility(Notification.VISIBILITY_PUBLIC) 281 .setOngoing(true) 282 .setUsesChronometer(true) 283 .setContentTitle(description.getTitle()) 284 .setContentText(description.getSubtitle()) 285 .setLargeIcon(art); 286 287 updateNotificationPlaybackState(); 288 289 mService.startForeground(NOTIFICATION_ID, mNotificationBuilder.build()); 290 if (fetchArtUrl != null) { 291 fetchBitmapFromURLAsync(fetchArtUrl); 292 } 293 } 294 295 private void updatePlayPauseAction() { 296 LogHelper.d(TAG, "updatePlayPauseAction"); 297 String playPauseLabel = ""; 298 int playPauseIcon; 299 if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING) { 300 playPauseLabel = mService.getString(R.string.label_pause); 301 playPauseIcon = R.drawable.ic_pause_white_24dp; 302 } else { 303 playPauseLabel = mService.getString(R.string.label_play); 304 playPauseIcon = R.drawable.ic_play_arrow_white_24dp; 305 } 306 if (mPlayPauseAction == null) { 307 mPlayPauseAction = new Notification.Action(playPauseIcon, playPauseLabel, 308 mIntents.get(playPauseIcon)); 309 } else { 310 mPlayPauseAction.icon = playPauseIcon; 311 mPlayPauseAction.title = playPauseLabel; 312 mPlayPauseAction.actionIntent = mIntents.get(playPauseIcon); 313 } 314 } 315 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 mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); 346 } 347 348 public void fetchBitmapFromURLAsync(final String source) { 349 LogHelper.d(TAG, "getBitmapFromURLAsync: starting asynctask to fetch ", source); 350 new AsyncTask<Void, Void, Bitmap>() { 351 @Override 352 protected Bitmap doInBackground(Void[] objects) { 353 Bitmap bitmap = null; 354 try { 355 bitmap = BitmapHelper.fetchAndRescaleBitmap(source, 356 BitmapHelper.MEDIA_ART_BIG_WIDTH, BitmapHelper.MEDIA_ART_BIG_HEIGHT); 357 mAlbumArtCache.put(source, bitmap); 358 } catch (IOException e) { 359 LogHelper.e(TAG, e, "getBitmapFromURLAsync: " + source); 360 } 361 return bitmap; 362 } 363 364 @Override 365 protected void onPostExecute(Bitmap bitmap) { 366 if (bitmap != null && mMetadata != null && 367 mNotificationBuilder != null && mMetadata.getDescription() != null && 368 !source.equals(mMetadata.getDescription().getIconUri())) { 369 // If the media is still the same, update the notification: 370 LogHelper.d(TAG, "getBitmapFromURLAsync: set bitmap to ", source); 371 mNotificationBuilder.setLargeIcon(bitmap); 372 mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); 373 } 374 } 375 }.execute(); 376 } 377 378 } 379