| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.music.utils; |
| |
| import android.app.Notification; |
| import android.app.NotificationManager; |
| import android.app.PendingIntent; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Color; |
| import android.media.MediaDescription; |
| import android.media.MediaMetadata; |
| import android.media.session.MediaController; |
| import android.media.session.MediaSession; |
| import android.media.session.PlaybackState; |
| import android.service.media.MediaBrowserService; |
| import android.util.Log; |
| import com.android.music.MediaPlaybackService; |
| import com.android.music.R; |
| |
| /** |
| * Keeps track of a notification and updates it automatically for a given |
| * MediaSession. Maintaining a visible notification (usually) guarantees that the music service |
| * won't be killed during playback. |
| */ |
| public class MediaNotificationManager extends BroadcastReceiver { |
| private static final String TAG = LogHelper.makeLogTag(MediaNotificationManager.class); |
| |
| private static final int NOTIFICATION_ID = 412; |
| private static final int REQUEST_CODE = 100; |
| |
| public static final String ACTION_PAUSE = "com.android.music.pause"; |
| public static final String ACTION_PLAY = "com.android.music.play"; |
| public static final String ACTION_PREV = "com.android.music.prev"; |
| public static final String ACTION_NEXT = "com.android.music.next"; |
| |
| private final MediaPlaybackService mService; |
| private MediaSession.Token mSessionToken; |
| private MediaController mController; |
| private MediaController.TransportControls mTransportControls; |
| |
| private PlaybackState mPlaybackState; |
| private MediaMetadata mMetadata; |
| |
| private NotificationManager mNotificationManager; |
| |
| private PendingIntent mPauseIntent; |
| private PendingIntent mPlayIntent; |
| private PendingIntent mPreviousIntent; |
| private PendingIntent mNextIntent; |
| |
| private int mNotificationColor; |
| |
| private boolean mStarted = false; |
| |
| public MediaNotificationManager(MediaPlaybackService service) { |
| mService = service; |
| updateSessionToken(); |
| |
| mNotificationColor = |
| ResourceHelper.getThemeColor(mService, android.R.attr.colorPrimary, Color.DKGRAY); |
| |
| mNotificationManager = |
| (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE); |
| |
| String pkg = mService.getPackageName(); |
| mPauseIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE, |
| new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); |
| mPlayIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE, |
| new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); |
| mPreviousIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE, |
| new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); |
| mNextIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE, |
| new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT); |
| |
| // Cancel all notifications to handle the case where the Service was killed and |
| // restarted by the system. |
| mNotificationManager.cancelAll(); |
| } |
| |
| /** |
| * Posts the notification and starts tracking the session to keep it |
| * updated. The notification will automatically be removed if the session is |
| * destroyed before {@link #stopNotification} is called. |
| */ |
| public void startNotification() { |
| if (!mStarted) { |
| mMetadata = mController.getMetadata(); |
| mPlaybackState = mController.getPlaybackState(); |
| |
| // The notification must be updated after setting started to true |
| Notification notification = createNotification(); |
| if (notification != null) { |
| mController.registerCallback(mCb); |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(ACTION_NEXT); |
| filter.addAction(ACTION_PAUSE); |
| filter.addAction(ACTION_PLAY); |
| filter.addAction(ACTION_PREV); |
| mService.registerReceiver(this, filter); |
| |
| mService.startForeground(NOTIFICATION_ID, notification); |
| mStarted = true; |
| } |
| } |
| } |
| |
| /** |
| * Removes the notification and stops tracking the session. If the session |
| * was destroyed this has no effect. |
| */ |
| public void stopNotification() { |
| if (mStarted) { |
| mStarted = false; |
| mController.unregisterCallback(mCb); |
| try { |
| mNotificationManager.cancel(NOTIFICATION_ID); |
| mService.unregisterReceiver(this); |
| } catch (IllegalArgumentException ex) { |
| // ignore if the receiver is not registered. |
| } |
| mService.stopForeground(true); |
| } |
| } |
| |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| final String action = intent.getAction(); |
| LogHelper.d(TAG, "Received intent with action " + action); |
| switch (action) { |
| case ACTION_PAUSE: |
| mTransportControls.pause(); |
| break; |
| case ACTION_PLAY: |
| mTransportControls.play(); |
| break; |
| case ACTION_NEXT: |
| mTransportControls.skipToNext(); |
| break; |
| case ACTION_PREV: |
| mTransportControls.skipToPrevious(); |
| break; |
| default: |
| LogHelper.w(TAG, "Unknown intent ignored. Action=", action); |
| } |
| } |
| |
| /** |
| * Update the state based on a change on the session token. Called either when |
| * we are running for the first time or when the media session owner has destroyed the session |
| * (see {@link android.media.session.MediaController.Callback#onSessionDestroyed()}) |
| */ |
| private void updateSessionToken() { |
| MediaSession.Token freshToken = mService.getSessionToken(); |
| if (mSessionToken == null || !mSessionToken.equals(freshToken)) { |
| if (mController != null) { |
| mController.unregisterCallback(mCb); |
| } |
| mSessionToken = freshToken; |
| mController = new MediaController(mService, mSessionToken); |
| mTransportControls = mController.getTransportControls(); |
| if (mStarted) { |
| mController.registerCallback(mCb); |
| } |
| } |
| } |
| |
| private PendingIntent createContentIntent() { |
| Intent openUI = new Intent(mService, MediaBrowserService.class); |
| openUI.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); |
| return PendingIntent.getActivity( |
| mService, REQUEST_CODE, openUI, PendingIntent.FLAG_CANCEL_CURRENT); |
| } |
| |
| private final MediaController.Callback mCb = new MediaController.Callback() { |
| @Override |
| public void onPlaybackStateChanged(PlaybackState state) { |
| mPlaybackState = state; |
| LogHelper.d(TAG, "Received new playback state", state); |
| if (state != null |
| && (state.getState() == PlaybackState.STATE_STOPPED |
| || state.getState() == PlaybackState.STATE_NONE)) { |
| stopNotification(); |
| } else { |
| Notification notification = createNotification(); |
| if (notification != null) { |
| mNotificationManager.notify(NOTIFICATION_ID, notification); |
| } |
| } |
| } |
| |
| @Override |
| public void onMetadataChanged(MediaMetadata metadata) { |
| mMetadata = metadata; |
| LogHelper.d(TAG, "Received new metadata ", metadata); |
| Notification notification = createNotification(); |
| if (notification != null) { |
| mNotificationManager.notify(NOTIFICATION_ID, notification); |
| } |
| } |
| |
| @Override |
| public void onSessionDestroyed() { |
| super.onSessionDestroyed(); |
| LogHelper.d(TAG, "Session was destroyed, resetting to the new session token"); |
| updateSessionToken(); |
| } |
| }; |
| |
| private Notification createNotification() { |
| LogHelper.d(TAG, "updateNotificationMetadata. mMetadata=" + mMetadata); |
| if (mMetadata == null || mPlaybackState == null) { |
| return null; |
| } |
| |
| Notification.Builder notificationBuilder = new Notification.Builder(mService); |
| int playPauseButtonPosition = 0; |
| |
| // If skip to previous action is enabled |
| if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) { |
| notificationBuilder.addAction(R.drawable.ic_skip_previous_white_24dp, |
| mService.getString(R.string.skip_previous), mPreviousIntent); |
| |
| // If there is a "skip to previous" button, the play/pause button will |
| // be the second one. We need to keep track of it, because the MediaStyle notification |
| // requires to specify the index of the buttons (actions) that should be visible |
| // when in compact view. |
| playPauseButtonPosition = 1; |
| } |
| |
| addPlayPauseAction(notificationBuilder); |
| |
| // If skip to next action is enabled |
| if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) { |
| notificationBuilder.addAction(R.drawable.ic_skip_next_white_24dp, |
| mService.getString(R.string.skip_next), mNextIntent); |
| } |
| |
| MediaDescription description = mMetadata.getDescription(); |
| |
| String fetchArtUrl = null; |
| Bitmap art = null; |
| if (description.getIconUri() != null) { |
| // This sample assumes the iconUri will be a valid URL formatted String, but |
| // it can actually be any valid Android Uri formatted String. |
| // async fetch the album art icon |
| String artUrl = description.getIconUri().toString(); |
| art = AlbumArtCache.getInstance().getBigImage(artUrl); |
| if (art == null) { |
| fetchArtUrl = artUrl; |
| // use a placeholder art while the remote art is being downloaded |
| art = BitmapFactory.decodeResource( |
| mService.getResources(), R.drawable.ic_default_art); |
| } |
| } |
| |
| notificationBuilder |
| .setStyle(new Notification.MediaStyle() |
| .setShowActionsInCompactView( |
| playPauseButtonPosition) // show only play/pause in |
| // compact view |
| .setMediaSession(mSessionToken)) |
| .setColor(mNotificationColor) |
| .setSmallIcon(R.drawable.ic_notification) |
| .setVisibility(Notification.VISIBILITY_PUBLIC) |
| .setUsesChronometer(true) |
| .setContentIntent(createContentIntent()) |
| .setContentTitle(description.getTitle()) |
| .setContentText(description.getSubtitle()) |
| .setLargeIcon(art); |
| |
| setNotificationPlaybackState(notificationBuilder); |
| if (fetchArtUrl != null) { |
| fetchBitmapFromURLAsync(fetchArtUrl, notificationBuilder); |
| } |
| |
| return notificationBuilder.build(); |
| } |
| |
| private void addPlayPauseAction(Notification.Builder builder) { |
| LogHelper.d(TAG, "updatePlayPauseAction"); |
| String label; |
| int icon; |
| PendingIntent intent; |
| if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING) { |
| label = mService.getString(R.string.play_pause); |
| icon = R.drawable.ic_pause_white_24dp; |
| intent = mPauseIntent; |
| } else { |
| label = mService.getString(R.string.play_item); |
| icon = R.drawable.ic_play_arrow_white_24dp; |
| intent = mPlayIntent; |
| } |
| builder.addAction(new Notification.Action(icon, label, intent)); |
| } |
| |
| private void setNotificationPlaybackState(Notification.Builder builder) { |
| LogHelper.d(TAG, "updateNotificationPlaybackState. mPlaybackState=" + mPlaybackState); |
| if (mPlaybackState == null || !mStarted) { |
| LogHelper.d(TAG, "updateNotificationPlaybackState. cancelling notification!"); |
| mService.stopForeground(true); |
| return; |
| } |
| if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING |
| && mPlaybackState.getPosition() >= 0) { |
| LogHelper.d(TAG, "updateNotificationPlaybackState. updating playback position to ", |
| (System.currentTimeMillis() - mPlaybackState.getPosition()) / 1000, " seconds"); |
| builder.setWhen(System.currentTimeMillis() - mPlaybackState.getPosition()) |
| .setShowWhen(true) |
| .setUsesChronometer(true); |
| } else { |
| LogHelper.d(TAG, "updateNotificationPlaybackState. hiding playback position"); |
| builder.setWhen(0).setShowWhen(false).setUsesChronometer(false); |
| } |
| |
| // Make sure that the notification can be dismissed by the user when we are not playing: |
| builder.setOngoing(mPlaybackState.getState() == PlaybackState.STATE_PLAYING); |
| } |
| |
| private void fetchBitmapFromURLAsync( |
| final String bitmapUrl, final Notification.Builder builder) { |
| AlbumArtCache.getInstance().fetch(bitmapUrl, new AlbumArtCache.FetchListener() { |
| @Override |
| public void onFetched(String artUrl, Bitmap bitmap, Bitmap icon) { |
| if (mMetadata != null && mMetadata.getDescription() != null |
| && artUrl.equals(mMetadata.getDescription().getIconUri().toString())) { |
| // If the media is still the same, update the notification: |
| LogHelper.d(TAG, "fetchBitmapFromURLAsync: set bitmap to ", artUrl); |
| builder.setLargeIcon(bitmap); |
| mNotificationManager.notify(NOTIFICATION_ID, builder.build()); |
| } |
| } |
| }); |
| } |
| } |