Home | History | Annotate | Download | only in utils
      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.android.music.utils;
     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 import android.service.media.MediaBrowserService;
     35 import android.util.Log;
     36 import com.android.music.MediaPlaybackService;
     37 import 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  */
     44 public 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 }
    354