blob: 65b9797ba5a8fbecfc0248099e0b73bbe99e14e3 [file] [log] [blame]
Jack Hef02d3c62017-02-21 00:39:22 -05001/*
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
17package com.android.music.utils;
18
19import android.app.Notification;
20import android.app.NotificationManager;
21import android.app.PendingIntent;
22import android.content.BroadcastReceiver;
23import android.content.Context;
24import android.content.Intent;
25import android.content.IntentFilter;
26import android.graphics.Bitmap;
27import android.graphics.BitmapFactory;
28import android.graphics.Color;
29import android.media.MediaDescription;
30import android.media.MediaMetadata;
31import android.media.session.MediaController;
32import android.media.session.MediaSession;
33import android.media.session.PlaybackState;
34import android.service.media.MediaBrowserService;
35import android.util.Log;
36import com.android.music.MediaPlaybackService;
37import 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 */
44public 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
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 */
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 */
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
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 */
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
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
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
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
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
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}