Home | History | Annotate | Download | only in 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.content.pm.ApplicationInfo;
     27 import android.content.pm.PackageManager;
     28 import android.content.res.Resources;
     29 import android.content.res.TypedArray;
     30 import android.graphics.Bitmap;
     31 import android.graphics.BitmapFactory;
     32 import android.graphics.Color;
     33 import android.media.MediaDescription;
     34 import android.media.MediaMetadata;
     35 import android.media.session.MediaController;
     36 import android.media.session.MediaSession;
     37 import android.media.session.PlaybackState;
     38 import android.os.AsyncTask;
     39 import android.util.LruCache;
     40 
     41 import com.example.android.mediabrowserservice.utils.BitmapHelper;
     42 import com.example.android.mediabrowserservice.utils.LogHelper;
     43 
     44 import java.io.IOException;
     45 
     46 /**
     47  * Keeps track of a notification and updates it automatically for a given
     48  * MediaSession. Maintaining a visible notification (usually) guarantees that the music service
     49  * won't be killed during playback.
     50  */
     51 public class MediaNotificationManager extends BroadcastReceiver {
     52     private static final String TAG = LogHelper.makeLogTag(MediaNotificationManager.class.getSimpleName());
     53 
     54     private static final int NOTIFICATION_ID = 412;
     55 
     56     public static final String ACTION_PAUSE = "com.example.android.mediabrowserservice.pause";
     57     public static final String ACTION_PLAY = "com.example.android.mediabrowserservice.play";
     58     public static final String ACTION_PREV = "com.example.android.mediabrowserservice.prev";
     59     public static final String ACTION_NEXT = "com.example.android.mediabrowserservice.next";
     60 
     61     private static final int MAX_ALBUM_ART_CACHE_SIZE = 1024*1024;
     62 
     63     private final MusicService mService;
     64     private MediaSession.Token mSessionToken;
     65     private MediaController mController;
     66     private MediaController.TransportControls mTransportControls;
     67     private final LruCache<String, Bitmap> mAlbumArtCache;
     68 
     69     private PlaybackState mPlaybackState;
     70     private MediaMetadata mMetadata;
     71 
     72     private Notification.Builder mNotificationBuilder;
     73     private NotificationManager mNotificationManager;
     74     private Notification.Action mPlayPauseAction;
     75 
     76     private PendingIntent mPauseIntent, mPlayIntent, mPreviousIntent, mNextIntent;
     77 
     78     private int mNotificationColor;
     79 
     80     private boolean mStarted = false;
     81 
     82     public MediaNotificationManager(MusicService service) {
     83         mService = service;
     84         updateSessionToken();
     85 
     86         // simple album art cache that holds no more than
     87         // MAX_ALBUM_ART_CACHE_SIZE bytes:
     88         mAlbumArtCache = new LruCache<String, Bitmap>(MAX_ALBUM_ART_CACHE_SIZE) {
     89             @Override
     90             protected int sizeOf(String key, Bitmap value) {
     91                 return value.getByteCount();
     92             }
     93         };
     94 
     95         mNotificationColor = getNotificationColor();
     96 
     97         mNotificationManager = (NotificationManager) mService
     98                 .getSystemService(Context.NOTIFICATION_SERVICE);
     99 
    100         String pkg = mService.getPackageName();
    101         mPauseIntent = PendingIntent.getBroadcast(mService, 100,
    102                 new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
    103         mPlayIntent = PendingIntent.getBroadcast(mService, 100,
    104                 new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
    105         mPreviousIntent = PendingIntent.getBroadcast(mService, 100,
    106                 new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
    107         mNextIntent = PendingIntent.getBroadcast(mService, 100,
    108                 new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
    109     }
    110 
    111     protected int getNotificationColor() {
    112         int notificationColor = 0;
    113         String packageName = mService.getPackageName();
    114         try {
    115             Context packageContext = mService.createPackageContext(packageName, 0);
    116             ApplicationInfo applicationInfo =
    117                     mService.getPackageManager().getApplicationInfo(packageName, 0);
    118             packageContext.setTheme(applicationInfo.theme);
    119             Resources.Theme theme = packageContext.getTheme();
    120             TypedArray ta = theme.obtainStyledAttributes(
    121                     new int[] {android.R.attr.colorPrimary});
    122             notificationColor = ta.getColor(0, Color.DKGRAY);
    123             ta.recycle();
    124         } catch (PackageManager.NameNotFoundException e) {
    125             e.printStackTrace();
    126         }
    127         return notificationColor;
    128     }
    129 
    130     /**
    131      * Posts the notification and starts tracking the session to keep it
    132      * updated. The notification will automatically be removed if the session is
    133      * destroyed before {@link #stopNotification} is called.
    134      */
    135     public void startNotification() {
    136         if (!mStarted) {
    137             mController.registerCallback(mCb);
    138             IntentFilter filter = new IntentFilter();
    139             filter.addAction(ACTION_NEXT);
    140             filter.addAction(ACTION_PAUSE);
    141             filter.addAction(ACTION_PLAY);
    142             filter.addAction(ACTION_PREV);
    143             mService.registerReceiver(this, filter);
    144 
    145             mMetadata = mController.getMetadata();
    146             mPlaybackState = mController.getPlaybackState();
    147 
    148             mStarted = true;
    149             // The notification must be updated after setting started to true
    150             updateNotificationMetadata();
    151         }
    152     }
    153 
    154     /**
    155      * Removes the notification and stops tracking the session. If the session
    156      * was destroyed this has no effect.
    157      */
    158     public void stopNotification() {
    159         mStarted = false;
    160         mController.unregisterCallback(mCb);
    161         try {
    162             mNotificationManager.cancel(NOTIFICATION_ID);
    163             mService.unregisterReceiver(this);
    164         } catch (IllegalArgumentException ex) {
    165             // ignore if the receiver is not registered.
    166         }
    167         mService.stopForeground(true);
    168     }
    169 
    170     @Override
    171     public void onReceive(Context context, Intent intent) {
    172         final String action = intent.getAction();
    173         LogHelper.d(TAG, "Received intent with action " + action);
    174         if (ACTION_PAUSE.equals(action)) {
    175             mTransportControls.pause();
    176         } else if (ACTION_PLAY.equals(action)) {
    177             mTransportControls.play();
    178         } else if (ACTION_NEXT.equals(action)) {
    179             mTransportControls.skipToNext();
    180         } else if (ACTION_PREV.equals(action)) {
    181             mTransportControls.skipToPrevious();
    182         }
    183     }
    184 
    185     /**
    186      * Update the state based on a change on the session token. Called either when
    187      * we are running for the first time or when the media session owner has destroyed the session
    188      * (see {@link android.media.session.MediaController.Callback#onSessionDestroyed()})
    189      */
    190     private void updateSessionToken() {
    191         MediaSession.Token freshToken = mService.getSessionToken();
    192         if (mSessionToken == null || !mSessionToken.equals(freshToken)) {
    193             if (mController != null) {
    194                 mController.unregisterCallback(mCb);
    195             }
    196             mSessionToken = freshToken;
    197             mController = new MediaController(mService, mSessionToken);
    198             mTransportControls = mController.getTransportControls();
    199             if (mStarted) {
    200                 mController.registerCallback(mCb);
    201             }
    202         }
    203     }
    204 
    205     private final MediaController.Callback mCb = new MediaController.Callback() {
    206         @Override
    207         public void onPlaybackStateChanged(PlaybackState state) {
    208             mPlaybackState = state;
    209             LogHelper.d(TAG, "Received new playback state", state);
    210             updateNotificationPlaybackState();
    211         }
    212 
    213         @Override
    214         public void onMetadataChanged(MediaMetadata metadata) {
    215             mMetadata = metadata;
    216             LogHelper.d(TAG, "Received new metadata ", metadata);
    217             updateNotificationMetadata();
    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 void updateNotificationMetadata() {
    229         LogHelper.d(TAG, "updateNotificationMetadata. mMetadata=" + mMetadata);
    230         if (mMetadata == null || mPlaybackState == null) {
    231             return;
    232         }
    233 
    234         updatePlayPauseAction();
    235 
    236         mNotificationBuilder = new Notification.Builder(mService);
    237         int playPauseActionIndex = 0;
    238 
    239         // If skip to previous action is enabled
    240         if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) {
    241             mNotificationBuilder
    242                     .addAction(R.drawable.ic_skip_previous_white_24dp,
    243                             mService.getString(R.string.label_previous), mPreviousIntent);
    244             playPauseActionIndex = 1;
    245         }
    246 
    247         mNotificationBuilder.addAction(mPlayPauseAction);
    248 
    249         // If skip to next action is enabled
    250         if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
    251             mNotificationBuilder.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 = description.getIconBitmap();
    259         if (art == null && 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 = mAlbumArtCache.get(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(), R.drawable.ic_default_art);
    269             }
    270         }
    271 
    272         mNotificationBuilder
    273                 .setStyle(new Notification.MediaStyle()
    274                         .setShowActionsInCompactView(playPauseActionIndex)  // only show play/pause in compact view
    275                         .setMediaSession(mSessionToken))
    276                 .setColor(mNotificationColor)
    277                 .setSmallIcon(R.drawable.ic_notification)
    278                 .setVisibility(Notification.VISIBILITY_PUBLIC)
    279                 .setUsesChronometer(true)
    280                 .setContentTitle(description.getTitle())
    281                 .setContentText(description.getSubtitle())
    282                 .setLargeIcon(art);
    283 
    284         updateNotificationPlaybackState();
    285 
    286         mService.startForeground(NOTIFICATION_ID, mNotificationBuilder.build());
    287         if (fetchArtUrl != null) {
    288             fetchBitmapFromURLAsync(fetchArtUrl);
    289         }
    290     }
    291 
    292     private void updatePlayPauseAction() {
    293         LogHelper.d(TAG, "updatePlayPauseAction");
    294         String label;
    295         int icon;
    296         PendingIntent intent;
    297         if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING) {
    298             label = mService.getString(R.string.label_pause);
    299             icon = R.drawable.ic_pause_white_24dp;
    300             intent = mPauseIntent;
    301         } else {
    302             label = mService.getString(R.string.label_play);
    303             icon = R.drawable.ic_play_arrow_white_24dp;
    304             intent = mPlayIntent;
    305         }
    306         if (mPlayPauseAction == null) {
    307             mPlayPauseAction = new Notification.Action(icon, label, intent);
    308         } else {
    309             mPlayPauseAction.icon = icon;
    310             mPlayPauseAction.title = label;
    311             mPlayPauseAction.actionIntent = intent;
    312         }
    313     }
    314 
    315     private void updateNotificationPlaybackState() {
    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 (mNotificationBuilder == null) {
    323             LogHelper.d(TAG, "updateNotificationPlaybackState. there is no notificationBuilder. Ignoring request to update state!");
    324             return;
    325         }
    326         if (mPlaybackState.getPosition() >= 0) {
    327             LogHelper.d(TAG, "updateNotificationPlaybackState. updating playback position to ",
    328                     (System.currentTimeMillis() - mPlaybackState.getPosition()) / 1000, " seconds");
    329             mNotificationBuilder
    330                     .setWhen(System.currentTimeMillis() - mPlaybackState.getPosition())
    331                     .setShowWhen(true)
    332                     .setUsesChronometer(true);
    333             mNotificationBuilder.setShowWhen(true);
    334         } else {
    335             LogHelper.d(TAG, "updateNotificationPlaybackState. hiding playback position");
    336             mNotificationBuilder
    337                     .setWhen(0)
    338                     .setShowWhen(false)
    339                     .setUsesChronometer(false);
    340         }
    341 
    342         updatePlayPauseAction();
    343 
    344         // Make sure that the notification can be dismissed by the user when we are not playing:
    345         mNotificationBuilder.setOngoing(mPlaybackState.getState() == PlaybackState.STATE_PLAYING);
    346 
    347         mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
    348     }
    349 
    350     public void fetchBitmapFromURLAsync(final String source) {
    351         LogHelper.d(TAG, "getBitmapFromURLAsync: starting asynctask to fetch ", source);
    352         new AsyncTask<Void, Void, Bitmap>() {
    353             @Override
    354             protected Bitmap doInBackground(Void[] objects) {
    355                 Bitmap bitmap = null;
    356                 try {
    357                     bitmap = BitmapHelper.fetchAndRescaleBitmap(source,
    358                             BitmapHelper.MEDIA_ART_BIG_WIDTH, BitmapHelper.MEDIA_ART_BIG_HEIGHT);
    359                     mAlbumArtCache.put(source, bitmap);
    360                 } catch (IOException e) {
    361                     LogHelper.e(TAG, e, "getBitmapFromURLAsync: " + source);
    362                 }
    363                 return bitmap;
    364             }
    365 
    366             @Override
    367             protected void onPostExecute(Bitmap bitmap) {
    368                 if (bitmap != null && mMetadata != null &&
    369                         mNotificationBuilder != null && mMetadata.getDescription() != null &&
    370                         !source.equals(mMetadata.getDescription().getIconUri())) {
    371                     // If the media is still the same, update the notification:
    372                     LogHelper.d(TAG, "getBitmapFromURLAsync: set bitmap to ", source);
    373                     mNotificationBuilder.setLargeIcon(bitmap);
    374                     mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
    375                 }
    376             }
    377         }.execute();
    378     }
    379 
    380 }
    381