diff options
Diffstat (limited to 'src/com/android/car/stream/media/MediaPlaybackMonitor.java')
-rw-r--r-- | src/com/android/car/stream/media/MediaPlaybackMonitor.java | 342 |
1 files changed, 342 insertions, 0 deletions
diff --git a/src/com/android/car/stream/media/MediaPlaybackMonitor.java b/src/com/android/car/stream/media/MediaPlaybackMonitor.java new file mode 100644 index 0000000..c360770 --- /dev/null +++ b/src/com/android/car/stream/media/MediaPlaybackMonitor.java @@ -0,0 +1,342 @@ +/* + * Copyright (c) 2016, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.car.stream.media; + +import android.content.Context; +import android.graphics.Bitmap; +import android.media.MediaDescription; +import android.media.MediaMetadata; +import android.media.session.PlaybackState; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; +import com.android.car.apps.common.BitmapDownloader; +import com.android.car.apps.common.BitmapWorkerOptions; +import com.android.car.stream.R; + +/** + * An service which connects to {@link MediaStateManager} for media updates (playback state and + * metadata) and notifies listeners for these changes. + * <p/> + */ +public class MediaPlaybackMonitor implements MediaStateManager.Listener { + protected static final String TAG = "MediaPlaybackMonitor"; + + // MSG for metadata update handler + private static final int MSG_UPDATE_METADATA = 1; + private static final int MSG_IMAGE_DOWNLOADED = 2; + private static final int MSG_NEW_ALBUM_ART_RECEIVED = 3; + + public interface MediaPlaybackMonitorListener { + void onPlaybackStateChanged(PlaybackState state); + + void onMetadataChanged(String title, String text, Bitmap art, int color, String appName); + + void onAlbumArtUpdated(Bitmap albumArt); + + void onNewAppConnected(); + + void removeMediaStreamCard(); + } + + private static final String[] PREFERRED_BITMAP_ORDER = { + MediaMetadata.METADATA_KEY_ALBUM_ART, + MediaMetadata.METADATA_KEY_ART, + MediaMetadata.METADATA_KEY_DISPLAY_ICON + }; + + private static final String[] PREFERRED_URI_ORDER = { + MediaMetadata.METADATA_KEY_ALBUM_ART_URI, + MediaMetadata.METADATA_KEY_ART_URI, + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI + }; + + private MediaMetadata mCurrentMetadata; + private MediaStatusUpdateHandler mMediaStatusUpdateHandler; + private MediaAppInfo mCurrentMediaAppInfo; + private MediaPlaybackMonitorListener mMonitorListener; + + private Context mContext; + + private final int mIconSize; + + public MediaPlaybackMonitor(Context context, @NonNull MediaPlaybackMonitorListener callback) { + mContext = context; + mMonitorListener = callback; + mIconSize = mContext.getResources().getDimensionPixelSize(R.dimen.stream_media_icon_size); + } + + public final void start() { + mMediaStatusUpdateHandler = new MediaStatusUpdateHandler(); + } + + public final void stop() { + if (mMediaStatusUpdateHandler != null) { + mMediaStatusUpdateHandler.removeCallbacksAndMessages(null); + mMediaStatusUpdateHandler = null; + } + } + + @Override + public void onMediaSessionConnected(PlaybackState state, MediaMetadata metaData, + MediaAppInfo appInfo) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "MediaSession onConnected called"); + } + + // If the current media app is not the same as the new media app, reset + // the media app in MediaStreamManager + if (mCurrentMediaAppInfo == null + || !mCurrentMediaAppInfo.getPackageName().equals(appInfo.getPackageName())) { + mMonitorListener.onNewAppConnected(); + if (mMediaStatusUpdateHandler != null) { + mMediaStatusUpdateHandler.removeCallbacksAndMessages(null); + } + mCurrentMediaAppInfo = appInfo; + } + + if (metaData != null) { + onMetadataChanged(metaData); + } + + if (state != null) { + onPlaybackStateChanged(state); + } + } + + @Override + public void onPlaybackStateChanged(@Nullable PlaybackState state) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "onPlaybackStateChanged called " + state.getState()); + } + + if (state == null) { + Log.w(TAG, "playback state is null in onPlaybackStateChanged"); + mMonitorListener.removeMediaStreamCard(); + return; + } + + if (mMonitorListener != null) { + mMonitorListener.onPlaybackStateChanged(state); + } + } + + @Override + public void onMetadataChanged(@Nullable MediaMetadata metadata) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "onMetadataChanged called"); + } + if (metadata == null) { + mMonitorListener.removeMediaStreamCard(); + return; + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "received " + metadata.getDescription()); + } + // Compare the new metadata and the last we have posted notification for. If both + // metadata and album art are the same, just ignore and return. If the album art is new, + // update the stream item with the new album art. + MediaDescription currentDescription = mCurrentMetadata == null ? + null : mCurrentMetadata.getDescription(); + + if (!MediaUtils.isSameMediaDescription(metadata.getDescription(), currentDescription)) { + Message msg = + mMediaStatusUpdateHandler.obtainMessage(MSG_UPDATE_METADATA, metadata); + // Remove obsolete notifications in the queue. + mMediaStatusUpdateHandler.removeMessages(MSG_UPDATE_METADATA); + mMediaStatusUpdateHandler.sendMessage(msg); + } else { + Bitmap newBitmap = metadata.getDescription().getIconBitmap(); + if (newBitmap == null) { + return; + } + if (newBitmap.sameAs(mMediaStatusUpdateHandler.getCurrentIcon())) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Received duplicate metadata, ignoring..."); + } + } else { + // same metadata, but new album art + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Received metadata with new album art"); + } + Message msg = mMediaStatusUpdateHandler + .obtainMessage(MSG_NEW_ALBUM_ART_RECEIVED, newBitmap); + mMediaStatusUpdateHandler.removeMessages(MSG_NEW_ALBUM_ART_RECEIVED); + mMediaStatusUpdateHandler.sendMessage(msg); + } + } + } + + @Override + public void onSessionDestroyed() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Media session destroyed"); + } + mMonitorListener.removeMediaStreamCard(); + } + + private class BitmapCallback extends BitmapDownloader.BitmapCallback { + final private int mSeq; + + public BitmapCallback(int seq) { + mSeq = seq; + } + + @Override + public void onBitmapRetrieved(Bitmap bitmap) { + if (mMediaStatusUpdateHandler == null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "The callback comes after we finish"); + } + return; + } + Message msg = mMediaStatusUpdateHandler.obtainMessage(MSG_IMAGE_DOWNLOADED, + mSeq, 0, bitmap); + mMediaStatusUpdateHandler.sendMessage(msg); + } + } + + private class MediaStatusUpdateHandler extends Handler { + private int mSeq = 0; + private BitmapCallback mCallback; + private MediaMetadata mMetadata; + private String mTitle; + private String mSubtitle; + private Bitmap mIcon; + private Uri mIconUri; + private final BitmapDownloader mDownloader = BitmapDownloader.getInstance(mContext); + + private void extractMetadata(MediaMetadata metadata) { + if (metadata == mMetadata) { + // We are up to date and must return here, because we've already recycled the bitmap + // inside it. + return; + } + // keep a reference so we know which metadata we have stored. + mMetadata = metadata; + MediaDescription description = metadata.getDescription(); + mTitle = description.getTitle() == null ? null : description.getTitle().toString(); + mSubtitle = description.getSubtitle() == null ? + null : description.getSubtitle().toString(); + final Bitmap originalBitmap = getMetadataBitmap(metadata); + if (originalBitmap != null) { + mIcon = originalBitmap; + } else { + mIcon = null; + } + mIconUri = getMetadataIconUri(metadata); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Album Art Uri: " + mIconUri); + } + } + + private Uri getMetadataIconUri(MediaMetadata metadata) { + // Get the best Uri we can find + for (int i = 0; i < PREFERRED_URI_ORDER.length; i++) { + String iconUri = metadata.getString(PREFERRED_URI_ORDER[i]); + if (!TextUtils.isEmpty(iconUri)) { + return Uri.parse(iconUri); + } + } + return null; + } + + private Bitmap getMetadataBitmap(MediaMetadata metadata) { + // Get the best art bitmap we can find + for (int i = 0; i < PREFERRED_BITMAP_ORDER.length; i++) { + Bitmap bitmap = metadata.getBitmap(PREFERRED_BITMAP_ORDER[i]); + if (bitmap != null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Retrieved bitmap type: " + PREFERRED_BITMAP_ORDER[i] + + " w: " + bitmap.getWidth() + + " h: " + bitmap.getHeight()); + } + return bitmap; + } + } + return null; + } + + public Bitmap getCurrentIcon() { + return mIcon; + } + + @Override + public void handleMessage(Message msg) { + MediaAppInfo mediaAppInfo = mCurrentMediaAppInfo; + int color = mediaAppInfo.getMediaClientAccentColor(); + String appName = mediaAppInfo.getAppName(); + switch (msg.what) { + case MSG_UPDATE_METADATA: + mSeq++; + MediaMetadata metadata = (MediaMetadata) msg.obj; + if (metadata == null) { + Log.w(TAG, "media metadata is null!"); + return; + } + extractMetadata(metadata); + if (mCallback != null) { + // it's ok to cancel a callback that has already been called, the downloader + // will just ignore the operation. + mDownloader.cancelDownload(mCallback); + mCallback = null; + } + if (mIcon != null) { + mMonitorListener.onMetadataChanged(mTitle, mSubtitle, mIcon, + color, appName); + } else if (mIconUri != null) { + mCallback = new BitmapCallback(mSeq); + mDownloader.getBitmap( + new BitmapWorkerOptions.Builder(mContext) + .resource(mIconUri).width(mIconSize) + .height(mIconSize).build(), mCallback); + } else { + mMonitorListener.onMetadataChanged(mTitle, mSubtitle, mIcon, + color, appName); + } + // Only set mCurrentMetadata after we have updated the listener (if the + // bitmap is downloaded asynchronously, that is fine too. The stream card will + // be posted, when image is downloaded.) + mCurrentMetadata = metadata; + break; + + case MSG_IMAGE_DOWNLOADED: + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Image downloaded..."); + } + int seq = msg.arg1; + Bitmap bitmap = (Bitmap) msg.obj; + if (seq == mSeq) { + mMonitorListener.onMetadataChanged(mTitle, mSubtitle, bitmap, color, appName); + } + break; + + case MSG_NEW_ALBUM_ART_RECEIVED: + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Received a new album art..."); + } + Bitmap newAlbumArt = (Bitmap) msg.obj; + mMonitorListener.onAlbumArtUpdated(newAlbumArt); + break; + default: + } + } + } +} |