Home | History | Annotate | Download | only in musicservicedemo
      1 /*
      2  * Copyright (C) 2014 Google Inc. All Rights Reserved.
      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.musicservicedemo;
     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 import android.util.SparseArray;
     41 
     42 import com.example.android.musicservicedemo.utils.BitmapHelper;
     43 import com.example.android.musicservicedemo.utils.LogHelper;
     44 
     45 import java.io.IOException;
     46 
     47 /**
     48  * Keeps track of a notification and updates it automatically for a given
     49  * MediaSession. Maintaining a visible notification (usually) guarantees that the music service
     50  * won't be killed during playback.
     51  */
     52 public class MediaNotification extends BroadcastReceiver {
     53     private static final String TAG = "MediaNotification";
     54 
     55     private static final int NOTIFICATION_ID = 412;
     56 
     57     public static final String ACTION_PAUSE = "com.example.android.musicservicedemo.pause";
     58     public static final String ACTION_PLAY = "com.example.android.musicservicedemo.play";
     59     public static final String ACTION_PREV = "com.example.android.musicservicedemo.prev";
     60     public static final String ACTION_NEXT = "com.example.android.musicservicedemo.next";
     61 
     62     private static final int MAX_ALBUM_ART_CACHE_SIZE = 1024*1024;
     63 
     64     private final MusicService mService;
     65     private MediaSession.Token mSessionToken;
     66     private MediaController mController;
     67     private MediaController.TransportControls mTransportControls;
     68     private final SparseArray<PendingIntent> mIntents = new SparseArray<PendingIntent>();
     69     private final LruCache<String, Bitmap> mAlbumArtCache;
     70 
     71     private PlaybackState mPlaybackState;
     72     private MediaMetadata mMetadata;
     73 
     74     private Notification.Builder mNotificationBuilder;
     75     private NotificationManager mNotificationManager;
     76     private Notification.Action mPlayPauseAction;
     77 
     78     private String mCurrentAlbumArt;
     79     private int mNotificationColor;
     80 
     81     private boolean mStarted = false;
     82 
     83     public MediaNotification(MusicService service) {
     84         mService = service;
     85         updateSessionToken();
     86 
     87         // simple album art cache that holds no more than
     88         // MAX_ALBUM_ART_CACHE_SIZE bytes:
     89         mAlbumArtCache = new LruCache<String, Bitmap>(MAX_ALBUM_ART_CACHE_SIZE) {
     90             @Override
     91             protected int sizeOf(String key, Bitmap value) {
     92                 return value.getByteCount();
     93             }
     94         };
     95 
     96         mNotificationColor = getNotificationColor();
     97 
     98         mNotificationManager = (NotificationManager) mService
     99                 .getSystemService(Context.NOTIFICATION_SERVICE);
    100 
    101         String pkg = mService.getPackageName();
    102         mIntents.put(R.drawable.ic_pause_white_24dp, PendingIntent.getBroadcast(mService, 100,
    103                 new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT));
    104         mIntents.put(R.drawable.ic_play_arrow_white_24dp, PendingIntent.getBroadcast(mService, 100,
    105                 new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT));
    106         mIntents.put(R.drawable.ic_skip_previous_white_24dp, PendingIntent.getBroadcast(mService, 100,
    107                 new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT));
    108         mIntents.put(R.drawable.ic_skip_next_white_24dp, PendingIntent.getBroadcast(mService, 100,
    109                 new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT));
    110     }
    111 
    112     protected int getNotificationColor() {
    113         int notificationColor = 0;
    114         String packageName = mService.getPackageName();
    115         try {
    116             Context packageContext = mService.createPackageContext(packageName, 0);
    117             ApplicationInfo applicationInfo =
    118                     mService.getPackageManager().getApplicationInfo(packageName, 0);
    119             packageContext.setTheme(applicationInfo.theme);
    120             Resources.Theme theme = packageContext.getTheme();
    121             TypedArray ta = theme.obtainStyledAttributes(
    122                     new int[] {android.R.attr.colorPrimary});
    123             notificationColor = ta.getColor(0, Color.DKGRAY);
    124             ta.recycle();
    125         } catch (PackageManager.NameNotFoundException e) {
    126             e.printStackTrace();
    127         }
    128         return notificationColor;
    129     }
    130 
    131     /**
    132      * Posts the notification and starts tracking the session to keep it
    133      * updated. The notification will automatically be removed if the session is
    134      * destroyed before {@link #stopNotification} is called.
    135      */
    136     public void startNotification() {
    137         if (!mStarted) {
    138             mController.registerCallback(mCb);
    139             IntentFilter filter = new IntentFilter();
    140             filter.addAction(ACTION_NEXT);
    141             filter.addAction(ACTION_PAUSE);
    142             filter.addAction(ACTION_PLAY);
    143             filter.addAction(ACTION_PREV);
    144             mService.registerReceiver(this, filter);
    145 
    146             mMetadata = mController.getMetadata();
    147             mPlaybackState = mController.getPlaybackState();
    148 
    149             mStarted = true;
    150             // The notification must be updated after setting started to true
    151             updateNotificationMetadata();
    152         }
    153     }
    154 
    155     /**
    156      * Removes the notification and stops tracking the session. If the session
    157      * was destroyed this has no effect.
    158      */
    159     public void stopNotification() {
    160         mStarted = false;
    161         mController.unregisterCallback(mCb);
    162         try {
    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),
    244                             mIntents.get(R.drawable.ic_skip_previous_white_24dp));
    245             playPauseActionIndex = 1;
    246         }
    247 
    248         mNotificationBuilder.addAction(mPlayPauseAction);
    249 
    250         // If skip to next action is enabled
    251         if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
    252             mNotificationBuilder.addAction(R.drawable.ic_skip_next_white_24dp,
    253                     mService.getString(R.string.label_next),
    254                     mIntents.get(R.drawable.ic_skip_next_white_24dp));
    255         }
    256 
    257         MediaDescription description = mMetadata.getDescription();
    258 
    259         String fetchArtUrl = null;
    260         Bitmap art = description.getIconBitmap();
    261         if (art == null && 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 = mAlbumArtCache.get(artUrl);
    267             if (art == null) {
    268                 fetchArtUrl = artUrl;
    269                 // use a placeholder art while the remote art is being downloaded
    270                 art = BitmapFactory.decodeResource(mService.getResources(), R.drawable.ic_default_art);
    271             }
    272         }
    273 
    274         mNotificationBuilder
    275                 .setStyle(new Notification.MediaStyle()
    276                         .setShowActionsInCompactView(playPauseActionIndex)  // only show play/pause in compact view
    277                         .setMediaSession(mSessionToken))
    278                 .setColor(mNotificationColor)
    279                 .setSmallIcon(R.drawable.ic_notification)
    280                 .setVisibility(Notification.VISIBILITY_PUBLIC)
    281                 .setOngoing(true)
    282                 .setUsesChronometer(true)
    283                 .setContentTitle(description.getTitle())
    284                 .setContentText(description.getSubtitle())
    285                 .setLargeIcon(art);
    286 
    287         updateNotificationPlaybackState();
    288 
    289         mService.startForeground(NOTIFICATION_ID, mNotificationBuilder.build());
    290         if (fetchArtUrl != null) {
    291             fetchBitmapFromURLAsync(fetchArtUrl);
    292         }
    293     }
    294 
    295     private void updatePlayPauseAction() {
    296         LogHelper.d(TAG, "updatePlayPauseAction");
    297         String playPauseLabel = "";
    298         int playPauseIcon;
    299         if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING) {
    300             playPauseLabel = mService.getString(R.string.label_pause);
    301             playPauseIcon = R.drawable.ic_pause_white_24dp;
    302         } else {
    303             playPauseLabel = mService.getString(R.string.label_play);
    304             playPauseIcon = R.drawable.ic_play_arrow_white_24dp;
    305         }
    306         if (mPlayPauseAction == null) {
    307             mPlayPauseAction = new Notification.Action(playPauseIcon, playPauseLabel,
    308                     mIntents.get(playPauseIcon));
    309         } else {
    310             mPlayPauseAction.icon = playPauseIcon;
    311             mPlayPauseAction.title = playPauseLabel;
    312             mPlayPauseAction.actionIntent = mIntents.get(playPauseIcon);
    313         }
    314     }
    315 
    316     private void updateNotificationPlaybackState() {
    317         LogHelper.d(TAG, "updateNotificationPlaybackState. mPlaybackState=" + mPlaybackState);
    318         if (mPlaybackState == null || !mStarted) {
    319             LogHelper.d(TAG, "updateNotificationPlaybackState. cancelling notification!");
    320             mService.stopForeground(true);
    321             return;
    322         }
    323         if (mNotificationBuilder == null) {
    324             LogHelper.d(TAG, "updateNotificationPlaybackState. there is no notificationBuilder. Ignoring request to update state!");
    325             return;
    326         }
    327         if (mPlaybackState.getPosition() >= 0) {
    328             LogHelper.d(TAG, "updateNotificationPlaybackState. updating playback position to ",
    329                     (System.currentTimeMillis() - mPlaybackState.getPosition()) / 1000, " seconds");
    330             mNotificationBuilder
    331                     .setWhen(System.currentTimeMillis() - mPlaybackState.getPosition())
    332                     .setShowWhen(true)
    333                     .setUsesChronometer(true);
    334             mNotificationBuilder.setShowWhen(true);
    335         } else {
    336             LogHelper.d(TAG, "updateNotificationPlaybackState. hiding playback position");
    337             mNotificationBuilder
    338                     .setWhen(0)
    339                     .setShowWhen(false)
    340                     .setUsesChronometer(false);
    341         }
    342 
    343         updatePlayPauseAction();
    344 
    345         mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
    346     }
    347 
    348     public void fetchBitmapFromURLAsync(final String source) {
    349         LogHelper.d(TAG, "getBitmapFromURLAsync: starting asynctask to fetch ", source);
    350         new AsyncTask<Void, Void, Bitmap>() {
    351             @Override
    352             protected Bitmap doInBackground(Void[] objects) {
    353                 Bitmap bitmap = null;
    354                 try {
    355                     bitmap = BitmapHelper.fetchAndRescaleBitmap(source,
    356                             BitmapHelper.MEDIA_ART_BIG_WIDTH, BitmapHelper.MEDIA_ART_BIG_HEIGHT);
    357                     mAlbumArtCache.put(source, bitmap);
    358                 } catch (IOException e) {
    359                     LogHelper.e(TAG, e, "getBitmapFromURLAsync: " + source);
    360                 }
    361                 return bitmap;
    362             }
    363 
    364             @Override
    365             protected void onPostExecute(Bitmap bitmap) {
    366                 if (bitmap != null && mMetadata != null &&
    367                         mNotificationBuilder != null && mMetadata.getDescription() != null &&
    368                         !source.equals(mMetadata.getDescription().getIconUri())) {
    369                     // If the media is still the same, update the notification:
    370                     LogHelper.d(TAG, "getBitmapFromURLAsync: set bitmap to ", source);
    371                     mNotificationBuilder.setLargeIcon(bitmap);
    372                     mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
    373                 }
    374             }
    375         }.execute();
    376     }
    377 
    378 }
    379