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