diff options
-rw-r--r-- | AndroidManifest.xml | 4 | ||||
-rw-r--r-- | src/com/android/car/media/MediaActivity.java | 73 | ||||
-rw-r--r-- | src/com/android/car/media/MediaCarMenuCallbacks.java | 618 | ||||
-rw-r--r-- | src/com/android/car/media/MediaMenuBitmapDownloader.java | 157 | ||||
-rw-r--r-- | src/com/android/car/media/MediaPlaybackFragment.java | 28 | ||||
-rw-r--r-- | src/com/android/car/media/MediaPlaybackModel.java | 63 | ||||
-rw-r--r-- | src/com/android/car/media/MediaProxyActivity.java | 26 | ||||
-rw-r--r-- | src/com/android/car/media/drawer/MediaBrowserItemsFetcher.java | 169 | ||||
-rw-r--r-- | src/com/android/car/media/drawer/MediaDrawerAdapter.java | 76 | ||||
-rw-r--r-- | src/com/android/car/media/drawer/MediaDrawerController.java | 123 | ||||
-rw-r--r-- | src/com/android/car/media/drawer/MediaItemsFetcher.java | 104 | ||||
-rw-r--r-- | src/com/android/car/media/drawer/MediaQueueItemsFetcher.java | 142 |
12 files changed, 717 insertions, 866 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 7900c7f..2f2baa3 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -30,8 +30,8 @@ android:name="android.car.application" android:resource="@xml/automotive_app_desc" /> - <activity android:name=".MediaProxyActivity" - android:theme="@android:style/Theme.NoTitleBar" + <activity android:name=".MediaActivity" + android:theme="@style/CarDrawerActivityTheme" android:label="CarMediaApp" android:resizeableActivity="true" android:launchMode="singleTop"> diff --git a/src/com/android/car/media/MediaActivity.java b/src/com/android/car/media/MediaActivity.java index ab282dc..3ff1d93 100644 --- a/src/com/android/car/media/MediaActivity.java +++ b/src/com/android/car/media/MediaActivity.java @@ -16,21 +16,23 @@ package com.android.car.media; import android.content.ComponentName; -import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.os.Bundle; import android.provider.MediaStore; -import android.support.car.Car; -import android.support.car.app.menu.CarDrawerActivity; +import android.support.v4.app.Fragment; import android.util.Log; import android.util.Pair; import android.util.TypedValue; import android.view.View; +import com.android.car.app.CarDrawerActivity; +import com.android.car.app.CarDrawerAdapter; +import com.android.car.media.drawer.MediaDrawerController; + /** * This activity controls the UI of media. It also updates the connection status for the media app - * by broadcast. Drawer menu is controlled by {@link MediaCarMenuCallbacks}. + * by broadcast. Drawer menu is controlled by {@link MediaDrawerController}. */ public class MediaActivity extends CarDrawerActivity { private static final String ACTION_MEDIA_APP_STATE_CHANGE @@ -59,21 +61,17 @@ public class MediaActivity extends CarDrawerActivity { */ private boolean mContentFragmentChangeQueued; + private MediaDrawerController mDrawerController; private View mScrimView; private CrossfadeImageView mAlbumArtView; private MediaPlaybackFragment mMediaPlaybackFragment; - private MediaCarMenuCallbacks mMediaCarMenuCallbacks; - - public MediaActivity(Proxy proxy, Context context, Car car) { - super(proxy, context, car); - } @Override protected void onStart() { super.onStart(); Intent i = new Intent(ACTION_MEDIA_APP_STATE_CHANGE); i.putExtra(EXTRA_MEDIA_APP_FOREGROUND, true); - getContext().sendBroadcast(i); + sendBroadcast(i); mIsStarted = true; @@ -91,22 +89,21 @@ public class MediaActivity extends CarDrawerActivity { super.onStop(); Intent i = new Intent(ACTION_MEDIA_APP_STATE_CHANGE); i.putExtra(EXTRA_MEDIA_APP_FOREGROUND, false); - getContext().sendBroadcast(i); + sendBroadcast(i); mIsStarted = false; } @Override protected void onCreate(Bundle savedInstanceState) { + mDrawerController = new MediaDrawerController(this); super.onCreate(savedInstanceState); - setLightMode(); - mMediaCarMenuCallbacks = new MediaCarMenuCallbacks(this); - setCarMenuCallbacks(mMediaCarMenuCallbacks); - setContentView(R.layout.media_activity); + + setMainContent(R.layout.media_activity); mScrimView = findViewById(R.id.scrim); mAlbumArtView = (CrossfadeImageView) findViewById(R.id.album_art); - setBackgroundColor(getContext().getColor(R.color.music_default_artwork)); - MediaManager.getInstance(getContext()).addListener(mListener); + setBackgroundColor(getColor(R.color.music_default_artwork)); + MediaManager.getInstance(this).addListener(mListener); } @Override @@ -114,10 +111,15 @@ public class MediaActivity extends CarDrawerActivity { super.onDestroy(); // Send the broadcast to let the current connected media app know it is disconnected now. sendMediaConnectionStatusBroadcast( - MediaManager.getInstance(getContext()).getCurrentComponent(), + MediaManager.getInstance(this).getCurrentComponent(), MediaConstants.MEDIA_DISCONNECTED); - mMediaCarMenuCallbacks.cleanup(); - MediaManager.getInstance(getContext()).removeListener(mListener); + mDrawerController.cleanup(); + MediaManager.getInstance(this).removeListener(mListener); + } + + @Override + protected CarDrawerAdapter getRootAdapter() { + return mDrawerController.getRootAdapter(); } @Override @@ -149,9 +151,7 @@ public class MediaActivity extends CarDrawerActivity { } setIntent(intent); - if (isDrawerShowing()) { - closeDrawer(); - } + closeDrawer(); } @Override @@ -230,19 +230,19 @@ public class MediaActivity extends CarDrawerActivity { intent.getStringExtra(MediaManager.KEY_MEDIA_PACKAGE), intent.getStringExtra(MediaManager.KEY_MEDIA_CLASS) ); - MediaManager.getInstance(getContext()).setMediaClientComponent(component); + MediaManager.getInstance(this).setMediaClientComponent(component); } else { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Launching most recent / default component."); } // Set it to the default GPM component. - MediaManager.getInstance(getContext()).connectToMostRecentMediaComponent( - new CarClientServiceAdapter(getContext().getPackageManager())); + MediaManager.getInstance(this).connectToMostRecentMediaComponent( + new CarClientServiceAdapter(getPackageManager())); } if (isSearchIntent(intent)) { - MediaManager.getInstance(getContext()).processSearchIntent(intent); + MediaManager.getInstance(this).processSearchIntent(intent); setIntent(null); } } @@ -253,7 +253,7 @@ public class MediaActivity extends CarDrawerActivity { } private void sendMediaConnectionStatusBroadcast( - ComponentName componentName, @Car.ConnectionType String connectionStatus) { + ComponentName componentName, String connectionStatus) { // It will be no current component if no media app is chosen before. if (componentName == null) { return; @@ -262,10 +262,10 @@ public class MediaActivity extends CarDrawerActivity { Intent intent = new Intent(MediaConstants.ACTION_MEDIA_STATUS); intent.setPackage(componentName.getPackageName()); intent.putExtra(MediaConstants.MEDIA_CONNECTION_STATUS, connectionStatus); - getContext().sendBroadcast(intent); + sendBroadcast(intent); } - public void attachContentFragment() { + void attachContentFragment() { if (mMediaPlaybackFragment == null) { mMediaPlaybackFragment = new MediaPlaybackFragment(); } @@ -298,4 +298,15 @@ public class MediaActivity extends CarDrawerActivity { @Override public void onStatusMessageChanged(String msg) {} }; -} + + private void setContentFragment(Fragment fragment) { + getSupportFragmentManager().beginTransaction() + .replace(getContentContainerId(), fragment) + .commit(); + } + + + void showQueueInDrawer() { + mDrawerController.showQueueInDrawer(); + } +}
\ No newline at end of file diff --git a/src/com/android/car/media/MediaCarMenuCallbacks.java b/src/com/android/car/media/MediaCarMenuCallbacks.java deleted file mode 100644 index 26415ab..0000000 --- a/src/com/android/car/media/MediaCarMenuCallbacks.java +++ /dev/null @@ -1,618 +0,0 @@ -/* - * 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.media; - -import android.content.ComponentName; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.media.browse.MediaBrowser; -import android.media.session.MediaController; -import android.media.session.MediaSession; -import android.media.session.PlaybackState; -import android.os.Bundle; -import android.os.Handler; -import android.support.car.app.menu.CarMenu; -import android.support.car.app.menu.CarMenuCallbacks; -import android.support.car.app.menu.RootMenu; -import android.support.car.app.menu.compat.CarMenuConstantsComapt; -import android.text.TextUtils; -import android.util.Log; - -import java.util.ArrayList; -import java.util.List; - -/** - * Manages all data needed for media drawer menu. - */ -public class MediaCarMenuCallbacks extends CarMenuCallbacks { - - public static final String QUEUE_ROOT = "QUEUE_ROOT"; - - private static final String TAG = "GH.MediaMenuCallbacks"; - // MEDIA_APP_ROOT is used for onGetRoot() of MediaMenuCallbacks, which is called so early that - // MediaBrowser hasn't got the root already. So we return this default root first and store the - // real one in mRootId. - private static final String MEDIA_APP_ROOT = "MEDIA_APP_ROOT"; - private static final String EXTRA_ICON_SIZE = - "com.google.android.gms.car.media.BrowserIconSize"; - private static final String QUEUE_ITEM_PREFIX = "queue_item_prefix_"; - private static final String MEDIA_QUEUE_EMPTY_PLACEHOLDER = "media_queue_emtpy_placeholder"; - - private final MediaActivity mActivity; - private final Context mContext; - private final Handler mHandler; - private MediaBrowser mBrowser; - private MediaController mController; - private CarMenu mMenuResult; - private String mMediaId; - private String mRootId; - // The media id we want to subscribe but media browser is not connected at that time. - private String mPendingMediaId; - private long mActiveQueueItemId; - private boolean mLoadQueueMenuPending; - // Whether we add "Queue" as the last item in the main menu. - private boolean mIsQueueInMenu; - private List<MediaBrowser.MediaItem> mItems; - private LoadQueueBitmapRunnable mLoadQueueBitmapRunnable; - private LoadMenuBitmapRunnable mLoadMenuBitmapRunnable; - // The parent ID is set whenever there's a onChildrenLoaded request. - private UpdateMenuRunnable mUpdateMenuRunnable = new UpdateMenuRunnable(); - - public MediaCarMenuCallbacks(MediaActivity activity) { - mActivity = activity; - mContext = activity.getContext(); - mHandler = new Handler(); - MediaManager.getInstance(mContext).addListener(mListener); - } - - public void cleanup() { - MediaManager.getInstance(mContext).removeListener(mListener); - mHandler.removeCallbacksAndMessages(null); - if (mBrowser != null) { - if (mMediaId != null) { - mBrowser.unsubscribe(mMediaId); - mMediaId = null; - } - mBrowser.disconnect(); - mBrowser = null; - } - if (mController != null) { - mController.unregisterCallback(mControllerCallback); - mController = null; - } - } - - @Override - public RootMenu onGetRoot(Bundle hints) { - // Return the default fake root due to the real one maybe not ready at this time. - return new RootMenu(MEDIA_APP_ROOT); - } - - @Override - public void onLoadChildren(String parentId, CarMenu result) { - Log.d(TAG, "onLoadChildren " + parentId); - resetCarMenu(result); - if (QUEUE_ROOT.equals(parentId)) { - // If mBrowser is not connected now, we will load the menu later when it is connected. - if (mBrowser == null || !mBrowser.isConnected()) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "MediaBrowser is not connected while loading menu."); - } - mLoadQueueMenuPending = true; - return; - } - - // Unsubscribe the old id first, or else it will affect to subscribe the new one. - if (!TextUtils.isEmpty(mMediaId) && !QUEUE_ROOT.equals(mMediaId)) { - mBrowser.unsubscribe(mMediaId); - } - mMediaId = parentId; - - loadQueueMenu(); - } else { - // If mBrowser is not connected now, we will load the menu later when it is connected. - if (mBrowser == null || !mBrowser.isConnected()) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "MediaBrowser is not connected while loading menu."); - } - mPendingMediaId = parentId; - return; - } - - // Unsubscribe the old id first, or else it will affect to subscribe the new one. - if (!TextUtils.isEmpty(mMediaId) && !QUEUE_ROOT.equals(mMediaId)) { - mBrowser.unsubscribe(mMediaId); - } - // Replace the fake root id with the real one, then we can use it to subscribe. - if (parentId.equals(MEDIA_APP_ROOT)) { - mMediaId = mRootId; - } else { - mMediaId = parentId; - } - mBrowser.subscribe(mMediaId, mSubscriptionCallback); - } - } - - @Override - public void onItemClicked(String id) { - // We treat queue item specially because its id is different from the normal one. - if (id.startsWith(QUEUE_ITEM_PREFIX)) { - String index = id.substring(QUEUE_ITEM_PREFIX.length()); - mController.getTransportControls().skipToQueueItem(Long.valueOf(index)); - mActivity.closeDrawer(); - } else { - if (mItems == null) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Media menu is empty."); - } - return; - } - - for (MediaBrowser.MediaItem item : mItems) { - if (item.getMediaId().equals(id)) { - if (item.isPlayable()) { - if (mController != null) { - mController.getTransportControls().pause(); - mController.getTransportControls().playFromMediaId(item.getMediaId(), - item.getDescription().getExtras()); - } else { - Log.e(TAG, "MediaSession is destroyed."); - } - mActivity.closeDrawer(); - } - break; - } - } - } - } - - private void resetCarMenu(CarMenu result) { - // Stop loading previous menu due to we are under the new one now. - if (mMenuResult != null) { - if (mUpdateMenuRunnable != null) { - mHandler.removeCallbacks(mUpdateMenuRunnable); - // Spot fix. This runnable is being used in the subscription callbacks and is - // causing a crash. The lifecycle here is a little messed up and needs to be - // straightened out but for now just set it to a new object instead of setting - // it to null. - mUpdateMenuRunnable = new UpdateMenuRunnable(); - } - if (mLoadMenuBitmapRunnable != null) { - mHandler.removeCallbacks(mLoadMenuBitmapRunnable); - mLoadMenuBitmapRunnable = null; - } - if (mLoadQueueBitmapRunnable != null) { - mHandler.removeCallbacks(mLoadQueueBitmapRunnable); - mLoadQueueBitmapRunnable = null; - } - } - mMenuResult = result; - mMenuResult.detach(); - } - - private CarMenu.Item emptyQueueMenu() { - CarMenu.Builder builder = new CarMenu.Builder(MEDIA_QUEUE_EMPTY_PLACEHOLDER); - - final int iconColor = mContext.getResources().getColor(R.color.car_tint); - Drawable drawable = mContext.getResources().getDrawable(R.drawable.ic_list_view_disable); - drawable.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); - builder.setIconFromSnapshot(drawable); - builder.setIsEmptyPlaceHolder(true); - return builder.build(); - } - - private void loadQueueMenu() { - if (mMenuResult == null) { - Log.w(TAG, "CarMenu is null while loading queue menu."); - return; - } - - List<CarMenu.Item> menuItems = new ArrayList<>(); - if (mController == null) { - Log.w(TAG, "MediaController is null while loading queue menu."); - - // Add a icon for empty menu. - sendEmptyMenu(); - } else { - List<MediaSession.QueueItem> queue = mController.getQueue(); - mActiveQueueItemId = getActiveQueueItemId(); - boolean hasImages = false; - for (MediaSession.QueueItem item : queue) { - if ((item.getDescription().getIconUri() != null) - || (item.getDescription().getIconBitmap() != null)) { - hasImages = true; - break; - } - } - boolean activeQueueItemFound = false; - for (final MediaSession.QueueItem item : queue) { - // Only queue items following the active item are displayed in the menu. - if (item.getQueueId() == mActiveQueueItemId) { - activeQueueItemFound = true; - } - - if (activeQueueItemFound) { - CarMenu.Builder builder = - new CarMenu.Builder(QUEUE_ITEM_PREFIX + item.getQueueId()); - builder.setTitle(item.getDescription().getTitle().toString()) - .setText(item.getDescription().getSubtitle().toString()); - // Place empty bitmap as place holder first, we will load the bitamp later. - if (hasImages) { - builder.setIcon(null); - } - if (item.getQueueId() == mActiveQueueItemId) { - int primaryColor = - MediaManager.getInstance(mContext).getMediaClientPrimaryColor(); - Drawable drawable = - mContext.getResources().getDrawable(R.drawable.ic_music_active); - drawable.setColorFilter(primaryColor, PorterDuff.Mode.SRC_IN); - builder.setRightIconFromSnapshot(drawable); - } - menuItems.add(builder.build()); - } - } - - // If we have not found any items then set the menu to empty placeholder item. - if (menuItems.size() == 0) { - sendEmptyMenu(); - } else { - mMenuResult.sendResult(menuItems); - mMenuResult = null; - } - - if (hasImages) { - if (mLoadQueueBitmapRunnable != null) { - mHandler.removeCallbacks(mLoadQueueBitmapRunnable); - } - mLoadQueueBitmapRunnable = new LoadQueueBitmapRunnable(queue, QUEUE_ROOT); - mHandler.post(mLoadQueueBitmapRunnable); - } - } - } - - private void sendEmptyMenu() { - if (mMenuResult != null) { - List<CarMenu.Item> menuItems = new ArrayList<CarMenu.Item>(); - menuItems.add(emptyQueueMenu()); - mMenuResult.sendResult(menuItems); - mMenuResult = null; - } - } - - private boolean enableQueueItem(List<MediaSession.QueueItem> items) { - if (items == null || mController == null) { - return false; - } - - if (mIsQueueInMenu) { - // We already have a queue item; do nothing - return false; - } - if (TextUtils.isEmpty(mController.getQueueTitle())) { - // No queue title to show; do nothing - return false; - } - return true; - } - - private long getActiveQueueItemId() { - if (mController == null) { - return MediaSession.QueueItem.UNKNOWN_ID; - } - - PlaybackState playbackState = mController.getPlaybackState(); - if (playbackState != null) { - return playbackState.getActiveQueueItemId(); - } else { - return MediaSession.QueueItem.UNKNOWN_ID; - } - } - - private final MediaManager.Listener mListener = new MediaManager.Listener() { - - @Override - public void onMediaAppChanged(ComponentName componentName) { - mRootId = null; - if (mBrowser != null) { - // Unsubscribe the old id first, or else it will affect to subscribe the new one. - if (!TextUtils.isEmpty(mMediaId) && !QUEUE_ROOT.equals(mMediaId)) { - mBrowser.unsubscribe(mMediaId); - mMediaId = null; - } - mBrowser.disconnect(); - mBrowser = null; - } - Resources resources = mContext.getResources(); - Bundle extras = new Bundle(); - if (resources != null) { - extras.putInt(EXTRA_ICON_SIZE, - resources.getDimensionPixelSize(R.dimen.car_list_item_icon_size)); - } - mBrowser = new MediaBrowser(mContext, componentName, mConnectionCallbacks, extras); - if (mController != null) { - mController.unregisterCallback(mControllerCallback); - mController = null; - } - mBrowser.connect(); - // Only store MediaManager instance to a local variable when it is short lived. - MediaManager mediaManager = MediaManager.getInstance(mContext); - mActivity.setTitle(mediaManager.getMediaClientName().toString()); - mActivity.setScrimColor(mediaManager.getMediaClientPrimaryColorDark()); - mActivity.attachContentFragment(); - } - - @Override - public void onStatusMessageChanged(String msg) {} - }; - - private final MediaBrowser.ConnectionCallback mConnectionCallbacks = - new MediaBrowser.ConnectionCallback() { - - @Override - public void onConnected() { - // Get the real root and will replace it with the default fake one which is set - // in onGetRoot(). - mRootId = mBrowser.getRoot(); - if (mPendingMediaId != null) { - mMediaId = mPendingMediaId.equals(MEDIA_APP_ROOT) ? mRootId : mPendingMediaId; - mPendingMediaId = null; - } else { - mMediaId = mRootId; - } - MediaSession.Token token = mBrowser.getSessionToken(); - if (token != null) { - mController = new MediaController(mContext, token); - mController.registerCallback(mControllerCallback); - } else { - // We will still be able to browse media content, but not able to play them. - Log.e(TAG, "Media session token is null for " - + MediaManager.getInstance(mContext).getMediaClientName()); - } - if (mLoadQueueMenuPending) { - mLoadQueueMenuPending = false; - loadQueueMenu(); - } else { - mBrowser.subscribe(mMediaId, mSubscriptionCallback); - } - } - - @Override - public void onConnectionSuspended() { - Log.w(TAG, "Media browser service connection suspended. Waiting to be" - + " reconnected...."); - } - - @Override - public void onConnectionFailed() { - Log.e(TAG, "Media browser service connection FAILED!"); - sendEmptyMenu(); - // disconnect anyway to make sure we get into a sanity state - mBrowser.disconnect(); - mBrowser = null; - } - }; - - private final MediaController.Callback mControllerCallback = new MediaController.Callback() { - - @Override - public void onSessionDestroyed() { - Log.e(TAG, "Media session is destroyed"); - sendEmptyMenu(); - if (mController != null) { - mController.unregisterCallback(mControllerCallback); - } - mController = null; - } - - @Override - public void onPlaybackStateChanged(PlaybackState state) { - long activeQueueItemId = getActiveQueueItemId(); - if (mActiveQueueItemId != activeQueueItemId) { - if (mMediaId == QUEUE_ROOT) { - // After this call, the whole queue menu will be refreshed. - notifyChildrenChanged(QUEUE_ROOT); - } - mActiveQueueItemId = activeQueueItemId; - } - } - - @Override - public void onQueueChanged(List<MediaSession.QueueItem> queue) { - if (mMediaId == mRootId && enableQueueItem(queue)) { - notifyChildrenChanged(MEDIA_APP_ROOT); - } - } - }; - - private final MediaBrowser.SubscriptionCallback mSubscriptionCallback = - new MediaBrowser.SubscriptionCallback() { - - @Override - public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children) { - Log.d(TAG, "onChildrenLoaded" + parentId); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Loaded " + children.size() + " children."); - for (MediaBrowser.MediaItem item : children) { - Log.d(TAG, "\t" + item.getDescription().getTitle()); - } - } - - mIsQueueInMenu = false; - if (mController == null) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "MediaController is null in SubscriptionCallback."); - } - sendEmptyMenu(); - // the session has been destroyed or we have moved to another facet. - return; - } - - mItems = new ArrayList<>(children); - mHandler.removeCallbacks(mUpdateMenuRunnable); - mUpdateMenuRunnable.setParentId(parentId); - mHandler.post(mUpdateMenuRunnable); - } - - @Override - public void onError(String mediaId) { - Log.e(TAG, "onError getting items for " + mediaId); - sendEmptyMenu(); - } - }; - - private class UpdateMenuRunnable implements Runnable { - private String mParentId; - - void setParentId(String parentId) { - mParentId = parentId; - } - - @Override - public void run() { - if (mMenuResult == null) { - Log.e(TAG, "CarMenu is null while update menu, notify change instead."); - notifyChildrenChanged(mParentId); - return; - } - - if (mItems == null) { - throw new IllegalArgumentException( - "You must supply CarMenu with a list of MediaItems."); - } - - boolean hasImages = false; - for (MediaBrowser.MediaItem item : mItems) { - if ((item.getDescription().getIconUri() != null) - || (item.getDescription().getIconBitmap() != null)) { - hasImages = true; - break; - } - } - List<CarMenu.Item> menuItems = new ArrayList<>(); - for (MediaBrowser.MediaItem item : mItems) { - menuItems.add(convertMediaItemToMenuItem(item, hasImages)); - } - // If it is under root menu and play queue is not empty, add "Queue" item to the menu. - if (mMediaId.equals(mRootId) && mController != null) { - List<MediaSession.QueueItem> queue = mController.getQueue(); - if (queue != null && queue.size() > 0 - && !TextUtils.isEmpty(mController.getQueueTitle())) { - String queueTitle = mController.getQueueTitle().toString(); - menuItems.add(new CarMenu.Builder(QUEUE_ROOT).setTitle(queueTitle) - .setFlags(CarMenuConstantsComapt.MenuItemConstants.FLAG_BROWSABLE) - .build()); - mIsQueueInMenu = true; - } - } - if (menuItems.size() == 0) { - sendEmptyMenu(); - } else { - mMenuResult.sendResult(menuItems); - mMenuResult = null; - } - - if (hasImages) { - if (mLoadMenuBitmapRunnable != null) { - mHandler.removeCallbacks(mLoadMenuBitmapRunnable); - } - // Due to we return fake root id in onGetRoot(), when we call notifyChildChanged() - // we still need to use the fake root id instead of the real one. - if (mMediaId.equals(mRootId)) { - mLoadMenuBitmapRunnable = new LoadMenuBitmapRunnable(mItems, MEDIA_APP_ROOT); - } else { - mLoadMenuBitmapRunnable = new LoadMenuBitmapRunnable(mItems, mMediaId); - } - mHandler.post(mLoadMenuBitmapRunnable); - } - } - - /** - * Returns CarMenu.Item which is used in rendering menu. - * - * @param item MediaItem which has all info to render menu. - * @param hasImages Whether the menu item has image or not. - * @return menu item. - */ - private CarMenu.Item convertMediaItemToMenuItem(MediaBrowser.MediaItem item, - boolean hasImages) { - CarMenu.Builder builder = new CarMenu.Builder(item.getMediaId()); - CharSequence title = item.getDescription().getTitle(); - if (title != null) { - builder.setTitle(title.toString()); - } - CharSequence subTitle = item.getDescription().getSubtitle(); - if (subTitle != null) { - builder.setText(subTitle.toString()); - } - if (item.isBrowsable()) { - builder.setFlags(CarMenuConstantsComapt.MenuItemConstants.FLAG_BROWSABLE); - } - // Place empty bitmap as place holder first, we will load the bitamp later. - if (hasImages) { - builder.setIcon(null); - } - return builder.build(); - } - } - - private class LoadQueueBitmapRunnable implements Runnable { - private final List<MediaSession.QueueItem> mQueue; - private final String mParentId; - - public LoadQueueBitmapRunnable(List<MediaSession.QueueItem> queue, String parentId) { - mQueue = queue; - mParentId = parentId; - } - - @Override - public void run() { - boolean activeQueueItemFound = false; - for (MediaSession.QueueItem item : mQueue) { - if (item.getQueueId() == mActiveQueueItemId) { - activeQueueItemFound = true; - } - - if (activeQueueItemFound) { - MediaMenuBitmapDownloader downloader = new MediaMenuBitmapDownloader(mContext, - MediaCarMenuCallbacks.this, mParentId, - QUEUE_ITEM_PREFIX + item.getQueueId(), mHandler); - downloader.setMenuBitmap(item.getDescription()); - } - } - } - } - - private class LoadMenuBitmapRunnable implements Runnable { - private List<MediaBrowser.MediaItem> mItemList; - private String mParentId; - - public LoadMenuBitmapRunnable(List<MediaBrowser.MediaItem> itemList, String parentId) { - mItemList = itemList; - mParentId = parentId; - } - - @Override - public void run() { - for (MediaBrowser.MediaItem item : mItemList) { - MediaMenuBitmapDownloader downloader = new MediaMenuBitmapDownloader(mContext, - MediaCarMenuCallbacks.this, mParentId, item.getMediaId(), mHandler); - downloader.setMenuBitmap(item.getDescription()); - } - } - } -} diff --git a/src/com/android/car/media/MediaMenuBitmapDownloader.java b/src/com/android/car/media/MediaMenuBitmapDownloader.java deleted file mode 100644 index ea8e370..0000000 --- a/src/com/android/car/media/MediaMenuBitmapDownloader.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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.media; - -import android.content.Context; -import android.graphics.Bitmap; -import android.media.MediaDescription; -import android.net.Uri; -import android.os.Handler; -import android.support.car.app.menu.CarMenu; -import android.util.Log; -import com.android.car.apps.common.BitmapDownloader; -import com.android.car.apps.common.BitmapWorkerOptions; -import com.android.car.apps.common.UriUtils; - -import java.lang.ref.WeakReference; - -/** - * Download the icon for car menu item. Once it is done, it will update the icon by calling - * CarMenuCallbacks.notifyChildChanged(), which is needed to be called after CarMenu.sendResult(). - */ -public class MediaMenuBitmapDownloader { - private static final String TAG = "GH.MBDownloader"; - private static final int MAX_ALBUM_ART_DOWNLOAD_RETRIES = 10; - private static final long ALBUM_ART_DOWNLOAD_RETRY_INTERVAL_MS = 1000; - - private final WeakReference<Context> mContext; - private final MediaCarMenuCallbacks mCallback; - private final String mParentId; - private final String mChildId; - private final Handler mHandler; - private BitmapDownloadRunnable mBitmapDownloadRunnable; - - public MediaMenuBitmapDownloader(Context context, MediaCarMenuCallbacks callback, - String parentId, String childId, Handler handler) { - mContext = new WeakReference<>(context); - mCallback = callback; - mParentId = parentId; - mChildId = childId; - mHandler = handler; - } - - public void setMenuBitmap(MediaDescription description) { - if (description == null) { - Log.w(TAG, "null media descriptor"); - return; - } - - if (mBitmapDownloadRunnable != null) { - mHandler.removeCallbacks(mBitmapDownloadRunnable); - mBitmapDownloadRunnable.cancelDownload(); - mBitmapDownloadRunnable = null; - } - - Bitmap bitmap = description.getIconBitmap(); - Uri iconUri = description.getIconUri(); - if (bitmap == null && iconUri == null) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "no bitmap or icon uri found"); - } - } else if (bitmap != null) { - updateIcon(bitmap); - } else { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "downloading bitmap " + iconUri); - } - mBitmapDownloadRunnable = new BitmapDownloadRunnable(iconUri); - mHandler.post(mBitmapDownloadRunnable); - } - } - - private void updateIcon(Bitmap bitmap) { - mCallback.notifyChildChanged(mParentId, - new CarMenu.Builder(mChildId).setIcon(bitmap).build()); - } - - private class BitmapDownloadRunnable implements Runnable { - private Uri mIconUri; - private int mRetries; - private BitmapDownloader.BitmapCallback mBitmapCallback; - - public BitmapDownloadRunnable(Uri icon_uri) { - mIconUri = icon_uri; - mRetries = 0; - } - - public void cancelDownload() { - if (mBitmapCallback != null) { - Context context = mContext.get(); - if (context == null) { - return; - } - - BitmapDownloader.getInstance(context).cancelDownload(mBitmapCallback); - } - } - - @Override - public void run() { - mBitmapCallback = new BitmapDownloader.BitmapCallback() { - @Override - public void onBitmapRetrieved(Bitmap bitmap) { - if (bitmap == null) { - if (++mRetries <= MAX_ALBUM_ART_DOWNLOAD_RETRIES) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "retrying after failing to download bitmap " - + mIconUri.toString()); - } - mHandler.postDelayed(BitmapDownloadRunnable.this, - ALBUM_ART_DOWNLOAD_RETRY_INTERVAL_MS); - } - } else { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "downloaded bitmap " + mIconUri.toString() + " retries:" - + mRetries); - } - updateIcon(bitmap); - } - } - }; - - Context context = mContext.get(); - if (context == null) { - return; - } - - int bitmapSize = - context.getResources().getDimensionPixelSize(R.dimen.car_list_item_icon_size); - BitmapDownloader.getInstance(context) - .getBitmap( - new BitmapWorkerOptions.Builder(context).resource(mIconUri) - .height(bitmapSize) - .width(bitmapSize) - // We don't want to cache android resources as they are needed - // to be refreshed after configuration changes. - .cacheFlag(UriUtils.isAndroidResourceUri(mIconUri) - ? (BitmapWorkerOptions.CACHE_FLAG_DISK_DISABLED - | BitmapWorkerOptions.CACHE_FLAG_MEM_DISABLED) - : 0) - .build(), - mBitmapCallback); - } - } -} diff --git a/src/com/android/car/media/MediaPlaybackFragment.java b/src/com/android/car/media/MediaPlaybackFragment.java index 04bd846..8bd44f1 100644 --- a/src/com/android/car/media/MediaPlaybackFragment.java +++ b/src/com/android/car/media/MediaPlaybackFragment.java @@ -144,11 +144,10 @@ public class MediaPlaybackFragment extends Fragment implements MediaPlaybackMode mActivity = (MediaActivity) getHost(); mShowTitleDelayMs = mActivity.getResources().getInteger(R.integer.new_album_art_fade_in_offset); - mMediaPlaybackModel = - new MediaPlaybackModel(mActivity.getContext(), null /* browserExtras */); + mMediaPlaybackModel = new MediaPlaybackModel(mActivity, null /* browserExtras */); mMediaPlaybackModel.addListener(this); - mTelephonyManager = (TelephonyManager) mActivity.getContext() - .getSystemService(Context.TELEPHONY_SERVICE); + mTelephonyManager = + (TelephonyManager) mActivity.getSystemService(Context.TELEPHONY_SERVICE); } @Override @@ -156,7 +155,6 @@ public class MediaPlaybackFragment extends Fragment implements MediaPlaybackMode super.onDestroy(); mCurrentView = null; mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); - mMediaPlaybackModel.onDestroy(); mMediaPlaybackModel = null; mActivity = null; // Calling this with null will clear queue of callbacks and message. @@ -237,7 +235,7 @@ public class MediaPlaybackFragment extends Fragment implements MediaPlaybackMode @Override public void onPause() { super.onPause(); - mMediaPlaybackModel.onPause(); + mMediaPlaybackModel.stop(); mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); } @@ -252,7 +250,7 @@ public class MediaPlaybackFragment extends Fragment implements MediaPlaybackMode @Override public void onResume() { super.onResume(); - mMediaPlaybackModel.onResume(); + mMediaPlaybackModel.start(); // Note: at registration, TelephonyManager will invoke the callback with the current state. mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); } @@ -271,8 +269,7 @@ public class MediaPlaybackFragment extends Fragment implements MediaPlaybackMode int overflowViewColor = mMediaPlaybackModel.getPrimaryColorDark(); mOverflowView.getBackground().setColorFilter(overflowViewColor, PorterDuff.Mode.SRC_IN); // Tint the overflow actions light or dark depending on contrast. - int overflowTintColor = ColorChecker.getTintColor( - mActivity.getContext(), overflowViewColor); + int overflowTintColor = ColorChecker.getTintColor(mActivity, overflowViewColor); for (ImageView v : mCustomActionButtons) { v.setColorFilter(overflowTintColor, PorterDuff.Mode.SRC_IN); } @@ -335,7 +332,7 @@ public class MediaPlaybackFragment extends Fragment implements MediaPlaybackMode } showInitialNoContentView(state.getErrorMessage() != null ? state.getErrorMessage().toString() : - mActivity.getContext().getString(R.string.unknown_error), true); + mActivity.getString(R.string.unknown_error), true); return; } @@ -400,7 +397,6 @@ public class MediaPlaybackFragment extends Fragment implements MediaPlaybackMode icon == null || mReturnFromOnStop ? 0 : mShowTitleDelayMs); } Uri iconUri = getMetadataIconUri(metadata); - Context context = mActivity.getContext(); if (icon != null) { Bitmap scaledIcon = cropAlbumArt(icon); if (scaledIcon != icon && !icon.isRecycled()) { @@ -412,7 +408,7 @@ public class MediaPlaybackFragment extends Fragment implements MediaPlaybackMode mActivity.setBackgroundBitmap(scaledIcon, !mReturnFromOnStop /* showAnimation */); } else if (iconUri != null) { if (mDownloader == null) { - mDownloader = new BitmapDownloader(context); + mDownloader = new BitmapDownloader(mActivity); } final int flags = BitmapWorkerOptions.CACHE_FLAG_DISK_DISABLED | BitmapWorkerOptions.CACHE_FLAG_MEM_DISABLED; @@ -420,7 +416,7 @@ public class MediaPlaybackFragment extends Fragment implements MediaPlaybackMode Log.v(TAG, "Album art size " + mAlbumArtWidth + "x" + mAlbumArtHeight); } - mDownloader.getBitmap(new BitmapWorkerOptions.Builder(context).resource(iconUri) + mDownloader.getBitmap(new BitmapWorkerOptions.Builder(mActivity).resource(iconUri) .height(mAlbumArtHeight).width(mAlbumArtWidth).cacheFlag(flags).build(), new BitmapDownloader.BitmapCallback() { @Override @@ -505,7 +501,7 @@ public class MediaPlaybackFragment extends Fragment implements MediaPlaybackMode } }); - int tint = ColorChecker.getTintColor(mActivity.getContext(), + int tint = ColorChecker.getTintColor(mActivity, mMediaPlaybackModel.getPrimaryColorDark()); mSeekBar.getProgressDrawable().setColorFilter(tint, PorterDuff.Mode.SRC_IN); } else { @@ -870,9 +866,7 @@ public class MediaPlaybackFragment extends Fragment implements MediaPlaybackMode } else { switch (v.getId()) { case R.id.play_queue: - CharSequence title = mMediaPlaybackModel.getQueueTitle(); - mActivity.showMenu(MediaCarMenuCallbacks.QUEUE_ROOT, title.toString()); - mActivity.openDrawer(); + mActivity.showQueueInDrawer(); break; case R.id.prev: transportControls.skipToPrevious(); diff --git a/src/com/android/car/media/MediaPlaybackModel.java b/src/com/android/car/media/MediaPlaybackModel.java index 6f234e2..03a8817 100644 --- a/src/com/android/car/media/MediaPlaybackModel.java +++ b/src/com/android/car/media/MediaPlaybackModel.java @@ -46,7 +46,7 @@ import java.util.function.Consumer; * main thread. Intended to provide a much more usable model interface to UI code. */ public class MediaPlaybackModel { - private static final String TAG = "GH.MediaPlaybackModel"; + private static final String TAG = "MediaPlaybackModel"; private final Context mContext; private final Bundle mBrowserExtras; @@ -55,6 +55,7 @@ public class MediaPlaybackModel { private Handler mHandler; private MediaController mController; private MediaBrowser mBrowser; + private int mPrimaryColor; private int mPrimaryColorDark; private int mAccentColor; private ComponentName mCurrentComponentName; @@ -94,6 +95,29 @@ public class MediaPlaybackModel { void onSessionDestroyed(CharSequence destroyedMediaClientName); } + /** Convenient Listener base class for extension */ + public static abstract class AbstractListener implements Listener { + @Override + public void onMediaAppChanged(@Nullable ComponentName currentName, + @Nullable ComponentName newName) {} + @Override + public void onMediaAppStatusMessageChanged(@Nullable String message) {} + @Override + public void onMediaConnected() {} + @Override + public void onMediaConnectionSuspended() {} + @Override + public void onMediaConnectionFailed(CharSequence failedMediaClientName) {} + @Override + public void onPlaybackStateChanged(@Nullable PlaybackState state) {} + @Override + public void onMetadataChanged(@Nullable MediaMetadata metadata) {} + @Override + public void onQueueChanged(List<MediaSession.QueueItem> queue) {} + @Override + public void onSessionDestroyed(CharSequence destroyedMediaClientName) {} + } + public MediaPlaybackModel(Context context, Bundle browserExtras) { mContext = context; mBrowserExtras = browserExtras; @@ -101,13 +125,13 @@ public class MediaPlaybackModel { } @MainThread - public void onDestroy() { + public void start() { Assert.isMainThread(); - mHandler = null; + MediaManager.getInstance(mContext).addListener(mMediaManagerListener); } @MainThread - public void onPause() { + public void stop() { Assert.isMainThread(); MediaManager.getInstance(mContext).removeListener(mMediaManagerListener); if (mBrowser != null) { @@ -126,12 +150,6 @@ public class MediaPlaybackModel { } @MainThread - public void onResume() { - Assert.isMainThread(); - MediaManager.getInstance(mContext).addListener(mMediaManagerListener); - } - - @MainThread public void addListener(MediaPlaybackModel.Listener listener) { Assert.isMainThread(); mListeners.add(listener); @@ -146,8 +164,11 @@ public class MediaPlaybackModel { @MainThread private void notifyListeners(Consumer<Listener> callback) { Assert.isMainThread(); + // Clone mListeners in case any of the callbacks made triggers a listener to be added or + // removed to/from mListeners. + List<Listener> listenersCopy = new LinkedList<>(mListeners); // Invokes callback.accept(listener) for each listener. - mListeners.forEach(callback); + listenersCopy.forEach(callback); } @MainThread @@ -157,6 +178,12 @@ public class MediaPlaybackModel { } @MainThread + public int getPrimaryColor() { + Assert.isMainThread(); + return mPrimaryColor; + } + + @MainThread public int getAccentColor() { Assert.isMainThread(); return mAccentColor; @@ -227,6 +254,12 @@ public class MediaPlaybackModel { } @MainThread + public MediaBrowser getMediaBrowser() { + Assert.isMainThread(); + return mBrowser; + } + + @MainThread public MediaController.TransportControls getTransportControls() { Assert.isMainThread(); if (mController == null) { @@ -266,10 +299,10 @@ public class MediaPlaybackModel { mBrowser.connect(); // reset the colors and views if we switch to another app. - mAccentColor = MediaManager.getInstance(mContext) - .getMediaClientAccentColor(); - mPrimaryColorDark = MediaManager.getInstance(mContext) - .getMediaClientPrimaryColorDark(); + MediaManager manager = MediaManager.getInstance(mContext); + mPrimaryColor = manager.getMediaClientPrimaryColor(); + mAccentColor = manager.getMediaClientAccentColor(); + mPrimaryColorDark = manager.getMediaClientPrimaryColorDark(); final ComponentName currentName = mCurrentComponentName; notifyListeners((listener) -> listener.onMediaAppChanged(currentName, name)); diff --git a/src/com/android/car/media/MediaProxyActivity.java b/src/com/android/car/media/MediaProxyActivity.java deleted file mode 100644 index c1950db..0000000 --- a/src/com/android/car/media/MediaProxyActivity.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.media; - -import android.support.car.app.CarProxyActivity; - - -public class MediaProxyActivity extends CarProxyActivity { - - public MediaProxyActivity() { - super(MediaActivity.class); - } -} diff --git a/src/com/android/car/media/drawer/MediaBrowserItemsFetcher.java b/src/com/android/car/media/drawer/MediaBrowserItemsFetcher.java new file mode 100644 index 0000000..80bc5d7 --- /dev/null +++ b/src/com/android/car/media/drawer/MediaBrowserItemsFetcher.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2017 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.media.drawer; + +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.media.browse.MediaBrowser; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.util.Log; + +import com.android.car.app.CarDrawerActivity; +import com.android.car.app.DrawerItemViewHolder; +import com.android.car.media.MediaPlaybackModel; +import com.android.car.media.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link MediaItemsFetcher} implementation that fetches items from a specific {@link MediaBrowser} + * node. + * <p> + * It optionally supports surfacing the Media app's queue as the last item. + */ +class MediaBrowserItemsFetcher implements MediaItemsFetcher { + private static final String TAG = "Media.BrowserFetcher"; + + private final CarDrawerActivity mActivity; + private final MediaPlaybackModel mMediaPlaybackModel; + private final String mMediaId; + private final boolean mShowQueueItem; + private ItemsUpdatedCallback mCallback; + private List<MediaBrowser.MediaItem> mItems = new ArrayList<>(); + private boolean mQueueAvailable; + + MediaBrowserItemsFetcher(CarDrawerActivity activity, MediaPlaybackModel model, String mediaId, + boolean showQueueItem) { + mActivity = activity; + mMediaPlaybackModel = model; + mMediaId = mediaId; + mShowQueueItem = showQueueItem; + } + + @Override + public void start(ItemsUpdatedCallback callback) { + mCallback = callback; + updateQueueAvailability(); + mMediaPlaybackModel.getMediaBrowser().subscribe(mMediaId, mSubscriptionCallback); + mMediaPlaybackModel.addListener(mModelListener); + } + + private final MediaPlaybackModel.Listener mModelListener = + new MediaPlaybackModel.AbstractListener() { + @Override + public void onQueueChanged(List<MediaSession.QueueItem> queue) { + updateQueueAvailability(); + } + @Override + public void onSessionDestroyed(CharSequence destroyedMediaClientName) { + updateQueueAvailability(); + } + }; + + private final MediaBrowser.SubscriptionCallback mSubscriptionCallback = + new MediaBrowser.SubscriptionCallback() { + @Override + public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children) { + mItems.clear(); + mItems.addAll(children); + mCallback.onItemsUpdated(); + } + + @Override + public void onError(String parentId) { + Log.e(TAG, "Error loading children of: " + mMediaId); + mItems.clear(); + mCallback.onItemsUpdated(); + } + }; + + private void updateQueueAvailability() { + if (mShowQueueItem && !mMediaPlaybackModel.getQueue().isEmpty()) { + mQueueAvailable = true; + } + } + + @Override + public int getItemCount() { + int size = mItems.size(); + if (mQueueAvailable) { + size++; + } + return size; + } + + @Override + public void populateViewHolder(DrawerItemViewHolder holder, int position) { + if (mQueueAvailable && position == mItems.size()) { + holder.getTitle().setText(mMediaPlaybackModel.getQueueTitle()); + return; + } + MediaBrowser.MediaItem item = mItems.get(position); + MediaItemsFetcher.populateViewHolderFrom(holder, item.getDescription()); + + // TODO(sriniv): Once we use smallLayout, text and rightIcon fields may be unavailable. + // Related to b/36573125. + if (item.isBrowsable()) { + int iconColor = mActivity.getColor(R.color.car_tint); + Drawable drawable = mActivity.getDrawable(R.drawable.ic_chevron_right); + drawable.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); + holder.getRightIcon().setImageDrawable(drawable); + } else { + holder.getRightIcon().setImageDrawable(null); + } + } + + @Override + public void onItemClick(int position) { + if (mQueueAvailable && position == mItems.size()) { + MediaItemsFetcher fetcher = new MediaQueueItemsFetcher(mActivity, mMediaPlaybackModel); + setupAdapterAndSwitch(fetcher, mMediaPlaybackModel.getQueueTitle()); + return; + } + + MediaBrowser.MediaItem item = mItems.get(position); + if (item.isBrowsable()) { + MediaItemsFetcher fetcher = new MediaBrowserItemsFetcher( + mActivity, mMediaPlaybackModel, item.getMediaId(), false /* showQueueItem */); + setupAdapterAndSwitch(fetcher, item.getDescription().getTitle()); + } else if (item.isPlayable()) { + MediaController.TransportControls controls = mMediaPlaybackModel.getTransportControls(); + if (controls != null) { + controls.pause(); + controls.playFromMediaId(item.getMediaId(), item.getDescription().getExtras()); + } + mActivity.closeDrawer(); + } else { + Log.w(TAG, "Unknown item type; don't know how to handle!"); + } + } + + private void setupAdapterAndSwitch(MediaItemsFetcher fetcher, CharSequence title) { + MediaDrawerAdapter subAdapter = new MediaDrawerAdapter(mActivity, false /* smallLayout */); + subAdapter.setFetcher(fetcher); + subAdapter.setTitle(title); + mActivity.switchToAdapter(subAdapter); + } + + @Override + public void cleanup() { + mMediaPlaybackModel.removeListener(mModelListener); + mMediaPlaybackModel.getMediaBrowser().unsubscribe(mMediaId); + mCallback = null; + } +}
\ No newline at end of file diff --git a/src/com/android/car/media/drawer/MediaDrawerAdapter.java b/src/com/android/car/media/drawer/MediaDrawerAdapter.java new file mode 100644 index 0000000..dc483a2 --- /dev/null +++ b/src/com/android/car/media/drawer/MediaDrawerAdapter.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2017 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.media.drawer; + +import com.android.car.app.CarDrawerActivity; +import com.android.car.app.CarDrawerAdapter; +import com.android.car.app.DrawerItemViewHolder; + +/** + * Subclass of CarDrawerAdapter used by the Media app. + * <p> + * This adapter delegates actual fetching of items (and other operations) to a + * {@link MediaItemsFetcher}. The current fetcher being used can be updated at runtime. + */ +class MediaDrawerAdapter extends CarDrawerAdapter { + private final CarDrawerActivity mActivity; + private MediaItemsFetcher mCurrentFetcher; + + MediaDrawerAdapter(CarDrawerActivity activity, boolean useSmallLayout) { + super(activity, true /* showDisabledListOnEmpty */, useSmallLayout); + mActivity = activity; + } + + /** + * Switch the {@link MediaItemsFetcher} being used to fetch items. The new fetcher is kicked-off + * and the drawer's content's will be updated to show newly loaded items. Any old fetcher is + * cleaned up and released. + * + * @param fetcher New {@link MediaItemsFetcher} to use for display Drawer items. + */ + void setFetcher(MediaItemsFetcher fetcher) { + if (mCurrentFetcher != null) { + mCurrentFetcher.cleanup(); + } + mCurrentFetcher = fetcher; + mCurrentFetcher.start(() -> { + mActivity.showLoadingProgressBar(false); + notifyDataSetChanged(); + }); + // Initially there will be no items and we don't want to show empty-list indicator briefly + // until items are fetched. + mActivity.showLoadingProgressBar(true); + } + + @Override + protected int getActualItemCount() { + return mCurrentFetcher != null ? mCurrentFetcher.getItemCount() : 0; + } + + @Override + protected void populateViewHolder(DrawerItemViewHolder holder, int position) { + if (mCurrentFetcher != null) { + mCurrentFetcher.populateViewHolder(holder, position); + } + } + + @Override + public void onItemClick(int position) { + if (mCurrentFetcher != null) { + mCurrentFetcher.onItemClick(position); + } + } +} diff --git a/src/com/android/car/media/drawer/MediaDrawerController.java b/src/com/android/car/media/drawer/MediaDrawerController.java new file mode 100644 index 0000000..e1715bd --- /dev/null +++ b/src/com/android/car/media/drawer/MediaDrawerController.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2017 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.media.drawer; + +import android.content.ComponentName; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.widget.DrawerLayout; +import android.view.View; + +import com.android.car.app.CarDrawerActivity; +import com.android.car.app.CarDrawerAdapter; +import com.android.car.media.MediaManager; +import com.android.car.media.MediaPlaybackModel; +import com.android.car.media.R; + +/** + * Manages overall Drawer functionality. + * <p> + * Maintains separate MediaPlaybackModel for media browsing and control. Sets up root Drawer + * adapter with root of media-browse tree (using MediaBrowserItemsFetcher). Supports switching the + * rootAdapter to show the queue-items (using MediaQueueItemsFetcher). + */ +public class MediaDrawerController { + private static final String TAG = "MediaDrawerController"; + private static final String EXTRA_ICON_SIZE = + "com.google.android.gms.car.media.BrowserIconSize"; + + private final CarDrawerActivity mActivity; + private final MediaPlaybackModel mMediaPlaybackModel; + private MediaDrawerAdapter mRootAdapter; + + public MediaDrawerController(CarDrawerActivity activity) { + mActivity = activity; + Bundle extras = new Bundle(); + extras.putInt(EXTRA_ICON_SIZE, + mActivity.getResources().getDimensionPixelSize(R.dimen.car_list_item_icon_size)); + mMediaPlaybackModel = new MediaPlaybackModel(mActivity, extras); + mMediaPlaybackModel.addListener(mModelListener); + + // TODO(sriniv): Needs smallLayout below. But breaks when showing queue items (b/36573125). + mRootAdapter = new MediaDrawerAdapter(mActivity, false /* useSmallLayout */); + // Start with a empty title since we depend on the mMediaManagerListener callback to + // know which app is being used and set the actual title there. + mRootAdapter.setTitle(""); + + // Kick off MediaBrowser/MediaController connection. + mMediaPlaybackModel.start(); + } + + public void cleanup() { + mRootAdapter.cleanup(); + mMediaPlaybackModel.stop(); + } + + /** + * @return Adapter to display root items of MediaBrowse tree. {@link #showQueueInDrawer()} can + * be used to display items from the queue. + */ + public CarDrawerAdapter getRootAdapter() { + return mRootAdapter; + } + + private MediaItemsFetcher createRootMediaItemsFetcher() { + return new MediaBrowserItemsFetcher(mActivity, mMediaPlaybackModel, + mMediaPlaybackModel.getMediaBrowser().getRoot(), true /* showQueueItem */); + } + + private final MediaPlaybackModel.Listener mModelListener = + new MediaPlaybackModel.AbstractListener() { + @Override + public void onMediaAppChanged(@Nullable ComponentName currentName, + @Nullable ComponentName newName) { + // Only store MediaManager instance to a local variable when it is short lived. + MediaManager mediaManager = MediaManager.getInstance(mActivity); + mRootAdapter.setTitle(mediaManager.getMediaClientName()); + } + + @Override + public void onMediaConnected() { + mRootAdapter.setFetcher(createRootMediaItemsFetcher()); + } + }; + + /** + * Switch the rootAdapter to show items from the currently playing Queue and open the drawer. + * When the drawer is closed, the adapter items are switched back to media-browse root. + */ + public void showQueueInDrawer() { + mRootAdapter.setFetcher(new MediaQueueItemsFetcher(mActivity, mMediaPlaybackModel)); + mRootAdapter.setTitle(mMediaPlaybackModel.getQueueTitle()); + mActivity.openDrawer(); + mActivity.addDrawerListener(new DrawerLayout.DrawerListener() { + @Override + public void onDrawerClosed(View drawerView) { + mRootAdapter.setFetcher(createRootMediaItemsFetcher()); + mActivity.removeDrawerListener(this); + mRootAdapter.setTitle( + MediaManager.getInstance(mActivity).getMediaClientName()); + } + + @Override + public void onDrawerSlide(View drawerView, float slideOffset) {} + @Override + public void onDrawerOpened(View drawerView) {} + @Override + public void onDrawerStateChanged(int newState) {} + }); + } +}
\ No newline at end of file diff --git a/src/com/android/car/media/drawer/MediaItemsFetcher.java b/src/com/android/car/media/drawer/MediaItemsFetcher.java new file mode 100644 index 0000000..a712f7b --- /dev/null +++ b/src/com/android/car/media/drawer/MediaItemsFetcher.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2017 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.media.drawer; + +import android.content.Context; +import android.graphics.Bitmap; +import android.media.MediaDescription; + +import com.android.car.app.DrawerItemViewHolder; +import com.android.car.apps.common.BitmapDownloader; +import com.android.car.apps.common.BitmapWorkerOptions; +import com.android.car.apps.common.UriUtils; +import com.android.car.media.R; + +/** + * Component that handles fetching of items for {@link MediaDrawerAdapter}. + * <p> + * It also handles ViewHolder population and item clicks. + */ +interface MediaItemsFetcher { + /** + * Used to inform owning {@link MediaDrawerAdapter} that items have changed. + */ + interface ItemsUpdatedCallback { + void onItemsUpdated(); + } + + /** + * Kick-off fetching/monitoring of items. + * + * @param callback Callback that is invoked when items are first loaded ar if they change + * subsequently. + */ + void start(ItemsUpdatedCallback callback); + + /** + * @return Number of items currently fetched. + */ + int getItemCount(); + + /** + * Used by owning {@link MediaDrawerAdapter} to populate views. + * + * @param holder View-holder to populate. + * @param position Item position. + */ + void populateViewHolder(DrawerItemViewHolder holder, int position); + + /** + * Used by owning {@link MediaDrawerAdapter} to handle clicks. + * + * @param position Item position. + */ + void onItemClick(int position); + + /** + * Used when this instance is going to be released. Subclasses should release resources. + */ + void cleanup(); + + /** + * Utility method to populate {@code holder} with details from {@code description}. It populates + * title, text and icon at most. + */ + static void populateViewHolderFrom(DrawerItemViewHolder holder, MediaDescription description) { + Context context = holder.itemView.getContext(); + // TODO(sriniv): Once we use smallLayout, text and rightIcon fields may be unavailable. + // Related to b/36573125. + holder.getTitle().setText(description.getTitle()); + holder.getText().setText(description.getSubtitle()); + Bitmap iconBitmap = description.getIconBitmap(); + holder.getIcon().setImageBitmap(iconBitmap); // Ok to set null here for clearing. + if (iconBitmap == null && description.getIconUri() != null) { + int bitmapSize = + context.getResources().getDimensionPixelSize(R.dimen.car_list_item_icon_size); + // We don't want to cache android resources as they are needed to be refreshed after + // configuration changes. + int cacheFlag = UriUtils.isAndroidResourceUri(description.getIconUri()) + ? (BitmapWorkerOptions.CACHE_FLAG_DISK_DISABLED + | BitmapWorkerOptions.CACHE_FLAG_MEM_DISABLED) + : 0; + BitmapWorkerOptions options = new BitmapWorkerOptions.Builder(context) + .resource(description.getIconUri()) + .height(bitmapSize) + .width(bitmapSize) + .cacheFlag(cacheFlag) + .build(); + BitmapDownloader.getInstance(context).loadBitmap(options, holder.getIcon()); + } + } +}
\ No newline at end of file diff --git a/src/com/android/car/media/drawer/MediaQueueItemsFetcher.java b/src/com/android/car/media/drawer/MediaQueueItemsFetcher.java new file mode 100644 index 0000000..d27d092 --- /dev/null +++ b/src/com/android/car/media/drawer/MediaQueueItemsFetcher.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2017 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.media.drawer; + +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.os.Handler; +import android.support.annotation.Nullable; + +import com.android.car.app.CarDrawerActivity; +import com.android.car.app.DrawerItemViewHolder; +import com.android.car.media.MediaPlaybackModel; +import com.android.car.media.R; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * {@link MediaItemsFetcher} implementation that fetches items from the {@link MediaController}'s + * currently playing queue. + */ +class MediaQueueItemsFetcher implements MediaItemsFetcher { + private static final String TAG = "MediaQueueItemsFetcher"; + + private final Handler mHandler = new Handler(); + private final CarDrawerActivity mActivity; + private MediaPlaybackModel mMediaPlaybackModel; + private ItemsUpdatedCallback mCallback; + private List<MediaSession.QueueItem> mItems = new ArrayList<>(); + + MediaQueueItemsFetcher(CarDrawerActivity activity, MediaPlaybackModel model) { + mActivity = activity; + mMediaPlaybackModel = model; + } + + @Override + public void start(ItemsUpdatedCallback callback) { + mCallback = callback; + if (mMediaPlaybackModel != null) { + mMediaPlaybackModel.addListener(listener); + updateItemsFrom(mMediaPlaybackModel.getQueue()); + } + // Inform client of current items. Invoke async to avoid re-entrancy issues. + mHandler.post(mCallback::onItemsUpdated); + } + + @Override + public int getItemCount() { + return mItems.size(); + } + + @Override + public void populateViewHolder(DrawerItemViewHolder holder, int position) { + MediaSession.QueueItem item = mItems.get(position); + MediaItemsFetcher.populateViewHolderFrom(holder, item.getDescription()); + + if (item.getQueueId() == getActiveQueueItemId()) { + int primaryColor = mMediaPlaybackModel.getPrimaryColor(); + Drawable drawable = + mActivity.getResources().getDrawable(R.drawable.ic_music_active); + drawable.setColorFilter(primaryColor, PorterDuff.Mode.SRC_IN); + holder.getRightIcon().setImageDrawable(drawable); + } else { + holder.getRightIcon().setImageBitmap(null); + } + } + + @Override + public void onItemClick(int position) { + MediaController.TransportControls controls = mMediaPlaybackModel.getTransportControls(); + if (controls != null) { + controls.skipToQueueItem(mItems.get(position).getQueueId()); + } + mActivity.closeDrawer(); + } + + @Override + public void cleanup() { + mMediaPlaybackModel.removeListener(listener); + } + + private void updateItemsFrom(List<MediaSession.QueueItem> queue) { + mItems.clear(); + // We only show items starting from active-item in the queue. + final long activeItemId = getActiveQueueItemId(); + boolean activeItemFound = false; + for (MediaSession.QueueItem item : queue) { + if (activeItemId == item.getQueueId()) { + activeItemFound = true; + } + if (activeItemFound) { + mItems.add(item); + } + } + } + + private long getActiveQueueItemId() { + if (mMediaPlaybackModel != null) { + PlaybackState playbackState = mMediaPlaybackModel.getPlaybackState(); + if (playbackState != null) { + return playbackState.getActiveQueueItemId(); + } + } + return MediaSession.QueueItem.UNKNOWN_ID; + } + + private final MediaPlaybackModel.Listener listener = new MediaPlaybackModel.AbstractListener() { + @Override + public void onQueueChanged(List<MediaSession.QueueItem> queue) { + updateItemsFrom(queue); + mCallback.onItemsUpdated(); + } + + @Override + public void onPlaybackStateChanged(@Nullable PlaybackState state) { + // Since active playing item may have changed, force re-draw of queue items. + mCallback.onItemsUpdated(); + } + + @Override + public void onSessionDestroyed(CharSequence destroyedMediaClientName) { + onQueueChanged(Collections.emptyList()); + } + }; +} |