Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (c) 2016, 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 package com.android.car.stream.media;
     17 
     18 import android.content.Context;
     19 import android.graphics.Bitmap;
     20 import android.media.MediaDescription;
     21 import android.media.MediaMetadata;
     22 import android.media.session.PlaybackState;
     23 import android.net.Uri;
     24 import android.os.Handler;
     25 import android.os.Message;
     26 import android.support.annotation.NonNull;
     27 import android.support.annotation.Nullable;
     28 import android.text.TextUtils;
     29 import android.util.Log;
     30 import com.android.car.apps.common.BitmapDownloader;
     31 import com.android.car.apps.common.BitmapWorkerOptions;
     32 import com.android.car.stream.R;
     33 
     34 /**
     35  * An service which connects to {@link MediaStateManager} for media updates (playback state and
     36  * metadata) and notifies listeners for these changes.
     37  * <p/>
     38  */
     39 public class MediaPlaybackMonitor implements MediaStateManager.Listener {
     40     protected static final String TAG = "MediaPlaybackMonitor";
     41 
     42     // MSG for metadata update handler
     43     private static final int MSG_UPDATE_METADATA = 1;
     44     private static final int MSG_IMAGE_DOWNLOADED = 2;
     45     private static final int MSG_NEW_ALBUM_ART_RECEIVED = 3;
     46 
     47     public interface MediaPlaybackMonitorListener {
     48         void onPlaybackStateChanged(PlaybackState state);
     49 
     50         void onMetadataChanged(String title, String text, Bitmap art, int color, String appName);
     51 
     52         void onAlbumArtUpdated(Bitmap albumArt);
     53 
     54         void onNewAppConnected();
     55 
     56         void removeMediaStreamCard();
     57     }
     58 
     59     private static final String[] PREFERRED_BITMAP_ORDER = {
     60             MediaMetadata.METADATA_KEY_ALBUM_ART,
     61             MediaMetadata.METADATA_KEY_ART,
     62             MediaMetadata.METADATA_KEY_DISPLAY_ICON
     63     };
     64 
     65     private static final String[] PREFERRED_URI_ORDER = {
     66             MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
     67             MediaMetadata.METADATA_KEY_ART_URI,
     68             MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
     69     };
     70 
     71     private MediaMetadata mCurrentMetadata;
     72     private MediaStatusUpdateHandler mMediaStatusUpdateHandler;
     73     private MediaAppInfo mCurrentMediaAppInfo;
     74     private MediaPlaybackMonitorListener mMonitorListener;
     75 
     76     private Context mContext;
     77 
     78     private final int mIconSize;
     79 
     80     public MediaPlaybackMonitor(Context context, @NonNull MediaPlaybackMonitorListener callback) {
     81         mContext = context;
     82         mMonitorListener = callback;
     83         mIconSize = mContext.getResources().getDimensionPixelSize(R.dimen.stream_media_icon_size);
     84     }
     85 
     86     public final void start() {
     87         mMediaStatusUpdateHandler = new MediaStatusUpdateHandler();
     88     }
     89 
     90     public final void stop() {
     91         if (mMediaStatusUpdateHandler != null) {
     92             mMediaStatusUpdateHandler.removeCallbacksAndMessages(null);
     93             mMediaStatusUpdateHandler = null;
     94         }
     95     }
     96 
     97     @Override
     98     public void onMediaSessionConnected(PlaybackState state, MediaMetadata metaData,
     99             MediaAppInfo appInfo) {
    100         if (Log.isLoggable(TAG, Log.DEBUG)) {
    101             Log.d(TAG, "MediaSession onConnected called");
    102         }
    103 
    104         // If the current media app is not the same as the new media app, reset
    105         // the media app in MediaStreamManager
    106         if (mCurrentMediaAppInfo == null
    107                 || !mCurrentMediaAppInfo.getPackageName().equals(appInfo.getPackageName())) {
    108             mMonitorListener.onNewAppConnected();
    109             if (mMediaStatusUpdateHandler != null) {
    110                 mMediaStatusUpdateHandler.removeCallbacksAndMessages(null);
    111             }
    112             mCurrentMediaAppInfo = appInfo;
    113         }
    114 
    115         if (metaData != null) {
    116             onMetadataChanged(metaData);
    117         }
    118 
    119         if (state != null) {
    120             onPlaybackStateChanged(state);
    121         }
    122     }
    123 
    124     @Override
    125     public void onPlaybackStateChanged(@Nullable PlaybackState state) {
    126         if (Log.isLoggable(TAG, Log.DEBUG)) {
    127             Log.d(TAG, "onPlaybackStateChanged called " + state.getState());
    128         }
    129 
    130         if (state == null) {
    131             Log.w(TAG, "playback state is null in onPlaybackStateChanged");
    132             mMonitorListener.removeMediaStreamCard();
    133             return;
    134         }
    135 
    136         if (mMonitorListener != null) {
    137             mMonitorListener.onPlaybackStateChanged(state);
    138         }
    139     }
    140 
    141     @Override
    142     public void onMetadataChanged(@Nullable MediaMetadata metadata) {
    143         if (Log.isLoggable(TAG, Log.DEBUG)) {
    144             Log.d(TAG, "onMetadataChanged called");
    145         }
    146         if (metadata == null) {
    147             mMonitorListener.removeMediaStreamCard();
    148             return;
    149         }
    150         if (Log.isLoggable(TAG, Log.DEBUG)) {
    151             Log.d(TAG, "received " + metadata.getDescription());
    152         }
    153         // Compare the new metadata and the last we have posted notification for. If both
    154         // metadata and album art are the same, just ignore and return. If the album art is new,
    155         // update the stream item with the new album art.
    156         MediaDescription currentDescription = mCurrentMetadata == null ?
    157                 null : mCurrentMetadata.getDescription();
    158 
    159         if (!MediaUtils.isSameMediaDescription(metadata.getDescription(), currentDescription)) {
    160             Message msg =
    161                     mMediaStatusUpdateHandler.obtainMessage(MSG_UPDATE_METADATA, metadata);
    162             // Remove obsolete notifications  in the queue.
    163             mMediaStatusUpdateHandler.removeMessages(MSG_UPDATE_METADATA);
    164             mMediaStatusUpdateHandler.sendMessage(msg);
    165         } else {
    166             Bitmap newBitmap = metadata.getDescription().getIconBitmap();
    167             if (newBitmap == null) {
    168                 return;
    169             }
    170             if (newBitmap.sameAs(mMediaStatusUpdateHandler.getCurrentIcon())) {
    171                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    172                     Log.d(TAG, "Received duplicate metadata, ignoring...");
    173                 }
    174             } else {
    175                 // same metadata, but new album art
    176                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    177                     Log.d(TAG, "Received metadata with new album art");
    178                 }
    179                 Message msg = mMediaStatusUpdateHandler
    180                         .obtainMessage(MSG_NEW_ALBUM_ART_RECEIVED, newBitmap);
    181                 mMediaStatusUpdateHandler.removeMessages(MSG_NEW_ALBUM_ART_RECEIVED);
    182                 mMediaStatusUpdateHandler.sendMessage(msg);
    183             }
    184         }
    185     }
    186 
    187     @Override
    188     public void onSessionDestroyed() {
    189         if (Log.isLoggable(TAG, Log.DEBUG)) {
    190             Log.d(TAG, "Media session destroyed");
    191         }
    192         mMonitorListener.removeMediaStreamCard();
    193     }
    194 
    195     private class BitmapCallback extends BitmapDownloader.BitmapCallback {
    196         final private int mSeq;
    197 
    198         public BitmapCallback(int seq) {
    199             mSeq = seq;
    200         }
    201 
    202         @Override
    203         public void onBitmapRetrieved(Bitmap bitmap) {
    204             if (mMediaStatusUpdateHandler == null) {
    205                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    206                     Log.d(TAG, "The callback comes after we finish");
    207                 }
    208                 return;
    209             }
    210             Message msg = mMediaStatusUpdateHandler.obtainMessage(MSG_IMAGE_DOWNLOADED,
    211                     mSeq, 0, bitmap);
    212             mMediaStatusUpdateHandler.sendMessage(msg);
    213         }
    214     }
    215 
    216     private class MediaStatusUpdateHandler extends Handler {
    217         private int mSeq = 0;
    218         private BitmapCallback mCallback;
    219         private MediaMetadata mMetadata;
    220         private String mTitle;
    221         private String mSubtitle;
    222         private Bitmap mIcon;
    223         private Uri mIconUri;
    224         private final BitmapDownloader mDownloader = BitmapDownloader.getInstance(mContext);
    225 
    226         private void extractMetadata(MediaMetadata metadata) {
    227             if (metadata == mMetadata) {
    228                 // We are up to date and must return here, because we've already recycled the bitmap
    229                 // inside it.
    230                 return;
    231             }
    232             // keep a reference so we know which metadata we have stored.
    233             mMetadata = metadata;
    234             MediaDescription description = metadata.getDescription();
    235             mTitle = description.getTitle() == null ? null : description.getTitle().toString();
    236             mSubtitle = description.getSubtitle() == null ?
    237                     null : description.getSubtitle().toString();
    238             final Bitmap originalBitmap = getMetadataBitmap(metadata);
    239             if (originalBitmap != null) {
    240                 mIcon = originalBitmap;
    241             } else {
    242                 mIcon = null;
    243             }
    244             mIconUri = getMetadataIconUri(metadata);
    245             if (Log.isLoggable(TAG, Log.DEBUG)) {
    246                 Log.d(TAG, "Album Art Uri: " + mIconUri);
    247             }
    248         }
    249 
    250         private Uri getMetadataIconUri(MediaMetadata metadata) {
    251             // Get the best Uri we can find
    252             for (int i = 0; i < PREFERRED_URI_ORDER.length; i++) {
    253                 String iconUri = metadata.getString(PREFERRED_URI_ORDER[i]);
    254                 if (!TextUtils.isEmpty(iconUri)) {
    255                     return Uri.parse(iconUri);
    256                 }
    257             }
    258             return null;
    259         }
    260 
    261         private Bitmap getMetadataBitmap(MediaMetadata metadata) {
    262             // Get the best art bitmap we can find
    263             for (int i = 0; i < PREFERRED_BITMAP_ORDER.length; i++) {
    264                 Bitmap bitmap = metadata.getBitmap(PREFERRED_BITMAP_ORDER[i]);
    265                 if (bitmap != null) {
    266                     if (Log.isLoggable(TAG, Log.DEBUG)) {
    267                         Log.d(TAG, "Retrieved bitmap type: " + PREFERRED_BITMAP_ORDER[i]
    268                                 + " w: " + bitmap.getWidth()
    269                                 + " h: " + bitmap.getHeight());
    270                     }
    271                     return bitmap;
    272                 }
    273             }
    274             return null;
    275         }
    276 
    277         public Bitmap getCurrentIcon() {
    278             return mIcon;
    279         }
    280 
    281         @Override
    282         public void handleMessage(Message msg) {
    283             MediaAppInfo mediaAppInfo = mCurrentMediaAppInfo;
    284             int color = mediaAppInfo.getMediaClientAccentColor();
    285             String appName = mediaAppInfo.getAppName();
    286             switch (msg.what) {
    287                 case MSG_UPDATE_METADATA:
    288                     mSeq++;
    289                     MediaMetadata metadata = (MediaMetadata) msg.obj;
    290                     if (metadata == null) {
    291                         Log.w(TAG, "media metadata is null!");
    292                         return;
    293                     }
    294                     extractMetadata(metadata);
    295                     if (mCallback != null) {
    296                         // it's ok to cancel a callback that has already been called, the downloader
    297                         // will just ignore the operation.
    298                         mDownloader.cancelDownload(mCallback);
    299                         mCallback = null;
    300                     }
    301                     if (mIcon != null) {
    302                         mMonitorListener.onMetadataChanged(mTitle, mSubtitle, mIcon,
    303                                 color, appName);
    304                     } else if (mIconUri != null) {
    305                         mCallback = new BitmapCallback(mSeq);
    306                         mDownloader.getBitmap(
    307                                 new BitmapWorkerOptions.Builder(mContext)
    308                                         .resource(mIconUri).width(mIconSize)
    309                                         .height(mIconSize).build(), mCallback);
    310                     } else {
    311                         mMonitorListener.onMetadataChanged(mTitle, mSubtitle, mIcon,
    312                                 color, appName);
    313                     }
    314                     // Only set mCurrentMetadata after we have updated the listener (if the
    315                     // bitmap is downloaded asynchronously, that is fine too. The stream card will
    316                     // be posted, when image is downloaded.)
    317                     mCurrentMetadata = metadata;
    318                     break;
    319 
    320                 case MSG_IMAGE_DOWNLOADED:
    321                     if (Log.isLoggable(TAG, Log.DEBUG)) {
    322                         Log.d(TAG, "Image downloaded...");
    323                     }
    324                     int seq = msg.arg1;
    325                     Bitmap bitmap = (Bitmap) msg.obj;
    326                     if (seq == mSeq) {
    327                         mMonitorListener.onMetadataChanged(mTitle, mSubtitle, bitmap, color, appName);
    328                     }
    329                     break;
    330 
    331                 case MSG_NEW_ALBUM_ART_RECEIVED:
    332                     if (Log.isLoggable(TAG, Log.DEBUG)) {
    333                         Log.d(TAG, "Received a new album art...");
    334                     }
    335                     Bitmap newAlbumArt = (Bitmap) msg.obj;
    336                     mMonitorListener.onAlbumArtUpdated(newAlbumArt);
    337                     break;
    338                 default:
    339             }
    340         }
    341     }
    342 }
    343