1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.bluetooth.avrcp; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.media.MediaDescription; 22 import android.media.MediaMetadata; 23 import android.media.session.MediaSession; 24 import android.media.session.MediaSession.QueueItem; 25 import android.media.session.PlaybackState; 26 import android.os.Bundle; 27 import android.util.Log; 28 29 import com.android.bluetooth.Utils; 30 import com.android.bluetooth.btservice.ProfileService; 31 32 import java.nio.ByteBuffer; 33 import java.util.ArrayList; 34 import java.util.Arrays; 35 import 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 43 public 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