blob: 26415ab94472f18eb3c2dc433b0f2df14fa39414 [file] [log] [blame]
/*
* 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());
}
}
}
}