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