AddressedMediaPlayer.java revision 5aca05c1d79f3412b6964b3b6335ad6f2d558756
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 void sendTrackChangeWithId(int type, @Nullable MediaController mediaController) { 240 if (DEBUG) 241 Log.d(TAG, "sendTrackChangeWithId (" + type + "): controller " + mediaController); 242 long qid = getActiveQueueItemId(mediaController); 243 byte[] track = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array(); 244 mMediaInterface.trackChangedRsp(type, track); 245 mLastTrackIdSent = qid; 246 // The nowPlaying might have changed. 247 updateNowPlayingList(mediaController); 248 } 249 250 /* 251 * helper method to check if startItem and endItem index is with range of 252 * MediaItem list. (Resultset containing all items in current path) 253 */ 254 private @Nullable List<MediaSession.QueueItem> getQueueSubset( 255 @NonNull List<MediaSession.QueueItem> items, long startItem, long endItem) { 256 if (endItem > items.size()) endItem = items.size() - 1; 257 if (startItem > Integer.MAX_VALUE) startItem = Integer.MAX_VALUE; 258 try { 259 List<MediaSession.QueueItem> selected = 260 items.subList((int) startItem, (int) Math.min(items.size(), endItem + 1)); 261 if (selected.isEmpty()) { 262 Log.i(TAG, "itemsSubList is empty."); 263 return null; 264 } 265 return selected; 266 } catch (IndexOutOfBoundsException ex) { 267 Log.i(TAG, "Range (" + startItem + ", " + endItem + ") invalid"); 268 } catch (IllegalArgumentException ex) { 269 Log.i(TAG, "Range start " + startItem + " > size (" + items.size() + ")"); 270 } 271 return null; 272 } 273 274 /* 275 * helper method to filter required attibutes before sending GetFolderItems 276 * response 277 */ 278 private void getFolderItemsFilterAttr(byte[] bdaddr, AvrcpCmd.FolderItemsCmd folderItemsReqObj, 279 @NonNull List<MediaSession.QueueItem> items, byte scope, long startItem, long endItem, 280 @NonNull MediaController mediaController) { 281 if (DEBUG) Log.d(TAG, "getFolderItemsFilterAttr: startItem =" + startItem + ", endItem = " 282 + endItem); 283 284 List<MediaSession.QueueItem> result_items = getQueueSubset(items, startItem, endItem); 285 /* check for index out of bound errors */ 286 if (result_items == null) { 287 Log.w(TAG, "getFolderItemsFilterAttr: result_items is empty"); 288 mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_INV_RANGE, null); 289 return; 290 } 291 292 FolderItemsData folderDataNative = new FolderItemsData(result_items.size()); 293 294 /* variables to accumulate attrs */ 295 ArrayList<String> attrArray = new ArrayList<String>(); 296 ArrayList<Integer> attrId = new ArrayList<Integer>(); 297 298 for (int itemIndex = 0; itemIndex < result_items.size(); itemIndex++) { 299 MediaSession.QueueItem item = result_items.get(itemIndex); 300 // get the queue id 301 long qid = item.getQueueId(); 302 byte[] uid = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array(); 303 304 // get the array of uid from 2d to array 1D array 305 for (int idx = 0; idx < AvrcpConstants.UID_SIZE; idx++) { 306 folderDataNative.mItemUid[itemIndex * AvrcpConstants.UID_SIZE + idx] = uid[idx]; 307 } 308 309 /* Set display name for current item */ 310 folderDataNative.mDisplayNames[itemIndex] = 311 getAttrValue(AvrcpConstants.ATTRID_TITLE, item, mediaController); 312 313 int maxAttributesRequested = 0; 314 boolean isAllAttribRequested = false; 315 /* check if remote requested for attributes */ 316 if (folderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) { 317 int attrCnt = 0; 318 319 /* add requested attr ids to a temp array */ 320 if (folderItemsReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) { 321 isAllAttribRequested = true; 322 maxAttributesRequested = AvrcpConstants.MAX_NUM_ATTR; 323 } else { 324 /* get only the requested attribute ids from the request */ 325 maxAttributesRequested = folderItemsReqObj.mNumAttr; 326 } 327 328 /* lookup and copy values of attributes for ids requested above */ 329 for (int idx = 0; idx < maxAttributesRequested; idx++) { 330 /* check if media player provided requested attributes */ 331 String value = null; 332 333 int attribId = 334 isAllAttribRequested ? (idx + 1) : folderItemsReqObj.mAttrIDs[idx]; 335 value = getAttrValue(attribId, item, mediaController); 336 if (value != null) { 337 attrArray.add(value); 338 attrId.add(attribId); 339 attrCnt++; 340 } 341 } 342 /* add num attr actually received from media player for a particular item */ 343 folderDataNative.mAttributesNum[itemIndex] = attrCnt; 344 } 345 } 346 347 /* copy filtered attr ids and attr values to response parameters */ 348 if (folderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) { 349 folderDataNative.mAttrIds = new int[attrId.size()]; 350 for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++) 351 folderDataNative.mAttrIds[attrIndex] = attrId.get(attrIndex); 352 folderDataNative.mAttrValues = attrArray.toArray(new String[attrArray.size()]); 353 } 354 for (int attrIndex = 0; attrIndex < folderDataNative.mAttributesNum.length; attrIndex++) 355 if (DEBUG) 356 Log.d(TAG, "folderDataNative.mAttributesNum" 357 + folderDataNative.mAttributesNum[attrIndex] + " attrIndex " 358 + attrIndex); 359 360 /* create rsp object and send response to remote device */ 361 FolderItemsRsp rspObj = new FolderItemsRsp(AvrcpConstants.RSP_NO_ERROR, Avrcp.sUIDCounter, 362 scope, folderDataNative.mNumItems, folderDataNative.mFolderTypes, 363 folderDataNative.mPlayable, folderDataNative.mItemTypes, folderDataNative.mItemUid, 364 folderDataNative.mDisplayNames, folderDataNative.mAttributesNum, 365 folderDataNative.mAttrIds, folderDataNative.mAttrValues); 366 mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj); 367 } 368 369 private String getAttrValue( 370 int attr, MediaSession.QueueItem item, @Nullable MediaController mediaController) { 371 String attrValue = null; 372 if (item == null) { 373 if (DEBUG) Log.d(TAG, "getAttrValue received null item"); 374 return null; 375 } 376 try { 377 MediaDescription desc = item.getDescription(); 378 Bundle extras = desc.getExtras(); 379 boolean isCurrentTrack = item.getQueueId() == getActiveQueueItemId(mediaController); 380 if (isCurrentTrack) { 381 if (DEBUG) Log.d(TAG, "getAttrValue: item is active, using current data"); 382 extras = fillBundle(mediaController.getMetadata(), extras); 383 } 384 if (DEBUG) Log.d(TAG, "getAttrValue: item " + item + " : " + desc); 385 switch (attr) { 386 case AvrcpConstants.ATTRID_TITLE: 387 /* Title is mandatory attribute */ 388 if (isCurrentTrack) { 389 attrValue = extras.getString(MediaMetadata.METADATA_KEY_TITLE); 390 } else { 391 attrValue = desc.getTitle().toString(); 392 } 393 break; 394 395 case AvrcpConstants.ATTRID_ARTIST: 396 attrValue = extras.getString(MediaMetadata.METADATA_KEY_ARTIST); 397 break; 398 399 case AvrcpConstants.ATTRID_ALBUM: 400 attrValue = extras.getString(MediaMetadata.METADATA_KEY_ALBUM); 401 break; 402 403 case AvrcpConstants.ATTRID_TRACK_NUM: 404 attrValue = 405 Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER)); 406 break; 407 408 case AvrcpConstants.ATTRID_NUM_TRACKS: 409 attrValue = 410 Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS)); 411 break; 412 413 case AvrcpConstants.ATTRID_GENRE: 414 attrValue = extras.getString(MediaMetadata.METADATA_KEY_GENRE); 415 break; 416 417 case AvrcpConstants.ATTRID_PLAY_TIME: 418 attrValue = Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_DURATION)); 419 break; 420 421 case AvrcpConstants.ATTRID_COVER_ART: 422 Log.e(TAG, "getAttrValue: Cover art attribute not supported"); 423 return null; 424 425 default: 426 Log.e(TAG, "getAttrValue: Unknown attribute ID requested: " + attr); 427 return null; 428 } 429 } catch (NullPointerException ex) { 430 Log.w(TAG, "getAttrValue: attr id not found in result"); 431 /* checking if attribute is title, then it is mandatory and cannot send null */ 432 if (attr == AvrcpConstants.ATTRID_TITLE) { 433 attrValue = "<Unknown Title>"; 434 } else { 435 return null; 436 } 437 } 438 if (DEBUG) Log.d(TAG, "getAttrValue: attrvalue = " + attrValue + ", attr id:" + attr); 439 return attrValue; 440 } 441 442 private void getItemAttrFilterAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd mItemAttrReqObj, 443 MediaSession.QueueItem mediaItem, @Nullable MediaController mediaController) { 444 /* Response parameters */ 445 int[] attrIds = null; /* array of attr ids */ 446 String[] attrValues = null; /* array of attr values */ 447 448 /* variables to temperorily add attrs */ 449 ArrayList<String> attrArray = new ArrayList<String>(); 450 ArrayList<Integer> attrId = new ArrayList<Integer>(); 451 ArrayList<Integer> attrTempId = new ArrayList<Integer>(); 452 453 /* check if remote device has requested for attributes */ 454 if (mItemAttrReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) { 455 if (mItemAttrReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) { 456 for (int idx = 1; idx < AvrcpConstants.MAX_NUM_ATTR; idx++) { 457 attrTempId.add(idx); /* attr id 0x00 is unused */ 458 } 459 } else { 460 /* get only the requested attribute ids from the request */ 461 for (int idx = 0; idx < mItemAttrReqObj.mNumAttr; idx++) { 462 if (DEBUG) 463 Log.d(TAG, "getItemAttrFilterAttr: attr id[" + idx + "] :" 464 + mItemAttrReqObj.mAttrIDs[idx]); 465 attrTempId.add(mItemAttrReqObj.mAttrIDs[idx]); 466 } 467 } 468 } 469 470 if (DEBUG) Log.d(TAG, "getItemAttrFilterAttr: attr id list size:" + attrTempId.size()); 471 /* lookup and copy values of attributes for ids requested above */ 472 for (int idx = 0; idx < attrTempId.size(); idx++) { 473 /* check if media player provided requested attributes */ 474 String value = getAttrValue(attrTempId.get(idx), mediaItem, mediaController); 475 if (value != null) { 476 attrArray.add(value); 477 attrId.add(attrTempId.get(idx)); 478 } 479 } 480 481 /* copy filtered attr ids and attr values to response parameters */ 482 if (mItemAttrReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) { 483 attrIds = new int[attrId.size()]; 484 485 for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++) 486 attrIds[attrIndex] = attrId.get(attrIndex); 487 488 attrValues = attrArray.toArray(new String[attrId.size()]); 489 490 /* create rsp object and send response */ 491 ItemAttrRsp rspObj = new ItemAttrRsp(AvrcpConstants.RSP_NO_ERROR, attrIds, attrValues); 492 mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj); 493 return; 494 } 495 } 496 497 private long getActiveQueueItemId(@Nullable MediaController controller) { 498 if (controller == null) return MediaSession.QueueItem.UNKNOWN_ID; 499 PlaybackState state = controller.getPlaybackState(); 500 if (state == null) return MediaSession.QueueItem.UNKNOWN_ID; 501 long qid = state.getActiveQueueItemId(); 502 if (qid != MediaSession.QueueItem.UNKNOWN_ID) return qid; 503 // Check if we're presenting a "one item queue" 504 if (controller.getMetadata() != null) return SINGLE_QID; 505 return MediaSession.QueueItem.UNKNOWN_ID; 506 } 507 508 public void dump(StringBuilder sb, @Nullable MediaController mediaController) { 509 ProfileService.println(sb, "AddressedPlayer info:"); 510 ProfileService.println(sb, "mLastTrackIdSent: " + mLastTrackIdSent); 511 ProfileService.println(sb, "mNowPlayingList: " + mNowPlayingList.size() + " elements"); 512 long currentQueueId = getActiveQueueItemId(mediaController); 513 for (MediaSession.QueueItem item : mNowPlayingList) { 514 long itemId = item.getQueueId(); 515 ProfileService.println(sb, (itemId == currentQueueId ? "*" : " ") + item.toString()); 516 } 517 } 518} 519