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