| /* |
| * Copyright (C) 2015 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.example.android.supportv4.media; |
| |
| import android.app.Notification; |
| 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.os.RemoteException; |
| import android.support.v4.app.NotificationCompat; |
| import android.support.v4.app.NotificationManagerCompat; |
| import android.support.v4.media.MediaDescriptionCompat; |
| import android.support.v4.media.MediaMetadataCompat; |
| import android.support.v4.media.session.MediaControllerCompat; |
| import android.support.v4.media.session.MediaSessionCompat; |
| import android.support.v4.media.session.PlaybackStateCompat; |
| import android.util.Log; |
| |
| import com.example.android.supportv4.R; |
| import com.example.android.supportv4.media.utils.ResourceHelper; |
| |
| /** |
| * 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 = "MediaNotiManager"; |
| |
| private static final int NOTIFICATION_ID = 412; |
| private static final int REQUEST_CODE = 100; |
| |
| public static final String ACTION_PAUSE = "com.example.android.supportv4.media.pause"; |
| public static final String ACTION_PLAY = "com.example.android.supportv4.media.play"; |
| public static final String ACTION_PREV = "com.example.android.supportv4.media.prev"; |
| public static final String ACTION_NEXT = "com.example.android.supportv4.media.next"; |
| |
| private final MediaBrowserServiceSupport mService; |
| private MediaSessionCompat.Token mSessionToken; |
| private MediaControllerCompat mController; |
| private MediaControllerCompat.TransportControls mTransportControls; |
| |
| private PlaybackStateCompat mPlaybackState; |
| private MediaMetadataCompat mMetadata; |
| |
| private NotificationManagerCompat mNotificationManager; |
| |
| private PendingIntent mPauseIntent; |
| private PendingIntent mPlayIntent; |
| private PendingIntent mPreviousIntent; |
| private PendingIntent mNextIntent; |
| |
| private int mNotificationColor; |
| |
| private boolean mStarted = false; |
| |
| public MediaNotificationManager(MediaBrowserServiceSupport service) { |
| mService = service; |
| updateSessionToken(); |
| |
| mNotificationColor = ResourceHelper.getThemeColor(mService, |
| android.R.attr.colorPrimary, Color.DKGRAY); |
| |
| mNotificationManager = NotificationManagerCompat.from(mService); |
| |
| 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(); |
| Log.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: |
| Log.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() { |
| MediaSessionCompat.Token freshToken = mService.getSessionToken(); |
| if (mSessionToken == null || !mSessionToken.equals(freshToken)) { |
| if (mController != null) { |
| mController.unregisterCallback(mCb); |
| } |
| mSessionToken = freshToken; |
| try { |
| mController = new MediaControllerCompat(mService, mSessionToken); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Failed to create MediaControllerCompat.", e); |
| } |
| mTransportControls = mController.getTransportControls(); |
| if (mStarted) { |
| mController.registerCallback(mCb); |
| } |
| } |
| } |
| |
| private PendingIntent createContentIntent() { |
| Intent openUI = new Intent(mService, MediaBrowserSupport.class); |
| openUI.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); |
| return PendingIntent.getActivity(mService, REQUEST_CODE, openUI, |
| PendingIntent.FLAG_CANCEL_CURRENT); |
| } |
| |
| private final MediaControllerCompat.Callback mCb = new MediaControllerCompat.Callback() { |
| @Override |
| public void onPlaybackStateChanged(PlaybackStateCompat state) { |
| mPlaybackState = state; |
| Log.d(TAG, "Received new playback state " + state); |
| if (state != null && (state.getState() == PlaybackStateCompat.STATE_STOPPED || |
| state.getState() == PlaybackStateCompat.STATE_NONE)) { |
| stopNotification(); |
| } else { |
| Notification notification = createNotification(); |
| if (notification != null) { |
| mNotificationManager.notify(NOTIFICATION_ID, notification); |
| } |
| } |
| } |
| |
| @Override |
| public void onMetadataChanged(MediaMetadataCompat metadata) { |
| mMetadata = metadata; |
| Log.d(TAG, "Received new metadata " + metadata); |
| Notification notification = createNotification(); |
| if (notification != null) { |
| mNotificationManager.notify(NOTIFICATION_ID, notification); |
| } |
| } |
| |
| @Override |
| public void onSessionDestroyed() { |
| super.onSessionDestroyed(); |
| Log.d(TAG, "Session was destroyed, resetting to the new session token"); |
| updateSessionToken(); |
| } |
| }; |
| |
| private Notification createNotification() { |
| Log.d(TAG, "updateNotificationMetadata. mMetadata=" + mMetadata); |
| if (mMetadata == null || mPlaybackState == null) { |
| return null; |
| } |
| |
| NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(mService); |
| |
| // If skip to previous action is enabled |
| if ((mPlaybackState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0) { |
| notificationBuilder.addAction(R.drawable.ic_skip_previous_white_24dp, |
| mService.getString(R.string.label_previous), mPreviousIntent); |
| } |
| |
| addPlayPauseAction(notificationBuilder); |
| |
| // If skip to next action is enabled |
| if ((mPlaybackState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) { |
| notificationBuilder.addAction(R.drawable.ic_skip_next_white_24dp, |
| mService.getString(R.string.label_next), mNextIntent); |
| } |
| |
| MediaDescriptionCompat 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 |
| .setColor(mNotificationColor) |
| .setSmallIcon(R.drawable.ic_notification) |
| .setVisibility(NotificationCompat.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(NotificationCompat.Builder builder) { |
| Log.d(TAG, "updatePlayPauseAction"); |
| String label; |
| int icon; |
| PendingIntent intent; |
| if (mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING) { |
| label = mService.getString(R.string.label_pause); |
| icon = R.drawable.ic_pause_white_24dp; |
| intent = mPauseIntent; |
| } else { |
| label = mService.getString(R.string.label_play); |
| icon = R.drawable.ic_play_arrow_white_24dp; |
| intent = mPlayIntent; |
| } |
| builder.addAction(new NotificationCompat.Action(icon, label, intent)); |
| } |
| |
| private void setNotificationPlaybackState(NotificationCompat.Builder builder) { |
| Log.d(TAG, "updateNotificationPlaybackState. mPlaybackState=" + mPlaybackState); |
| if (mPlaybackState == null || !mStarted) { |
| Log.d(TAG, "updateNotificationPlaybackState. cancelling notification!"); |
| mService.stopForeground(true); |
| return; |
| } |
| if (mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING |
| && mPlaybackState.getPosition() >= 0) { |
| Log.d(TAG, "updateNotificationPlaybackState. updating playback position to " |
| + (System.currentTimeMillis() - mPlaybackState.getPosition()) / 1000 |
| + " seconds"); |
| builder |
| .setWhen(System.currentTimeMillis() - mPlaybackState.getPosition()) |
| .setShowWhen(true) |
| .setUsesChronometer(true); |
| } else { |
| Log.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() == PlaybackStateCompat.STATE_PLAYING); |
| } |
| |
| private void fetchBitmapFromURLAsync(final String bitmapUrl, |
| final NotificationCompat.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: |
| Log.d(TAG, "fetchBitmapFromURLAsync: set bitmap to " + artUrl); |
| builder.setLargeIcon(bitmap); |
| mNotificationManager.notify(NOTIFICATION_ID, builder.build()); |
| } |
| } |
| }); |
| } |
| } |