• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
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         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 
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             mService.unregisterReceiver(this);
164         } catch (IllegalArgumentException ex) {
165             // ignore if the receiver is not registered.
166         }
167         mService.stopForeground(true);
168     }
169 
170     @Override
onReceive(Context context, Intent intent)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      */
updateSessionToken()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 
updateNotificationMetadata()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 
updatePlayPauseAction()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 
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         mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
346     }
347 
fetchBitmapFromURLAsync(final String source)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