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