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