MediaNotificationManager.java revision b31c3281d870e9abb673db239234d580dcc4feff
1/* 2 * Copyright (C) 2015 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 17package com.example.android.supportv4.media; 18 19import android.app.Notification; 20import android.app.PendingIntent; 21import android.content.BroadcastReceiver; 22import android.content.Context; 23import android.content.Intent; 24import android.content.IntentFilter; 25import android.graphics.Bitmap; 26import android.graphics.BitmapFactory; 27import android.graphics.Color; 28import android.os.RemoteException; 29import androidx.core.app.NotificationCompat; 30import androidx.core.app.NotificationManagerCompat; 31import android.support.v4.media.MediaDescriptionCompat; 32import android.support.v4.media.MediaMetadataCompat; 33import androidx.media.session.MediaControllerCompat; 34import android.support.v4.media.session.MediaSessionCompat; 35import android.support.v4.media.session.PlaybackStateCompat; 36import android.util.Log; 37 38import com.example.android.supportv4.R; 39import com.example.android.supportv4.media.utils.ResourceHelper; 40 41/** 42 * Keeps track of a notification and updates it automatically for a given 43 * MediaSession. Maintaining a visible notification (usually) guarantees that the music service 44 * won't be killed during playback. 45 */ 46public class MediaNotificationManager extends BroadcastReceiver { 47 private static final String TAG = "MediaNotiManager"; 48 49 private static final int NOTIFICATION_ID = 412; 50 private static final int REQUEST_CODE = 100; 51 52 public static final String ACTION_PAUSE = "com.example.android.supportv4.media.pause"; 53 public static final String ACTION_PLAY = "com.example.android.supportv4.media.play"; 54 public static final String ACTION_PREV = "com.example.android.supportv4.media.prev"; 55 public static final String ACTION_NEXT = "com.example.android.supportv4.media.next"; 56 57 private final MediaBrowserServiceSupport mService; 58 private MediaSessionCompat.Token mSessionToken; 59 private MediaControllerCompat mController; 60 private MediaControllerCompat.TransportControls mTransportControls; 61 62 private PlaybackStateCompat mPlaybackState; 63 private MediaMetadataCompat mMetadata; 64 65 private NotificationManagerCompat mNotificationManager; 66 67 private PendingIntent mPauseIntent; 68 private PendingIntent mPlayIntent; 69 private PendingIntent mPreviousIntent; 70 private PendingIntent mNextIntent; 71 72 private int mNotificationColor; 73 74 private boolean mStarted = false; 75 76 public MediaNotificationManager(MediaBrowserServiceSupport service) { 77 mService = service; 78 updateSessionToken(); 79 80 mNotificationColor = ResourceHelper.getThemeColor(mService, 81 android.R.attr.colorPrimary, Color.DKGRAY); 82 83 mNotificationManager = NotificationManagerCompat.from(mService); 84 85 String pkg = mService.getPackageName(); 86 mPauseIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE, 87 new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); 88 mPlayIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE, 89 new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); 90 mPreviousIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE, 91 new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); 92 mNextIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE, 93 new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); 94 95 // Cancel all notifications to handle the case where the Service was killed and 96 // restarted by the system. 97 mNotificationManager.cancelAll(); 98 } 99 100 /** 101 * Posts the notification and starts tracking the session to keep it 102 * updated. The notification will automatically be removed if the session is 103 * destroyed before {@link #stopNotification} is called. 104 */ 105 public void startNotification() { 106 if (!mStarted) { 107 mMetadata = mController.getMetadata(); 108 mPlaybackState = mController.getPlaybackState(); 109 110 // The notification must be updated after setting started to true 111 Notification notification = createNotification(); 112 if (notification != null) { 113 mController.registerCallback(mCb); 114 IntentFilter filter = new IntentFilter(); 115 filter.addAction(ACTION_NEXT); 116 filter.addAction(ACTION_PAUSE); 117 filter.addAction(ACTION_PLAY); 118 filter.addAction(ACTION_PREV); 119 mService.registerReceiver(this, filter); 120 121 mService.startForeground(NOTIFICATION_ID, notification); 122 mStarted = true; 123 } 124 } 125 } 126 127 /** 128 * Removes the notification and stops tracking the session. If the session 129 * was destroyed this has no effect. 130 */ 131 public void stopNotification() { 132 if (mStarted) { 133 mStarted = false; 134 mController.unregisterCallback(mCb); 135 try { 136 mNotificationManager.cancel(NOTIFICATION_ID); 137 mService.unregisterReceiver(this); 138 } catch (IllegalArgumentException ex) { 139 // ignore if the receiver is not registered. 140 } 141 mService.stopForeground(true); 142 } 143 } 144 145 @Override 146 public void onReceive(Context context, Intent intent) { 147 final String action = intent.getAction(); 148 Log.d(TAG, "Received intent with action " + action); 149 switch (action) { 150 case ACTION_PAUSE: 151 mTransportControls.pause(); 152 break; 153 case ACTION_PLAY: 154 mTransportControls.play(); 155 break; 156 case ACTION_NEXT: 157 mTransportControls.skipToNext(); 158 break; 159 case ACTION_PREV: 160 mTransportControls.skipToPrevious(); 161 break; 162 default: 163 Log.w(TAG, "Unknown intent ignored. Action=" + action); 164 } 165 } 166 167 /** 168 * Update the state based on a change on the session token. Called either when 169 * we are running for the first time or when the media session owner has destroyed the session 170 * (see {@link android.media.session.MediaController.Callback#onSessionDestroyed()}) 171 */ 172 private void updateSessionToken() { 173 MediaSessionCompat.Token freshToken = mService.getSessionToken(); 174 if (mSessionToken == null || !mSessionToken.equals(freshToken)) { 175 if (mController != null) { 176 mController.unregisterCallback(mCb); 177 } 178 mSessionToken = freshToken; 179 try { 180 mController = new MediaControllerCompat(mService, mSessionToken); 181 } catch (RemoteException e) { 182 Log.e(TAG, "Failed to create MediaControllerCompat.", e); 183 } 184 mTransportControls = mController.getTransportControls(); 185 if (mStarted) { 186 mController.registerCallback(mCb); 187 } 188 } 189 } 190 191 private PendingIntent createContentIntent() { 192 Intent openUI = new Intent(mService, MediaBrowserSupport.class); 193 openUI.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); 194 return PendingIntent.getActivity(mService, REQUEST_CODE, openUI, 195 PendingIntent.FLAG_CANCEL_CURRENT); 196 } 197 198 private final MediaControllerCompat.Callback mCb = new MediaControllerCompat.Callback() { 199 @Override 200 public void onPlaybackStateChanged(PlaybackStateCompat state) { 201 mPlaybackState = state; 202 Log.d(TAG, "Received new playback state " + state); 203 if (state != null && (state.getState() == PlaybackStateCompat.STATE_STOPPED || 204 state.getState() == PlaybackStateCompat.STATE_NONE)) { 205 stopNotification(); 206 } else { 207 Notification notification = createNotification(); 208 if (notification != null) { 209 mNotificationManager.notify(NOTIFICATION_ID, notification); 210 } 211 } 212 } 213 214 @Override 215 public void onMetadataChanged(MediaMetadataCompat metadata) { 216 mMetadata = metadata; 217 Log.d(TAG, "Received new metadata " + metadata); 218 Notification notification = createNotification(); 219 if (notification != null) { 220 mNotificationManager.notify(NOTIFICATION_ID, notification); 221 } 222 } 223 224 @Override 225 public void onSessionDestroyed() { 226 super.onSessionDestroyed(); 227 Log.d(TAG, "Session was destroyed, resetting to the new session token"); 228 updateSessionToken(); 229 } 230 }; 231 232 private Notification createNotification() { 233 Log.d(TAG, "updateNotificationMetadata. mMetadata=" + mMetadata); 234 if (mMetadata == null || mPlaybackState == null) { 235 return null; 236 } 237 238 NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(mService); 239 240 // If skip to previous action is enabled 241 if ((mPlaybackState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0) { 242 notificationBuilder.addAction(R.drawable.ic_skip_previous_white_24dp, 243 mService.getString(R.string.label_previous), mPreviousIntent); 244 } 245 246 addPlayPauseAction(notificationBuilder); 247 248 // If skip to next action is enabled 249 if ((mPlaybackState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) { 250 notificationBuilder.addAction(R.drawable.ic_skip_next_white_24dp, 251 mService.getString(R.string.label_next), mNextIntent); 252 } 253 254 MediaDescriptionCompat description = mMetadata.getDescription(); 255 256 String fetchArtUrl = null; 257 Bitmap art = null; 258 if (description.getIconUri() != null) { 259 // This sample assumes the iconUri will be a valid URL formatted String, but 260 // it can actually be any valid Android Uri formatted String. 261 // async fetch the album art icon 262 String artUrl = description.getIconUri().toString(); 263 art = AlbumArtCache.getInstance().getBigImage(artUrl); 264 if (art == null) { 265 fetchArtUrl = artUrl; 266 // use a placeholder art while the remote art is being downloaded 267 art = BitmapFactory.decodeResource(mService.getResources(), 268 R.drawable.ic_default_art); 269 } 270 } 271 272 notificationBuilder 273 .setColor(mNotificationColor) 274 .setSmallIcon(R.drawable.ic_notification) 275 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) 276 .setUsesChronometer(true) 277 .setContentIntent(createContentIntent()) 278 .setContentTitle(description.getTitle()) 279 .setContentText(description.getSubtitle()) 280 .setLargeIcon(art); 281 282 setNotificationPlaybackState(notificationBuilder); 283 if (fetchArtUrl != null) { 284 fetchBitmapFromURLAsync(fetchArtUrl, notificationBuilder); 285 } 286 287 return notificationBuilder.build(); 288 } 289 290 private void addPlayPauseAction(NotificationCompat.Builder builder) { 291 Log.d(TAG, "updatePlayPauseAction"); 292 String label; 293 int icon; 294 PendingIntent intent; 295 if (mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING) { 296 label = mService.getString(R.string.label_pause); 297 icon = R.drawable.ic_pause_white_24dp; 298 intent = mPauseIntent; 299 } else { 300 label = mService.getString(R.string.label_play); 301 icon = R.drawable.ic_play_arrow_white_24dp; 302 intent = mPlayIntent; 303 } 304 builder.addAction(new NotificationCompat.Action(icon, label, intent)); 305 } 306 307 private void setNotificationPlaybackState(NotificationCompat.Builder builder) { 308 Log.d(TAG, "updateNotificationPlaybackState. mPlaybackState=" + mPlaybackState); 309 if (mPlaybackState == null || !mStarted) { 310 Log.d(TAG, "updateNotificationPlaybackState. cancelling notification!"); 311 mService.stopForeground(true); 312 return; 313 } 314 if (mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING 315 && mPlaybackState.getPosition() >= 0) { 316 Log.d(TAG, "updateNotificationPlaybackState. updating playback position to " 317 + (System.currentTimeMillis() - mPlaybackState.getPosition()) / 1000 318 + " seconds"); 319 builder 320 .setWhen(System.currentTimeMillis() - mPlaybackState.getPosition()) 321 .setShowWhen(true) 322 .setUsesChronometer(true); 323 } else { 324 Log.d(TAG, "updateNotificationPlaybackState. hiding playback position"); 325 builder 326 .setWhen(0) 327 .setShowWhen(false) 328 .setUsesChronometer(false); 329 } 330 331 // Make sure that the notification can be dismissed by the user when we are not playing: 332 builder.setOngoing(mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING); 333 } 334 335 private void fetchBitmapFromURLAsync(final String bitmapUrl, 336 final NotificationCompat.Builder builder) { 337 AlbumArtCache.getInstance().fetch(bitmapUrl, new AlbumArtCache.FetchListener() { 338 @Override 339 public void onFetched(String artUrl, Bitmap bitmap, Bitmap icon) { 340 if (mMetadata != null && mMetadata.getDescription() != null && 341 artUrl.equals(mMetadata.getDescription().getIconUri().toString())) { 342 // If the media is still the same, update the notification: 343 Log.d(TAG, "fetchBitmapFromURLAsync: set bitmap to " + artUrl); 344 builder.setLargeIcon(bitmap); 345 mNotificationManager.notify(NOTIFICATION_ID, builder.build()); 346 } 347 } 348 }); 349 } 350} 351