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