/* * 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 mNowPlayingList; private final List mEmptyNowPlayingList; private long mLastTrackIdSent; public AddressedMediaPlayer(AvrcpMediaRspInterface mediaInterface) { mEmptyNowPlayingList = new ArrayList(); 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 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 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 getNowPlayingList( @Nullable MediaController mediaController) { if (mediaController == null) return mEmptyNowPlayingList; List 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(); items.add(current); } mNowPlayingList = items; // TODO (jamuraa): test to see if the single-item queue is the same and don't send 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 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 items = getNowPlayingList(mediaController); if (DEBUG) Log.d(TAG, "getTotalNumOfItems: " + items.size() + " items."); mMediaInterface.getTotalNumOfItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, 0, items.size()); } boolean sendTrackChangeWithId(boolean requesting, @Nullable MediaController mediaController) { if (DEBUG) Log.d(TAG, "sendTrackChangeWithId (" + requesting + "): controller " + mediaController); long qid = getActiveQueueItemId(mediaController); byte[] track = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array(); if (DEBUG) Log.d(TAG, "trackChangedRsp: 0x" + Utils.byteArrayToString(track)); int trackChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED; if (requesting) trackChangedNT = AvrcpConstants.NOTIFICATION_TYPE_INTERIM; mMediaInterface.trackChangedRsp(trackChangedNT, track); mLastTrackIdSent = qid; // The nowPlaying might have changed. updateNowPlayingList(mediaController); return (trackChangedNT == AvrcpConstants.NOTIFICATION_TYPE_CHANGED); } /* * 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 getQueueSubset( @NonNull List items, long startItem, long endItem) { if (endItem > items.size()) endItem = items.size() - 1; if (startItem > Integer.MAX_VALUE) startItem = Integer.MAX_VALUE; try { List 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 items, byte scope, long startItem, long endItem, @NonNull MediaController mediaController) { if (DEBUG) Log.d(TAG, "getFolderItemsFilterAttr: startItem =" + startItem + ", endItem = " + endItem); List 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 attrArray = new ArrayList(); ArrayList attrId = new ArrayList(); 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 = ""; } 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 attrArray = new ArrayList(); ArrayList attrId = new ArrayList(); ArrayList attrTempId = new ArrayList(); /* 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; } 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 ? "*" : " ") + item.toString()); } } }