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