blob: 28a901aabb41609198cc3613557cf96d0c25777d [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.bluetooth.avrcp;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.bluetooth.BluetoothAvrcp;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.media.session.MediaSession.QueueItem;
import android.media.MediaDescription;
import android.media.MediaMetadata;
import android.os.Bundle;
import android.util.Log;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.Utils;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;
/*************************************************************************************************
* Provides functionality required for Addressed Media Player, like Now Playing List related
* browsing commands, control commands to the current addressed player(playItem, play, pause, etc)
* Acts as an Interface to communicate with media controller APIs for NowPlayingItems.
************************************************************************************************/
public class AddressedMediaPlayer {
static private final String TAG = "AddressedMediaPlayer";
static private final Boolean DEBUG = false;
static private final long SINGLE_QID = 1;
static private final String UNKNOWN_TITLE = "(unknown)";
private AvrcpMediaRspInterface mMediaInterface;
private @NonNull List<MediaSession.QueueItem> mNowPlayingList;
private final List<MediaSession.QueueItem> mEmptyNowPlayingList;
private long mLastTrackIdSent;
public AddressedMediaPlayer(AvrcpMediaRspInterface mediaInterface) {
mEmptyNowPlayingList = new ArrayList<MediaSession.QueueItem>();
mNowPlayingList = mEmptyNowPlayingList;
mMediaInterface = mediaInterface;
mLastTrackIdSent = MediaSession.QueueItem.UNKNOWN_ID;
}
void cleanup() {
if (DEBUG) Log.v(TAG, "cleanup");
mNowPlayingList = mEmptyNowPlayingList;
mMediaInterface = null;
mLastTrackIdSent = MediaSession.QueueItem.UNKNOWN_ID;
}
/* get now playing list from addressed player */
void getFolderItemsNowPlaying(byte[] bdaddr, AvrcpCmd.FolderItemsCmd reqObj,
@Nullable MediaController mediaController) {
if (DEBUG) Log.v(TAG, "getFolderItemsNowPlaying");
if (mediaController == null) {
// No players (if a player exists, we would have selected it)
Log.e(TAG, "mediaController = null, sending no available players response");
mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_AVBL_PLAY, null);
return;
}
List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
getFolderItemsFilterAttr(bdaddr, reqObj, items, AvrcpConstants.BTRC_SCOPE_NOW_PLAYING,
reqObj.mStartItem, reqObj.mEndItem, mediaController);
}
/* get item attributes for item in now playing list */
void getItemAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd itemAttr,
@Nullable MediaController mediaController) {
int status = AvrcpConstants.RSP_NO_ERROR;
long mediaId = ByteBuffer.wrap(itemAttr.mUid).getLong();
List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
// NOTE: this is out-of-spec (AVRCP 1.6.1 sec 6.10.4.3, p90) but we answer it anyway
// because some CTs ask for it.
if (Arrays.equals(itemAttr.mUid, AvrcpConstants.TRACK_IS_SELECTED)) {
if (DEBUG) Log.d(TAG, "getItemAttr: Remote requests for now playing contents:");
// get the current playing metadata and send.
getItemAttrFilterAttr(bdaddr, itemAttr, getCurrentQueueItem(mediaController, mediaId),
mediaController);
return;
}
if (DEBUG) Log.d(TAG, "getItemAttr-UID: 0x" + Utils.byteArrayToString(itemAttr.mUid));
for (MediaSession.QueueItem item : items) {
if (item.getQueueId() == mediaId) {
getItemAttrFilterAttr(bdaddr, itemAttr, item, mediaController);
return;
}
}
// Couldn't find it, so the id is invalid
mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_INV_ITEM, null);
}
/* Refresh and get the queue of now playing.
*/
private @NonNull List<MediaSession.QueueItem> getNowPlayingList(
@Nullable MediaController mediaController) {
if (mediaController == null) return mEmptyNowPlayingList;
List<MediaSession.QueueItem> items = mediaController.getQueue();
if (items == mNowPlayingList) return mNowPlayingList;
if (items == null) {
Log.i(TAG, "null queue from " + mediaController.getPackageName()
+ ", constructing single-item list");
MediaMetadata metadata = mediaController.getMetadata();
// Because we are database-unaware, we can just number the item here whatever we want
// because they have to re-poll it every time.
MediaSession.QueueItem current = getCurrentQueueItem(mediaController, SINGLE_QID);
items = new ArrayList<MediaSession.QueueItem>();
items.add(current);
}
mNowPlayingList = items;
// TODO (jamuraa): test to see if the single-item queue is the same and don't send
if (mMediaInterface != null) {
mMediaInterface.nowPlayingChangedRsp(AvrcpConstants.NOTIFICATION_TYPE_CHANGED);
}
return items;
}
/* Constructs a queue item representing the current playing metadata from an
* active controller with queue id |qid|.
*/
private MediaSession.QueueItem getCurrentQueueItem(
@Nullable MediaController controller, long qid) {
if (controller == null) {
MediaDescription.Builder bob = new MediaDescription.Builder();
bob.setTitle(UNKNOWN_TITLE);
return new QueueItem(bob.build(), qid);
}
MediaMetadata metadata = controller.getMetadata();
if (metadata == null) {
Log.w(TAG, "Controller has no metadata!? Making an empty one");
metadata = (new MediaMetadata.Builder()).build();
}
MediaDescription.Builder bob = new MediaDescription.Builder();
MediaDescription desc = metadata.getDescription();
// set the simple ones that MediaMetadata builds for us
bob.setMediaId(desc.getMediaId());
bob.setTitle(desc.getTitle());
bob.setSubtitle(desc.getSubtitle());
bob.setDescription(desc.getDescription());
// fill the ones that we use later
bob.setExtras(fillBundle(metadata, desc.getExtras()));
// build queue item with the new metadata
desc = bob.build();
return new QueueItem(desc, qid);
}
private Bundle fillBundle(MediaMetadata metadata, Bundle currentExtras) {
if (metadata == null) {
return currentExtras;
}
Bundle bundle = currentExtras;
if (bundle == null) bundle = new Bundle();
String[] stringKeys = {MediaMetadata.METADATA_KEY_TITLE, MediaMetadata.METADATA_KEY_ARTIST,
MediaMetadata.METADATA_KEY_ALBUM, MediaMetadata.METADATA_KEY_GENRE};
for (String key : stringKeys) {
String current = bundle.getString(key);
if (current == null) bundle.putString(key, metadata.getString(key));
}
String[] longKeys = {MediaMetadata.METADATA_KEY_TRACK_NUMBER,
MediaMetadata.METADATA_KEY_NUM_TRACKS, MediaMetadata.METADATA_KEY_DURATION};
for (String key : longKeys) {
if (!bundle.containsKey(key)) bundle.putLong(key, metadata.getLong(key));
}
return bundle;
}
void updateNowPlayingList(@Nullable MediaController mediaController) {
getNowPlayingList(mediaController);
}
/* Instructs media player to play particular media item */
void playItem(byte[] bdaddr, byte[] uid, @Nullable MediaController mediaController) {
long qid = ByteBuffer.wrap(uid).getLong();
List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
if (mediaController == null) {
Log.e(TAG, "No mediaController when PlayItem " + qid + " requested");
mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR);
return;
}
MediaController.TransportControls mediaControllerCntrl =
mediaController.getTransportControls();
if (items == null) {
Log.w(TAG, "nowPlayingItems is null");
mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR);
return;
}
for (MediaSession.QueueItem item : items) {
if (qid == item.getQueueId()) {
if (DEBUG) Log.d(TAG, "Skipping to ID " + qid);
mediaControllerCntrl.skipToQueueItem(qid);
mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR);
return;
}
}
Log.w(TAG, "Invalid now playing Queue ID " + qid);
mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INV_ITEM);
}
void getTotalNumOfItems(byte[] bdaddr, @Nullable MediaController mediaController) {
List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
if (DEBUG) Log.d(TAG, "getTotalNumOfItems: " + items.size() + " items.");
mMediaInterface.getTotalNumOfItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, 0, items.size());
}
void sendTrackChangeWithId(int type, @Nullable MediaController mediaController) {
if (DEBUG)
Log.d(TAG, "sendTrackChangeWithId (" + type + "): controller " + mediaController);
long qid = getActiveQueueItemId(mediaController);
byte[] track = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array();
mMediaInterface.trackChangedRsp(type, track);
mLastTrackIdSent = qid;
// The nowPlaying might have changed.
updateNowPlayingList(mediaController);
}
/*
* helper method to check if startItem and endItem index is with range of
* MediaItem list. (Resultset containing all items in current path)
*/
private @Nullable List<MediaSession.QueueItem> getQueueSubset(
@NonNull List<MediaSession.QueueItem> items, long startItem, long endItem) {
if (endItem > items.size()) endItem = items.size() - 1;
if (startItem > Integer.MAX_VALUE) startItem = Integer.MAX_VALUE;
try {
List<MediaSession.QueueItem> selected =
items.subList((int) startItem, (int) Math.min(items.size(), endItem + 1));
if (selected.isEmpty()) {
Log.i(TAG, "itemsSubList is empty.");
return null;
}
return selected;
} catch (IndexOutOfBoundsException ex) {
Log.i(TAG, "Range (" + startItem + ", " + endItem + ") invalid");
} catch (IllegalArgumentException ex) {
Log.i(TAG, "Range start " + startItem + " > size (" + items.size() + ")");
}
return null;
}
/*
* helper method to filter required attibutes before sending GetFolderItems
* response
*/
private void getFolderItemsFilterAttr(byte[] bdaddr, AvrcpCmd.FolderItemsCmd folderItemsReqObj,
@NonNull List<MediaSession.QueueItem> items, byte scope, long startItem, long endItem,
@NonNull MediaController mediaController) {
if (DEBUG) Log.d(TAG, "getFolderItemsFilterAttr: startItem =" + startItem + ", endItem = "
+ endItem);
List<MediaSession.QueueItem> result_items = getQueueSubset(items, startItem, endItem);
/* check for index out of bound errors */
if (result_items == null) {
Log.w(TAG, "getFolderItemsFilterAttr: result_items is empty");
mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_INV_RANGE, null);
return;
}
FolderItemsData folderDataNative = new FolderItemsData(result_items.size());
/* variables to accumulate attrs */
ArrayList<String> attrArray = new ArrayList<String>();
ArrayList<Integer> attrId = new ArrayList<Integer>();
for (int itemIndex = 0; itemIndex < result_items.size(); itemIndex++) {
MediaSession.QueueItem item = result_items.get(itemIndex);
// get the queue id
long qid = item.getQueueId();
byte[] uid = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array();
// get the array of uid from 2d to array 1D array
for (int idx = 0; idx < AvrcpConstants.UID_SIZE; idx++) {
folderDataNative.mItemUid[itemIndex * AvrcpConstants.UID_SIZE + idx] = uid[idx];
}
/* Set display name for current item */
folderDataNative.mDisplayNames[itemIndex] =
getAttrValue(AvrcpConstants.ATTRID_TITLE, item, mediaController);
int maxAttributesRequested = 0;
boolean isAllAttribRequested = false;
/* check if remote requested for attributes */
if (folderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
int attrCnt = 0;
/* add requested attr ids to a temp array */
if (folderItemsReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) {
isAllAttribRequested = true;
maxAttributesRequested = AvrcpConstants.MAX_NUM_ATTR;
} else {
/* get only the requested attribute ids from the request */
maxAttributesRequested = folderItemsReqObj.mNumAttr;
}
/* lookup and copy values of attributes for ids requested above */
for (int idx = 0; idx < maxAttributesRequested; idx++) {
/* check if media player provided requested attributes */
String value = null;
int attribId =
isAllAttribRequested ? (idx + 1) : folderItemsReqObj.mAttrIDs[idx];
value = getAttrValue(attribId, item, mediaController);
if (value != null) {
attrArray.add(value);
attrId.add(attribId);
attrCnt++;
}
}
/* add num attr actually received from media player for a particular item */
folderDataNative.mAttributesNum[itemIndex] = attrCnt;
}
}
/* copy filtered attr ids and attr values to response parameters */
if (folderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
folderDataNative.mAttrIds = new int[attrId.size()];
for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++)
folderDataNative.mAttrIds[attrIndex] = attrId.get(attrIndex);
folderDataNative.mAttrValues = attrArray.toArray(new String[attrArray.size()]);
}
for (int attrIndex = 0; attrIndex < folderDataNative.mAttributesNum.length; attrIndex++)
if (DEBUG)
Log.d(TAG, "folderDataNative.mAttributesNum"
+ folderDataNative.mAttributesNum[attrIndex] + " attrIndex "
+ attrIndex);
/* create rsp object and send response to remote device */
FolderItemsRsp rspObj = new FolderItemsRsp(AvrcpConstants.RSP_NO_ERROR, Avrcp.sUIDCounter,
scope, folderDataNative.mNumItems, folderDataNative.mFolderTypes,
folderDataNative.mPlayable, folderDataNative.mItemTypes, folderDataNative.mItemUid,
folderDataNative.mDisplayNames, folderDataNative.mAttributesNum,
folderDataNative.mAttrIds, folderDataNative.mAttrValues);
mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj);
}
private String getAttrValue(
int attr, MediaSession.QueueItem item, @Nullable MediaController mediaController) {
String attrValue = null;
if (item == null) {
if (DEBUG) Log.d(TAG, "getAttrValue received null item");
return null;
}
try {
MediaDescription desc = item.getDescription();
Bundle extras = desc.getExtras();
boolean isCurrentTrack = item.getQueueId() == getActiveQueueItemId(mediaController);
if (isCurrentTrack) {
if (DEBUG) Log.d(TAG, "getAttrValue: item is active, using current data");
extras = fillBundle(mediaController.getMetadata(), extras);
}
if (DEBUG) Log.d(TAG, "getAttrValue: item " + item + " : " + desc);
switch (attr) {
case AvrcpConstants.ATTRID_TITLE:
/* Title is mandatory attribute */
if (isCurrentTrack) {
attrValue = extras.getString(MediaMetadata.METADATA_KEY_TITLE);
} else {
attrValue = desc.getTitle().toString();
}
break;
case AvrcpConstants.ATTRID_ARTIST:
attrValue = extras.getString(MediaMetadata.METADATA_KEY_ARTIST);
break;
case AvrcpConstants.ATTRID_ALBUM:
attrValue = extras.getString(MediaMetadata.METADATA_KEY_ALBUM);
break;
case AvrcpConstants.ATTRID_TRACK_NUM:
attrValue =
Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER));
break;
case AvrcpConstants.ATTRID_NUM_TRACKS:
attrValue =
Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS));
break;
case AvrcpConstants.ATTRID_GENRE:
attrValue = extras.getString(MediaMetadata.METADATA_KEY_GENRE);
break;
case AvrcpConstants.ATTRID_PLAY_TIME:
attrValue = Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_DURATION));
break;
case AvrcpConstants.ATTRID_COVER_ART:
Log.e(TAG, "getAttrValue: Cover art attribute not supported");
return null;
default:
Log.e(TAG, "getAttrValue: Unknown attribute ID requested: " + attr);
return null;
}
} catch (NullPointerException ex) {
Log.w(TAG, "getAttrValue: attr id not found in result");
/* checking if attribute is title, then it is mandatory and cannot send null */
if (attr == AvrcpConstants.ATTRID_TITLE) {
attrValue = "<Unknown Title>";
} else {
return null;
}
}
if (DEBUG) Log.d(TAG, "getAttrValue: attrvalue = " + attrValue + ", attr id:" + attr);
return attrValue;
}
private void getItemAttrFilterAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd mItemAttrReqObj,
MediaSession.QueueItem mediaItem, @Nullable MediaController mediaController) {
/* Response parameters */
int[] attrIds = null; /* array of attr ids */
String[] attrValues = null; /* array of attr values */
/* variables to temperorily add attrs */
ArrayList<String> attrArray = new ArrayList<String>();
ArrayList<Integer> attrId = new ArrayList<Integer>();
ArrayList<Integer> attrTempId = new ArrayList<Integer>();
/* check if remote device has requested for attributes */
if (mItemAttrReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
if (mItemAttrReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) {
for (int idx = 1; idx < AvrcpConstants.MAX_NUM_ATTR; idx++) {
attrTempId.add(idx); /* attr id 0x00 is unused */
}
} else {
/* get only the requested attribute ids from the request */
for (int idx = 0; idx < mItemAttrReqObj.mNumAttr; idx++) {
if (DEBUG)
Log.d(TAG, "getItemAttrFilterAttr: attr id[" + idx + "] :"
+ mItemAttrReqObj.mAttrIDs[idx]);
attrTempId.add(mItemAttrReqObj.mAttrIDs[idx]);
}
}
}
if (DEBUG) Log.d(TAG, "getItemAttrFilterAttr: attr id list size:" + attrTempId.size());
/* lookup and copy values of attributes for ids requested above */
for (int idx = 0; idx < attrTempId.size(); idx++) {
/* check if media player provided requested attributes */
String value = getAttrValue(attrTempId.get(idx), mediaItem, mediaController);
if (value != null) {
attrArray.add(value);
attrId.add(attrTempId.get(idx));
}
}
/* copy filtered attr ids and attr values to response parameters */
if (mItemAttrReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
attrIds = new int[attrId.size()];
for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++)
attrIds[attrIndex] = attrId.get(attrIndex);
attrValues = attrArray.toArray(new String[attrId.size()]);
/* create rsp object and send response */
ItemAttrRsp rspObj = new ItemAttrRsp(AvrcpConstants.RSP_NO_ERROR, attrIds, attrValues);
mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj);
return;
}
}
private long getActiveQueueItemId(@Nullable MediaController controller) {
if (controller == null) return MediaSession.QueueItem.UNKNOWN_ID;
PlaybackState state = controller.getPlaybackState();
if (state == null) return MediaSession.QueueItem.UNKNOWN_ID;
long qid = state.getActiveQueueItemId();
if (qid != MediaSession.QueueItem.UNKNOWN_ID) return qid;
// Check if we're presenting a "one item queue"
if (controller.getMetadata() != null) return SINGLE_QID;
return MediaSession.QueueItem.UNKNOWN_ID;
}
String displayMediaItem(MediaSession.QueueItem item) {
StringBuilder sb = new StringBuilder();
sb.append("#");
sb.append(item.getQueueId());
sb.append(": ");
sb.append(Utils.ellipsize(getAttrValue(AvrcpConstants.ATTRID_TITLE, item, null)));
sb.append(" - ");
sb.append(Utils.ellipsize(getAttrValue(AvrcpConstants.ATTRID_ALBUM, item, null)));
sb.append(" by ");
sb.append(Utils.ellipsize(getAttrValue(AvrcpConstants.ATTRID_ARTIST, item, null)));
sb.append(" (");
sb.append(getAttrValue(AvrcpConstants.ATTRID_PLAY_TIME, item, null));
sb.append(" ");
sb.append(getAttrValue(AvrcpConstants.ATTRID_TRACK_NUM, item, null));
sb.append("/");
sb.append(getAttrValue(AvrcpConstants.ATTRID_NUM_TRACKS, item, null));
sb.append(") ");
sb.append(getAttrValue(AvrcpConstants.ATTRID_GENRE, item, null));
return sb.toString();
}
public void dump(StringBuilder sb, @Nullable MediaController mediaController) {
ProfileService.println(sb, "AddressedPlayer info:");
ProfileService.println(sb, "mLastTrackIdSent: " + mLastTrackIdSent);
ProfileService.println(sb, "mNowPlayingList: " + mNowPlayingList.size() + " elements");
long currentQueueId = getActiveQueueItemId(mediaController);
for (MediaSession.QueueItem item : mNowPlayingList) {
long itemId = item.getQueueId();
ProfileService.println(
sb, (itemId == currentQueueId ? "*" : " ") + displayMediaItem(item));
}
}
}