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