1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.bluetooth.avrcp;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.bluetooth.BluetoothAvrcp;
22 import android.media.session.MediaSession;
23 import android.media.session.PlaybackState;
24 import android.media.session.MediaSession.QueueItem;
25 import android.media.MediaDescription;
26 import android.media.MediaMetadata;
27 import android.os.Bundle;
28 import android.util.Log;
29 
30 import com.android.bluetooth.btservice.ProfileService;
31 import com.android.bluetooth.Utils;
32 
33 import java.nio.ByteBuffer;
34 import java.util.List;
35 import java.util.Arrays;
36 import java.util.ArrayList;
37 
38 /*************************************************************************************************
39  * Provides functionality required for Addressed Media Player, like Now Playing List related
40  * browsing commands, control commands to the current addressed player(playItem, play, pause, etc)
41  * Acts as an Interface to communicate with media controller APIs for NowPlayingItems.
42  ************************************************************************************************/
43 
44 public class AddressedMediaPlayer {
45     static private final String TAG = "AddressedMediaPlayer";
46     static private final Boolean DEBUG = false;
47 
48     static private final long SINGLE_QID = 1;
49     static private final String UNKNOWN_TITLE = "(unknown)";
50 
51     private AvrcpMediaRspInterface mMediaInterface;
52     private @NonNull List<MediaSession.QueueItem> mNowPlayingList;
53 
54     private final List<MediaSession.QueueItem> mEmptyNowPlayingList;
55 
56     private long mLastTrackIdSent;
57     private boolean mNowPlayingListUpdated;
58 
AddressedMediaPlayer(AvrcpMediaRspInterface mediaInterface)59     public AddressedMediaPlayer(AvrcpMediaRspInterface mediaInterface) {
60         mEmptyNowPlayingList = new ArrayList<MediaSession.QueueItem>();
61         mNowPlayingList = mEmptyNowPlayingList;
62         mMediaInterface = mediaInterface;
63         mLastTrackIdSent = MediaSession.QueueItem.UNKNOWN_ID;
64         mNowPlayingListUpdated = false;
65     }
66 
cleanup()67     void cleanup() {
68         if (DEBUG) Log.v(TAG, "cleanup");
69         mNowPlayingList = mEmptyNowPlayingList;
70         mMediaInterface = null;
71         mLastTrackIdSent = MediaSession.QueueItem.UNKNOWN_ID;
72         mNowPlayingListUpdated = false;
73     }
74 
75     /* get now playing list from addressed player */
getFolderItemsNowPlaying(byte[] bdaddr, AvrcpCmd.FolderItemsCmd reqObj, @Nullable MediaController mediaController)76     void getFolderItemsNowPlaying(byte[] bdaddr, AvrcpCmd.FolderItemsCmd reqObj,
77             @Nullable MediaController mediaController) {
78         if (mediaController == null) {
79             // No players (if a player exists, we would have selected it)
80             Log.e(TAG, "mediaController = null, sending no available players response");
81             mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_AVBL_PLAY, null);
82             return;
83         }
84         List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
85         getFolderItemsFilterAttr(bdaddr, reqObj, items, AvrcpConstants.BTRC_SCOPE_NOW_PLAYING,
86                 reqObj.mStartItem, reqObj.mEndItem, mediaController);
87     }
88 
89     /* get item attributes for item in now playing list */
getItemAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd itemAttr, @Nullable MediaController mediaController)90     void getItemAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd itemAttr,
91             @Nullable MediaController mediaController) {
92         int status = AvrcpConstants.RSP_NO_ERROR;
93         long mediaId = ByteBuffer.wrap(itemAttr.mUid).getLong();
94         List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
95 
96         // NOTE: this is out-of-spec (AVRCP 1.6.1 sec 6.10.4.3, p90) but we answer it anyway
97         // because some CTs ask for it.
98         if (Arrays.equals(itemAttr.mUid, AvrcpConstants.TRACK_IS_SELECTED)) {
99             if (DEBUG) Log.d(TAG, "getItemAttr: Remote requests for now playing contents:");
100 
101             // get the current playing metadata and send.
102             getItemAttrFilterAttr(bdaddr, itemAttr, getCurrentQueueItem(mediaController, mediaId),
103                     mediaController);
104             return;
105         }
106 
107         if (DEBUG) Log.d(TAG, "getItemAttr-UID: 0x" + Utils.byteArrayToString(itemAttr.mUid));
108         for (MediaSession.QueueItem item : items) {
109             if (item.getQueueId() == mediaId) {
110                 getItemAttrFilterAttr(bdaddr, itemAttr, item, mediaController);
111                 return;
112             }
113         }
114 
115         // Couldn't find it, so the id is invalid
116         mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_INV_ITEM, null);
117     }
118 
119     /* Refresh and get the queue of now playing.
120      */
getNowPlayingList( @ullable MediaController mediaController)121     private @NonNull List<MediaSession.QueueItem> getNowPlayingList(
122             @Nullable MediaController mediaController) {
123         if (mediaController == null) return mEmptyNowPlayingList;
124         List<MediaSession.QueueItem> items = mediaController.getQueue();
125         if (items != null && !mNowPlayingListUpdated) {
126             mNowPlayingList = items;
127             return mNowPlayingList;
128         }
129         if (items == null) {
130             Log.i(TAG, "null queue from " + mediaController.getPackageName()
131                             + ", constructing single-item list");
132             MediaMetadata metadata = mediaController.getMetadata();
133             // Because we are database-unaware, we can just number the item here whatever we want
134             // because they have to re-poll it every time.
135             MediaSession.QueueItem current = getCurrentQueueItem(mediaController, SINGLE_QID);
136             items = new ArrayList<MediaSession.QueueItem>();
137             items.add(current);
138         }
139 
140         mNowPlayingList = items;
141 
142         if (mNowPlayingListUpdated) sendNowPlayingListChanged();
143 
144         return mNowPlayingList;
145     }
146 
sendNowPlayingListChanged()147     private void sendNowPlayingListChanged() {
148         if (mMediaInterface == null) return;
149         mMediaInterface.uidsChangedRsp(AvrcpConstants.NOTIFICATION_TYPE_CHANGED);
150         mMediaInterface.nowPlayingChangedRsp(AvrcpConstants.NOTIFICATION_TYPE_CHANGED);
151         mNowPlayingListUpdated = false;
152     }
153 
154     /* Constructs a queue item representing the current playing metadata from an
155      * active controller with queue id |qid|.
156      */
getCurrentQueueItem( @ullable MediaController controller, long qid)157     private MediaSession.QueueItem getCurrentQueueItem(
158             @Nullable MediaController controller, long qid) {
159         if (controller == null) {
160             MediaDescription.Builder bob = new MediaDescription.Builder();
161             bob.setTitle(UNKNOWN_TITLE);
162             return new QueueItem(bob.build(), qid);
163         }
164 
165         MediaMetadata metadata = controller.getMetadata();
166         if (metadata == null) {
167             Log.w(TAG, "Controller has no metadata!? Making an empty one");
168             metadata = (new MediaMetadata.Builder()).build();
169         }
170 
171         MediaDescription.Builder bob = new MediaDescription.Builder();
172         MediaDescription desc = metadata.getDescription();
173 
174         // set the simple ones that MediaMetadata builds for us
175         bob.setMediaId(desc.getMediaId());
176         bob.setTitle(desc.getTitle());
177         bob.setSubtitle(desc.getSubtitle());
178         bob.setDescription(desc.getDescription());
179         // fill the ones that we use later
180         bob.setExtras(fillBundle(metadata, desc.getExtras()));
181 
182         // build queue item with the new metadata
183         desc = bob.build();
184         return new QueueItem(desc, qid);
185     }
186 
fillBundle(MediaMetadata metadata, Bundle currentExtras)187     private Bundle fillBundle(MediaMetadata metadata, Bundle currentExtras) {
188         if (metadata == null) {
189             return currentExtras;
190         }
191 
192         Bundle bundle = currentExtras;
193         if (bundle == null) bundle = new Bundle();
194 
195         String[] stringKeys = {MediaMetadata.METADATA_KEY_TITLE, MediaMetadata.METADATA_KEY_ARTIST,
196                 MediaMetadata.METADATA_KEY_ALBUM, MediaMetadata.METADATA_KEY_GENRE};
197         for (String key : stringKeys) {
198             String current = bundle.getString(key);
199             if (current == null) bundle.putString(key, metadata.getString(key));
200         }
201 
202         String[] longKeys = {MediaMetadata.METADATA_KEY_TRACK_NUMBER,
203                 MediaMetadata.METADATA_KEY_NUM_TRACKS, MediaMetadata.METADATA_KEY_DURATION};
204         for (String key : longKeys) {
205             if (!bundle.containsKey(key)) bundle.putLong(key, metadata.getLong(key));
206         }
207         return bundle;
208     }
209 
updateNowPlayingList(@ullable MediaController mediaController)210     void updateNowPlayingList(@Nullable MediaController mediaController) {
211         mNowPlayingListUpdated = true;
212         getNowPlayingList(mediaController);
213     }
214 
215     /* Instructs media player to play particular media item */
playItem(byte[] bdaddr, byte[] uid, @Nullable MediaController mediaController)216     void playItem(byte[] bdaddr, byte[] uid, @Nullable MediaController mediaController) {
217         long qid = ByteBuffer.wrap(uid).getLong();
218         List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
219 
220         if (mediaController == null) {
221             Log.e(TAG, "No mediaController when PlayItem " + qid + " requested");
222             mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR);
223             return;
224         }
225 
226         MediaController.TransportControls mediaControllerCntrl =
227                 mediaController.getTransportControls();
228 
229         if (items == null) {
230             Log.w(TAG, "nowPlayingItems is null");
231             mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR);
232             return;
233         }
234 
235         for (MediaSession.QueueItem item : items) {
236             if (qid == item.getQueueId()) {
237                 if (DEBUG) Log.d(TAG, "Skipping to ID " + qid);
238                 mediaControllerCntrl.skipToQueueItem(qid);
239                 mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR);
240                 return;
241             }
242         }
243 
244         Log.w(TAG, "Invalid now playing Queue ID " + qid);
245         mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INV_ITEM);
246     }
247 
getTotalNumOfItems(byte[] bdaddr, @Nullable MediaController mediaController)248     void getTotalNumOfItems(byte[] bdaddr, @Nullable MediaController mediaController) {
249         List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
250         if (DEBUG) Log.d(TAG, "getTotalNumOfItems: " + items.size() + " items.");
251         mMediaInterface.getTotalNumOfItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, 0, items.size());
252     }
253 
sendTrackChangeWithId(int type, @Nullable MediaController mediaController)254     void sendTrackChangeWithId(int type, @Nullable MediaController mediaController) {
255         Log.d(TAG, "sendTrackChangeWithId (" + type + "): controller " + mediaController);
256         long qid = getActiveQueueItemId(mediaController);
257         byte[] track = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array();
258         // The nowPlayingList changed: the new list has the full data for the current item
259         if (type == AvrcpConstants.NOTIFICATION_TYPE_CHANGED) sendNowPlayingListChanged();
260         mMediaInterface.trackChangedRsp(type, track);
261         mLastTrackIdSent = qid;
262     }
263 
264     /*
265      * helper method to check if startItem and endItem index is with range of
266      * MediaItem list. (Resultset containing all items in current path)
267      */
getQueueSubset( @onNull List<MediaSession.QueueItem> items, long startItem, long endItem)268     private @Nullable List<MediaSession.QueueItem> getQueueSubset(
269             @NonNull List<MediaSession.QueueItem> items, long startItem, long endItem) {
270         if (endItem > items.size()) endItem = items.size() - 1;
271         if (startItem > Integer.MAX_VALUE) startItem = Integer.MAX_VALUE;
272         try {
273             List<MediaSession.QueueItem> selected =
274                     items.subList((int) startItem, (int) Math.min(items.size(), endItem + 1));
275             if (selected.isEmpty()) {
276                 Log.i(TAG, "itemsSubList is empty.");
277                 return null;
278             }
279             return selected;
280         } catch (IndexOutOfBoundsException ex) {
281             Log.i(TAG, "Range (" + startItem + ", " + endItem + ") invalid");
282         } catch (IllegalArgumentException ex) {
283             Log.i(TAG, "Range start " + startItem + " > size (" + items.size() + ")");
284         }
285         return null;
286     }
287 
288     /*
289      * helper method to filter required attibutes before sending GetFolderItems
290      * response
291      */
getFolderItemsFilterAttr(byte[] bdaddr, AvrcpCmd.FolderItemsCmd folderItemsReqObj, @NonNull List<MediaSession.QueueItem> items, byte scope, long startItem, long endItem, @NonNull MediaController mediaController)292     private void getFolderItemsFilterAttr(byte[] bdaddr, AvrcpCmd.FolderItemsCmd folderItemsReqObj,
293             @NonNull List<MediaSession.QueueItem> items, byte scope, long startItem, long endItem,
294             @NonNull MediaController mediaController) {
295         if (DEBUG) Log.d(TAG, "getFolderItemsFilterAttr: startItem =" + startItem + ", endItem = "
296                 + endItem);
297 
298         List<MediaSession.QueueItem> result_items = getQueueSubset(items, startItem, endItem);
299         /* check for index out of bound errors */
300         if (result_items == null) {
301             Log.w(TAG, "getFolderItemsFilterAttr: result_items is empty");
302             mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_INV_RANGE, null);
303             return;
304         }
305 
306         FolderItemsData folderDataNative = new FolderItemsData(result_items.size());
307 
308         /* variables to accumulate attrs */
309         ArrayList<String> attrArray = new ArrayList<String>();
310         ArrayList<Integer> attrId = new ArrayList<Integer>();
311 
312         for (int itemIndex = 0; itemIndex < result_items.size(); itemIndex++) {
313             MediaSession.QueueItem item = result_items.get(itemIndex);
314             // get the queue id
315             long qid = item.getQueueId();
316             byte[] uid = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array();
317 
318             // get the array of uid from 2d to array 1D array
319             for (int idx = 0; idx < AvrcpConstants.UID_SIZE; idx++) {
320                 folderDataNative.mItemUid[itemIndex * AvrcpConstants.UID_SIZE + idx] = uid[idx];
321             }
322 
323             /* Set display name for current item */
324             folderDataNative.mDisplayNames[itemIndex] =
325                     getAttrValue(AvrcpConstants.ATTRID_TITLE, item, mediaController);
326 
327             int maxAttributesRequested = 0;
328             boolean isAllAttribRequested = false;
329             /* check if remote requested for attributes */
330             if (folderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
331                 int attrCnt = 0;
332 
333                 /* add requested attr ids to a temp array */
334                 if (folderItemsReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) {
335                     isAllAttribRequested = true;
336                     maxAttributesRequested = AvrcpConstants.MAX_NUM_ATTR;
337                 } else {
338                     /* get only the requested attribute ids from the request */
339                     maxAttributesRequested = folderItemsReqObj.mNumAttr;
340                 }
341 
342                 /* lookup and copy values of attributes for ids requested above */
343                 for (int idx = 0; idx < maxAttributesRequested; idx++) {
344                     /* check if media player provided requested attributes */
345                     String value = null;
346 
347                     int attribId =
348                             isAllAttribRequested ? (idx + 1) : folderItemsReqObj.mAttrIDs[idx];
349                     value = getAttrValue(attribId, item, mediaController);
350                     if (value != null) {
351                         attrArray.add(value);
352                         attrId.add(attribId);
353                         attrCnt++;
354                     }
355                 }
356                 /* add num attr actually received from media player for a particular item */
357                 folderDataNative.mAttributesNum[itemIndex] = attrCnt;
358             }
359         }
360 
361         /* copy filtered attr ids and attr values to response parameters */
362         if (folderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
363             folderDataNative.mAttrIds = new int[attrId.size()];
364             for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++)
365                 folderDataNative.mAttrIds[attrIndex] = attrId.get(attrIndex);
366             folderDataNative.mAttrValues = attrArray.toArray(new String[attrArray.size()]);
367         }
368         for (int attrIndex = 0; attrIndex < folderDataNative.mAttributesNum.length; attrIndex++)
369             if (DEBUG)
370                 Log.d(TAG, "folderDataNative.mAttributesNum"
371                                 + folderDataNative.mAttributesNum[attrIndex] + " attrIndex "
372                                 + attrIndex);
373 
374         /* create rsp object and send response to remote device */
375         FolderItemsRsp rspObj = new FolderItemsRsp(AvrcpConstants.RSP_NO_ERROR, Avrcp.sUIDCounter,
376                 scope, folderDataNative.mNumItems, folderDataNative.mFolderTypes,
377                 folderDataNative.mPlayable, folderDataNative.mItemTypes, folderDataNative.mItemUid,
378                 folderDataNative.mDisplayNames, folderDataNative.mAttributesNum,
379                 folderDataNative.mAttrIds, folderDataNative.mAttrValues);
380         mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj);
381     }
382 
getAttrValue( int attr, MediaSession.QueueItem item, @Nullable MediaController mediaController)383     private String getAttrValue(
384             int attr, MediaSession.QueueItem item, @Nullable MediaController mediaController) {
385         String attrValue = null;
386         if (item == null) {
387             if (DEBUG) Log.d(TAG, "getAttrValue received null item");
388             return null;
389         }
390         try {
391             MediaDescription desc = item.getDescription();
392             Bundle extras = desc.getExtras();
393             boolean isCurrentTrack = item.getQueueId() == getActiveQueueItemId(mediaController);
394             if (isCurrentTrack) {
395                 if (DEBUG) Log.d(TAG, "getAttrValue: item is active, using current data");
396                 extras = fillBundle(mediaController.getMetadata(), extras);
397             }
398             if (DEBUG) Log.d(TAG, "getAttrValue: item " + item + " : " + desc);
399             switch (attr) {
400                 case AvrcpConstants.ATTRID_TITLE:
401                     /* Title is mandatory attribute */
402                     if (isCurrentTrack) {
403                         attrValue = extras.getString(MediaMetadata.METADATA_KEY_TITLE);
404                     } else {
405                         attrValue = desc.getTitle().toString();
406                     }
407                     break;
408 
409                 case AvrcpConstants.ATTRID_ARTIST:
410                     attrValue = extras.getString(MediaMetadata.METADATA_KEY_ARTIST);
411                     break;
412 
413                 case AvrcpConstants.ATTRID_ALBUM:
414                     attrValue = extras.getString(MediaMetadata.METADATA_KEY_ALBUM);
415                     break;
416 
417                 case AvrcpConstants.ATTRID_TRACK_NUM:
418                     attrValue =
419                             Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER));
420                     break;
421 
422                 case AvrcpConstants.ATTRID_NUM_TRACKS:
423                     attrValue =
424                             Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS));
425                     break;
426 
427                 case AvrcpConstants.ATTRID_GENRE:
428                     attrValue = extras.getString(MediaMetadata.METADATA_KEY_GENRE);
429                     break;
430 
431                 case AvrcpConstants.ATTRID_PLAY_TIME:
432                     attrValue = Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_DURATION));
433                     break;
434 
435                 case AvrcpConstants.ATTRID_COVER_ART:
436                     Log.e(TAG, "getAttrValue: Cover art attribute not supported");
437                     return null;
438 
439                 default:
440                     Log.e(TAG, "getAttrValue: Unknown attribute ID requested: " + attr);
441                     return null;
442             }
443         } catch (NullPointerException ex) {
444             Log.w(TAG, "getAttrValue: attr id not found in result");
445             /* checking if attribute is title, then it is mandatory and cannot send null */
446             if (attr == AvrcpConstants.ATTRID_TITLE) {
447                 attrValue = "<Unknown Title>";
448             } else {
449                 return null;
450             }
451         }
452         if (DEBUG) Log.d(TAG, "getAttrValue: attrvalue = " + attrValue + ", attr id:" + attr);
453         return attrValue;
454     }
455 
getItemAttrFilterAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd mItemAttrReqObj, MediaSession.QueueItem mediaItem, @Nullable MediaController mediaController)456     private void getItemAttrFilterAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd mItemAttrReqObj,
457             MediaSession.QueueItem mediaItem, @Nullable MediaController mediaController) {
458         /* Response parameters */
459         int[] attrIds = null; /* array of attr ids */
460         String[] attrValues = null; /* array of attr values */
461 
462         /* variables to temperorily add attrs */
463         ArrayList<String> attrArray = new ArrayList<String>();
464         ArrayList<Integer> attrId = new ArrayList<Integer>();
465         ArrayList<Integer> attrTempId = new ArrayList<Integer>();
466 
467         /* check if remote device has requested for attributes */
468         if (mItemAttrReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
469             if (mItemAttrReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) {
470                 for (int idx = 1; idx < AvrcpConstants.MAX_NUM_ATTR; idx++) {
471                     attrTempId.add(idx); /* attr id 0x00 is unused */
472                 }
473             } else {
474                 /* get only the requested attribute ids from the request */
475                 for (int idx = 0; idx < mItemAttrReqObj.mNumAttr; idx++) {
476                     if (DEBUG)
477                         Log.d(TAG, "getItemAttrFilterAttr: attr id[" + idx + "] :"
478                                         + mItemAttrReqObj.mAttrIDs[idx]);
479                     attrTempId.add(mItemAttrReqObj.mAttrIDs[idx]);
480                 }
481             }
482         }
483 
484         if (DEBUG) Log.d(TAG, "getItemAttrFilterAttr: attr id list size:" + attrTempId.size());
485         /* lookup and copy values of attributes for ids requested above */
486         for (int idx = 0; idx < attrTempId.size(); idx++) {
487             /* check if media player provided requested attributes */
488             String value = getAttrValue(attrTempId.get(idx), mediaItem, mediaController);
489             if (value != null) {
490                 attrArray.add(value);
491                 attrId.add(attrTempId.get(idx));
492             }
493         }
494 
495         /* copy filtered attr ids and attr values to response parameters */
496         if (mItemAttrReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
497             attrIds = new int[attrId.size()];
498 
499             for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++)
500                 attrIds[attrIndex] = attrId.get(attrIndex);
501 
502             attrValues = attrArray.toArray(new String[attrId.size()]);
503 
504             /* create rsp object and send response */
505             ItemAttrRsp rspObj = new ItemAttrRsp(AvrcpConstants.RSP_NO_ERROR, attrIds, attrValues);
506             mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj);
507             return;
508         }
509     }
510 
getActiveQueueItemId(@ullable MediaController controller)511     private long getActiveQueueItemId(@Nullable MediaController controller) {
512         if (controller == null) return MediaSession.QueueItem.UNKNOWN_ID;
513         PlaybackState state = controller.getPlaybackState();
514         if (state == null) return MediaSession.QueueItem.UNKNOWN_ID;
515         long qid = state.getActiveQueueItemId();
516         if (qid != MediaSession.QueueItem.UNKNOWN_ID) return qid;
517         // Check if we're presenting a "one item queue"
518         if (controller.getMetadata() != null) return SINGLE_QID;
519         return MediaSession.QueueItem.UNKNOWN_ID;
520     }
521 
displayMediaItem(MediaSession.QueueItem item)522     String displayMediaItem(MediaSession.QueueItem item) {
523         StringBuilder sb = new StringBuilder();
524         sb.append("#");
525         sb.append(item.getQueueId());
526         sb.append(": ");
527         sb.append(Utils.ellipsize(getAttrValue(AvrcpConstants.ATTRID_TITLE, item, null)));
528         sb.append(" - ");
529         sb.append(Utils.ellipsize(getAttrValue(AvrcpConstants.ATTRID_ALBUM, item, null)));
530         sb.append(" by ");
531         sb.append(Utils.ellipsize(getAttrValue(AvrcpConstants.ATTRID_ARTIST, item, null)));
532         sb.append(" (");
533         sb.append(getAttrValue(AvrcpConstants.ATTRID_PLAY_TIME, item, null));
534         sb.append(" ");
535         sb.append(getAttrValue(AvrcpConstants.ATTRID_TRACK_NUM, item, null));
536         sb.append("/");
537         sb.append(getAttrValue(AvrcpConstants.ATTRID_NUM_TRACKS, item, null));
538         sb.append(") ");
539         sb.append(getAttrValue(AvrcpConstants.ATTRID_GENRE, item, null));
540         return sb.toString();
541     }
542 
dump(StringBuilder sb, @Nullable MediaController mediaController)543     public void dump(StringBuilder sb, @Nullable MediaController mediaController) {
544         ProfileService.println(sb, "AddressedPlayer info:");
545         ProfileService.println(sb, "mLastTrackIdSent: " + mLastTrackIdSent);
546         ProfileService.println(sb, "mNowPlayingList: " + mNowPlayingList.size() + " elements");
547         long currentQueueId = getActiveQueueItemId(mediaController);
548         for (MediaSession.QueueItem item : mNowPlayingList) {
549             long itemId = item.getQueueId();
550             ProfileService.println(
551                     sb, (itemId == currentQueueId ? "*" : " ") + displayMediaItem(item));
552         }
553     }
554 }
555