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.content.ComponentName; 21 import android.content.Context; 22 import android.media.MediaDescription; 23 import android.media.MediaMetadata; 24 import android.media.browse.MediaBrowser; 25 import android.media.browse.MediaBrowser.MediaItem; 26 import android.media.session.MediaSession; 27 import android.media.session.MediaSession.QueueItem; 28 import android.os.Bundle; 29 import android.util.Log; 30 31 import java.math.BigInteger; 32 import java.util.ArrayList; 33 import java.util.HashMap; 34 import java.util.List; 35 import java.util.Stack; 36 37 /************************************************************************************************* 38 * Provides functionality required for Browsed Media Player like browsing Virtual File System, get 39 * Item Attributes, play item from the file system, etc. 40 * Acts as an Interface to communicate with Media Browsing APIs for browsing FileSystem. 41 ************************************************************************************************/ 42 43 class BrowsedMediaPlayer { 44 private static final boolean DEBUG = false; 45 private static final String TAG = "BrowsedMediaPlayer"; 46 47 /* connection state with MediaBrowseService */ 48 private static final int DISCONNECTED = 0; 49 private static final int CONNECTED = 1; 50 private static final int SUSPENDED = 2; 51 52 private static final String[] ROOT_FOLDER = {"root"}; 53 54 /* package and service name of target Media Player which is set for browsing */ 55 private String mPackageName; 56 private String mConnectingPackageName; 57 private String mClassName; 58 private Context mContext; 59 private AvrcpMediaRspInterface mMediaInterface; 60 private byte[] mBDAddr; 61 62 /* Object used to connect to MediaBrowseService of Media Player */ 63 private MediaBrowser mMediaBrowser = null; 64 private MediaController mMediaController = null; 65 66 /* The mediaId to be used for subscribing for children using the MediaBrowser */ 67 private String mMediaId = null; 68 private String mRootFolderUid = null; 69 private int mConnState = DISCONNECTED; 70 71 /* stores the path trail during changePath */ 72 private Stack<String> mPathStack = null; 73 74 /* Number of items in current folder */ 75 private int mCurrFolderNumItems = 0; 76 77 /* store mapping between uid(Avrcp) and mediaId(Media Player). */ 78 private HashMap<Integer, String> mHmap = new HashMap<Integer, String>(); 79 80 /* command objects from avrcp handler */ 81 private AvrcpCmd.FolderItemsCmd mFolderItemsReqObj; 82 83 /* store result of getfolderitems with scope="vfs" */ 84 private List<MediaBrowser.MediaItem> mFolderItems = null; 85 86 /* Connection state callback handler */ 87 class MediaConnectionCallback extends MediaBrowser.ConnectionCallback { 88 private String mCallbackPackageName; 89 private MediaBrowser mBrowser; 90 91 public MediaConnectionCallback(String packageName) { 92 this.mCallbackPackageName = packageName; 93 } 94 95 public void setBrowser(MediaBrowser b) { 96 mBrowser = b; 97 } 98 99 @Override 100 public void onConnected() { 101 mConnState = CONNECTED; 102 if (DEBUG) Log.d(TAG, "mediaBrowser CONNECTED to " + mPackageName); 103 /* perform init tasks and set player as browsed player on successful connection */ 104 onBrowseConnect(mCallbackPackageName, mBrowser); 105 106 // Remove what could be a circular dependency causing GC to never happen on this object 107 mBrowser = null; 108 } 109 110 @Override 111 public void onConnectionFailed() { 112 mConnState = DISCONNECTED; 113 // Remove what could be a circular dependency causing GC to never happen on this object 114 mBrowser = null; 115 Log.e(TAG, "mediaBrowser Connection failed with " + mPackageName 116 + ", Sending fail response!"); 117 mMediaInterface.setBrowsedPlayerRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, 118 (byte)0x00, 0, null); 119 } 120 121 @Override 122 public void onConnectionSuspended() { 123 mBrowser = null; 124 mConnState = SUSPENDED; 125 Log.e(TAG, "mediaBrowser SUSPENDED connection with " + mPackageName); 126 } 127 } 128 129 /* Subscription callback handler. Subscribe to a folder to get its contents */ 130 private MediaBrowser.SubscriptionCallback folderItemsCb = 131 new MediaBrowser.SubscriptionCallback() { 132 133 @Override 134 public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children) { 135 if (DEBUG) Log.d(TAG, "OnChildren Loaded folder items: childrens= " + children.size()); 136 137 /* 138 * cache current folder items and send as rsp when remote requests 139 * get_folder_items (scope = vfs) 140 */ 141 if (mFolderItems == null) { 142 if (DEBUG) Log.d(TAG, "sending setbrowsed player rsp"); 143 mFolderItems = children; 144 mMediaInterface.setBrowsedPlayerRsp(mBDAddr, AvrcpConstants.RSP_NO_ERROR, 145 (byte)0x00, children.size(), ROOT_FOLDER); 146 } else { 147 mFolderItems = children; 148 mCurrFolderNumItems = mFolderItems.size(); 149 mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_NO_ERROR, 150 mCurrFolderNumItems); 151 } 152 mMediaBrowser.unsubscribe(parentId); 153 } 154 155 /* UID is invalid */ 156 @Override 157 public void onError(String id) { 158 Log.e(TAG, "set browsed player rsp. Could not get root folder items"); 159 mMediaInterface.setBrowsedPlayerRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, 160 (byte)0x00, 0, null); 161 } 162 }; 163 164 /* callback from media player in response to getitemAttr request */ 165 private class ItemAttribSubscriber extends MediaBrowser.SubscriptionCallback { 166 private String mMediaId; 167 private AvrcpCmd.ItemAttrCmd mAttrReq; 168 169 public ItemAttribSubscriber( 170 @NonNull AvrcpCmd.ItemAttrCmd attrReq, @NonNull String mediaId) { 171 mAttrReq = attrReq; 172 mMediaId = mediaId; 173 } 174 175 @Override 176 public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children) { 177 String logprefix = "ItemAttribSubscriber(" + mMediaId + "): "; 178 if (DEBUG) Log.d(TAG, logprefix + "OnChildren Loaded"); 179 int status = AvrcpConstants.RSP_INV_ITEM; 180 181 if (children == null) { 182 Log.w(TAG, logprefix + "children list is null parentId: " + parentId); 183 } else { 184 /* find the item in the folder */ 185 for (MediaBrowser.MediaItem item : children) { 186 if (item.getMediaId().equals(mMediaId)) { 187 if (DEBUG) Log.d(TAG, logprefix + "found item"); 188 getItemAttrFilterAttr(item); 189 status = AvrcpConstants.RSP_NO_ERROR; 190 break; 191 } 192 } 193 } 194 /* Send only error from here, in case of success, getItemAttrFilterAttr sends */ 195 if (status != AvrcpConstants.RSP_NO_ERROR) { 196 Log.e(TAG, logprefix + "not able to find item from " + parentId); 197 mMediaInterface.getItemAttrRsp(mBDAddr, status, null); 198 } 199 mMediaBrowser.unsubscribe(parentId); 200 } 201 202 @Override 203 public void onError(String id) { 204 Log.e(TAG, "Could not get attributes from media player id: " + id); 205 mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, null); 206 } 207 208 /* helper method to filter required attibuteand send GetItemAttr response */ 209 private void getItemAttrFilterAttr(@NonNull MediaBrowser.MediaItem mediaItem) { 210 /* Response parameters */ 211 int[] attrIds = null; /* array of attr ids */ 212 String[] attrValues = null; /* array of attr values */ 213 214 /* variables to temperorily add attrs */ 215 ArrayList<Integer> attrIdArray = new ArrayList<Integer>(); 216 ArrayList<String> attrValueArray = new ArrayList<String>(); 217 ArrayList<Integer> attrReqIds = new ArrayList<Integer>(); 218 219 if (mAttrReq.mNumAttr == AvrcpConstants.NUM_ATTR_NONE) { 220 // Note(jamuraa): the stack should never send this, remove? 221 Log.i(TAG, "getItemAttrFilterAttr: No attributes requested"); 222 mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_BAD_PARAM, null); 223 return; 224 } 225 226 /* check if remote device has requested all attributes */ 227 if (mAttrReq.mNumAttr == AvrcpConstants.NUM_ATTR_ALL 228 || mAttrReq.mNumAttr == AvrcpConstants.MAX_NUM_ATTR) { 229 for (int idx = 1; idx <= AvrcpConstants.MAX_NUM_ATTR; idx++) { 230 attrReqIds.add(idx); /* attr id 0x00 is unused */ 231 } 232 } else { 233 /* get only the requested attribute ids from the request */ 234 for (int idx = 0; idx < mAttrReq.mNumAttr; idx++) { 235 attrReqIds.add(mAttrReq.mAttrIDs[idx]); 236 } 237 } 238 239 /* lookup and copy values of attributes for ids requested above */ 240 for (int attrId : attrReqIds) { 241 /* check if media player provided requested attributes */ 242 String value = getAttrValue(attrId, mediaItem); 243 if (value != null) { 244 attrIdArray.add(attrId); 245 attrValueArray.add(value); 246 } 247 } 248 249 /* copy filtered attr ids and attr values to response parameters */ 250 attrIds = new int[attrIdArray.size()]; 251 for (int i = 0; i < attrIdArray.size(); i++) attrIds[i] = attrIdArray.get(i); 252 253 attrValues = attrValueArray.toArray(new String[attrIdArray.size()]); 254 255 /* create rsp object and send response */ 256 ItemAttrRsp rspObj = new ItemAttrRsp(AvrcpConstants.RSP_NO_ERROR, attrIds, attrValues); 257 mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_NO_ERROR, rspObj); 258 } 259 } 260 261 /* Constructor */ 262 public BrowsedMediaPlayer(byte[] address, Context context, 263 AvrcpMediaRspInterface mAvrcpMediaRspInterface) { 264 mContext = context; 265 mMediaInterface = mAvrcpMediaRspInterface; 266 mBDAddr = address; 267 } 268 269 /* initialize mediacontroller in order to communicate with media player. */ 270 private void onBrowseConnect(String connectedPackage, MediaBrowser browser) { 271 if (!connectedPackage.equals(mConnectingPackageName)) { 272 Log.w(TAG, "onBrowseConnect: recieved callback for package we aren't connecting to " 273 + connectedPackage); 274 return; 275 } 276 mConnectingPackageName = null; 277 278 if (browser == null) { 279 Log.e(TAG, "onBrowseConnect: received a null browser for " + connectedPackage); 280 mMediaInterface.setBrowsedPlayerRsp( 281 mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, (byte) 0x00, 0, null); 282 return; 283 } 284 285 MediaSession.Token token = null; 286 try { 287 if (!browser.isConnected()) { 288 Log.e(TAG, "setBrowsedPlayer: " + mPackageName + "not connected"); 289 } else if ((token = browser.getSessionToken()) == null) { 290 Log.e(TAG, "setBrowsedPlayer: " + mPackageName + "no Session token"); 291 } else { 292 /* update to the new MediaBrowser */ 293 if (mMediaBrowser != null) mMediaBrowser.disconnect(); 294 mMediaBrowser = browser; 295 mPackageName = connectedPackage; 296 297 /* get rootfolder uid from media player */ 298 if (mMediaId == null) { 299 mMediaId = mMediaBrowser.getRoot(); 300 /* 301 * assuming that root folder uid will not change on uids changed 302 */ 303 mRootFolderUid = mMediaId; 304 /* store root folder uid to stack */ 305 mPathStack.push(mMediaId); 306 } 307 308 mMediaController = MediaController.wrap( 309 new android.media.session.MediaController(mContext, token)); 310 /* get root folder items */ 311 mMediaBrowser.subscribe(mRootFolderUid, folderItemsCb); 312 return; 313 } 314 } catch (NullPointerException ex) { 315 Log.e(TAG, "setBrowsedPlayer : Null pointer during init"); 316 ex.printStackTrace(); 317 } 318 319 mMediaInterface.setBrowsedPlayerRsp( 320 mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, (byte) 0x00, 0, null); 321 } 322 323 public void setBrowsed(String packageName, String cls) { 324 mConnectingPackageName = packageName; 325 mClassName = cls; 326 /* cleanup variables from previous browsed calls */ 327 mFolderItems = null; 328 mMediaId = null; 329 mRootFolderUid = null; 330 /* 331 * create stack to store the navigation trail (current folder ID). This 332 * will be required while navigating up the folder 333 */ 334 mPathStack = new Stack<String>(); 335 336 /* Bind to MediaBrowseService of MediaPlayer */ 337 MediaConnectionCallback callback = new MediaConnectionCallback(packageName); 338 MediaBrowser tempBrowser = new MediaBrowser( 339 mContext, new ComponentName(packageName, mClassName), callback, null); 340 callback.setBrowser(tempBrowser); 341 342 tempBrowser.connect(); 343 } 344 345 /* called when connection to media player is closed */ 346 public void cleanup() { 347 if (DEBUG) Log.d(TAG, "cleanup"); 348 349 if (mConnState != DISCONNECTED) { 350 mMediaBrowser.disconnect(); 351 } 352 353 mHmap = null; 354 mMediaController = null; 355 mMediaBrowser = null; 356 mPathStack = null; 357 } 358 359 public boolean isPlayerConnected() { 360 if (mMediaBrowser == null) { 361 if (DEBUG) Log.d(TAG, "isPlayerConnected: mMediaBrowser = null!"); 362 return false; 363 } 364 365 return mMediaBrowser.isConnected(); 366 } 367 368 /* returns number of items in new path as reponse */ 369 public void changePath(byte[] folderUid, byte direction) { 370 if (DEBUG) Log.d(TAG, "changePath.direction = " + direction); 371 String newPath = ""; 372 373 if (isPlayerConnected() == false) { 374 Log.w(TAG, "changePath: disconnected from player service, sending internal error"); 375 mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, 0); 376 return; 377 } 378 379 if (mMediaBrowser == null) { 380 Log.e(TAG, "Media browser is null, sending internal error"); 381 mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, 0); 382 return; 383 } 384 385 /* check direction and change the path */ 386 if (direction == AvrcpConstants.DIR_DOWN) { /* move down */ 387 if ((newPath = byteToString(folderUid)) == null) { 388 Log.e(TAG, "Could not get media item from folder Uid, sending err response"); 389 mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INV_ITEM, 0); 390 } else if (isBrowsableFolderDn(newPath) == false) { 391 /* new path is not browsable */ 392 Log.e(TAG, "ItemUid received from changePath cmd is not browsable"); 393 mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INV_DIRECTORY, 0); 394 } else if (mPathStack.peek().equals(newPath) == true) { 395 /* new_folder is same as current folder */ 396 Log.e(TAG, "new_folder is same as current folder, Invalid direction!"); 397 mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INV_DIRN, 0); 398 } else { 399 mMediaBrowser.subscribe(newPath, folderItemsCb); 400 /* assume that call is success and update stack with new folder path */ 401 mPathStack.push(newPath); 402 } 403 } else if (direction == AvrcpConstants.DIR_UP) { /* move up */ 404 if (isBrowsableFolderUp() == false) { 405 /* Already on the root, cannot allow up: PTS: test case TC_TG_MCN_CB_BI_02_C 406 * This is required, otherwise some CT will keep on sending change path up 407 * until they receive error */ 408 Log.w(TAG, "Cannot go up from now, already in the root, Invalid direction!"); 409 mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INV_DIRN, 0); 410 } else { 411 /* move folder up */ 412 mPathStack.pop(); 413 newPath = mPathStack.peek(); 414 mMediaBrowser.subscribe(newPath, folderItemsCb); 415 } 416 } else { /* invalid direction */ 417 Log.w(TAG, "changePath : Invalid direction " + direction); 418 mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INV_DIRN, 0); 419 } 420 } 421 422 public void getItemAttr(AvrcpCmd.ItemAttrCmd itemAttr) { 423 String mediaID; 424 if (DEBUG) Log.d(TAG, "getItemAttr"); 425 426 /* check if uid is valid by doing a lookup in hashmap */ 427 mediaID = byteToString(itemAttr.mUid); 428 if (mediaID == null) { 429 Log.e(TAG, "uid is invalid"); 430 mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_INV_ITEM, null); 431 return; 432 } 433 434 /* check scope */ 435 if (itemAttr.mScope != AvrcpConstants.BTRC_SCOPE_FILE_SYSTEM) { 436 Log.e(TAG, "invalid scope"); 437 mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_INV_SCOPE, null); 438 return; 439 } 440 441 if (mMediaBrowser == null) { 442 Log.e(TAG, "mMediaBrowser is null"); 443 mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, null); 444 return; 445 } 446 447 /* Subscribe to the parent to list items and retrieve the right one */ 448 mMediaBrowser.subscribe(mPathStack.peek(), new ItemAttribSubscriber(itemAttr, mediaID)); 449 } 450 451 public void getTotalNumOfItems(byte scope) { 452 if (DEBUG) Log.d(TAG, "getTotalNumOfItems scope = " + scope); 453 if (scope != AvrcpConstants.BTRC_SCOPE_FILE_SYSTEM) { 454 Log.e(TAG, "getTotalNumOfItems error" + scope); 455 mMediaInterface.getTotalNumOfItemsRsp(mBDAddr, AvrcpConstants.RSP_INV_SCOPE, 0, 0); 456 return; 457 } 458 459 if (mFolderItems == null) { 460 Log.e(TAG, "mFolderItems is null, sending internal error"); 461 /* folderitems were not fetched during change path */ 462 mMediaInterface.getTotalNumOfItemsRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, 0, 0); 463 return; 464 } 465 466 /* find num items using size of already cached folder items */ 467 mMediaInterface.getTotalNumOfItemsRsp( 468 mBDAddr, AvrcpConstants.RSP_NO_ERROR, 0, mFolderItems.size()); 469 } 470 471 public void getFolderItemsVFS(AvrcpCmd.FolderItemsCmd reqObj) { 472 if (!isPlayerConnected()) { 473 Log.e(TAG, "unable to connect to media player, sending internal error"); 474 /* unable to connect to media player. Send error response to remote device */ 475 mMediaInterface.folderItemsRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, null); 476 return; 477 } 478 479 if (DEBUG) Log.d(TAG, "getFolderItemsVFS"); 480 mFolderItemsReqObj = reqObj; 481 482 if (mFolderItems == null) { 483 /* Failed to fetch folder items from media player. Send error to remote device */ 484 Log.e(TAG, "Failed to fetch folder items during getFolderItemsVFS"); 485 mMediaInterface.folderItemsRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, null); 486 return; 487 } 488 489 /* Filter attributes based on the request and send response to remote device */ 490 getFolderItemsFilterAttr(mBDAddr, reqObj, mFolderItems, 491 AvrcpConstants.BTRC_SCOPE_FILE_SYSTEM, mFolderItemsReqObj.mStartItem, 492 mFolderItemsReqObj.mEndItem); 493 } 494 495 /* Instructs media player to play particular media item */ 496 public void playItem(byte[] uid, byte scope) { 497 String folderUid; 498 499 if (isPlayerConnected()) { 500 /* check if uid is valid */ 501 if ((folderUid = byteToString(uid)) == null) { 502 Log.e(TAG, "uid is invalid!"); 503 mMediaInterface.playItemRsp(mBDAddr, AvrcpConstants.RSP_INV_ITEM); 504 return; 505 } 506 507 if (mMediaController != null) { 508 MediaController.TransportControls mediaControllerCntrl = 509 mMediaController.getTransportControls(); 510 if (DEBUG) Log.d(TAG, "Sending playID: " + folderUid); 511 512 if (scope == AvrcpConstants.BTRC_SCOPE_FILE_SYSTEM) { 513 mediaControllerCntrl.playFromMediaId(folderUid, null); 514 mMediaInterface.playItemRsp(mBDAddr, AvrcpConstants.RSP_NO_ERROR); 515 } else { 516 Log.e(TAG, "playItem received for invalid scope!"); 517 mMediaInterface.playItemRsp(mBDAddr, AvrcpConstants.RSP_INV_SCOPE); 518 } 519 } else { 520 Log.e(TAG, "mediaController is null"); 521 mMediaInterface.playItemRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR); 522 } 523 } else { 524 Log.e(TAG, "playItem: Not connected to media player"); 525 mMediaInterface.playItemRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR); 526 } 527 } 528 529 /* 530 * helper method to check if startItem and endItem index is with range of 531 * MediaItem list. (Resultset containing all items in current path) 532 */ 533 private List<MediaBrowser.MediaItem> checkIndexOutofBounds( 534 byte[] bdaddr, List<MediaBrowser.MediaItem> children, long startItem, long endItem) { 535 if (endItem >= children.size()) endItem = children.size() - 1; 536 if (startItem >= Integer.MAX_VALUE) startItem = Integer.MAX_VALUE; 537 try { 538 List<MediaBrowser.MediaItem> childrenSubList = 539 children.subList((int) startItem, (int) endItem + 1); 540 if (childrenSubList.isEmpty()) { 541 Log.i(TAG, "childrenSubList is empty."); 542 throw new IndexOutOfBoundsException(); 543 } 544 return childrenSubList; 545 } catch (IndexOutOfBoundsException ex) { 546 Log.w(TAG, "Index out of bounds start item ="+ startItem + " end item = "+ 547 Math.min(children.size(), endItem + 1)); 548 return null; 549 } catch (IllegalArgumentException ex) { 550 Log.i(TAG, "Index out of bounds start item =" + startItem + " > size"); 551 return null; 552 } 553 } 554 555 556 /* 557 * helper method to filter required attibutes before sending GetFolderItems response 558 */ 559 public void getFolderItemsFilterAttr(byte[] bdaddr, AvrcpCmd.FolderItemsCmd mFolderItemsReqObj, 560 List<MediaBrowser.MediaItem> children, byte scope, long startItem, long endItem) { 561 if (DEBUG) 562 Log.d(TAG, 563 "getFolderItemsFilterAttr: startItem =" + startItem + ", endItem = " + endItem); 564 565 List<MediaBrowser.MediaItem> result_items = new ArrayList<MediaBrowser.MediaItem>(); 566 567 if (children == null) { 568 Log.e(TAG, "Error: children are null in getFolderItemsFilterAttr"); 569 mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_INV_RANGE, null); 570 return; 571 } 572 573 /* check for index out of bound errors */ 574 result_items = checkIndexOutofBounds(bdaddr, children, startItem, endItem); 575 if (result_items == null) { 576 Log.w(TAG, "result_items is null."); 577 mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_INV_RANGE, null); 578 return; 579 } 580 FolderItemsData folderDataNative = new FolderItemsData(result_items.size()); 581 582 /* variables to temperorily add attrs */ 583 ArrayList<String> attrArray = new ArrayList<String>(); 584 ArrayList<Integer> attrId = new ArrayList<Integer>(); 585 586 for (int itemIndex = 0; itemIndex < result_items.size(); itemIndex++) { 587 /* item type. Needs to be set by media player */ 588 MediaBrowser.MediaItem item = result_items.get(itemIndex); 589 int flags = item.getFlags(); 590 if ((flags & MediaBrowser.MediaItem.FLAG_BROWSABLE) != 0) { 591 folderDataNative.mItemTypes[itemIndex] = AvrcpConstants.BTRC_ITEM_FOLDER; 592 } else { 593 folderDataNative.mItemTypes[itemIndex] = AvrcpConstants.BTRC_ITEM_MEDIA; 594 } 595 596 /* set playable */ 597 if ((flags & MediaBrowser.MediaItem.FLAG_PLAYABLE) != 0) { 598 folderDataNative.mPlayable[itemIndex] = AvrcpConstants.ITEM_PLAYABLE; 599 } else { 600 folderDataNative.mPlayable[itemIndex] = AvrcpConstants.ITEM_NOT_PLAYABLE; 601 } 602 /* set uid for current item */ 603 byte[] uid = stringToByte(item.getDescription().getMediaId()); 604 for (int idx = 0; idx < AvrcpConstants.UID_SIZE; idx++) { 605 folderDataNative.mItemUid[itemIndex * AvrcpConstants.UID_SIZE + idx] = uid[idx]; 606 } 607 608 /* Set display name for current item */ 609 folderDataNative.mDisplayNames[itemIndex] = 610 getAttrValue(AvrcpConstants.ATTRID_TITLE, item); 611 612 int maxAttributesRequested = 0; 613 boolean isAllAttribRequested = false; 614 /* check if remote requested for attributes */ 615 if (mFolderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) { 616 int attrCnt = 0; 617 618 /* add requested attr ids to a temp array */ 619 if (mFolderItemsReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) { 620 isAllAttribRequested = true; 621 maxAttributesRequested = AvrcpConstants.MAX_NUM_ATTR; 622 } else { 623 /* get only the requested attribute ids from the request */ 624 maxAttributesRequested = mFolderItemsReqObj.mNumAttr; 625 } 626 627 /* lookup and copy values of attributes for ids requested above */ 628 for (int idx = 0; idx < maxAttributesRequested; idx++) { 629 /* check if media player provided requested attributes */ 630 String value = null; 631 632 int attribId = isAllAttribRequested ? (idx + 1) : 633 mFolderItemsReqObj.mAttrIDs[idx]; 634 value = getAttrValue(attribId, result_items.get(itemIndex)); 635 if (value != null) { 636 attrArray.add(value); 637 attrId.add(attribId); 638 attrCnt++; 639 } 640 } 641 /* add num attr actually received from media player for a particular item */ 642 folderDataNative.mAttributesNum[itemIndex] = attrCnt; 643 } 644 } 645 646 /* copy filtered attr ids and attr values to response parameters */ 647 if (attrId.size() > 0) { 648 folderDataNative.mAttrIds = new int[attrId.size()]; 649 for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++) 650 folderDataNative.mAttrIds[attrIndex] = attrId.get(attrIndex); 651 folderDataNative.mAttrValues = attrArray.toArray(new String[attrArray.size()]); 652 } 653 654 /* create rsp object and send response to remote device */ 655 FolderItemsRsp rspObj = new FolderItemsRsp(AvrcpConstants.RSP_NO_ERROR, Avrcp.sUIDCounter, 656 scope, folderDataNative.mNumItems, folderDataNative.mFolderTypes, 657 folderDataNative.mPlayable, folderDataNative.mItemTypes, folderDataNative.mItemUid, 658 folderDataNative.mDisplayNames, folderDataNative.mAttributesNum, 659 folderDataNative.mAttrIds, folderDataNative.mAttrValues); 660 mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj); 661 } 662 663 public static String getAttrValue(int attr, MediaBrowser.MediaItem item) { 664 String attrValue = null; 665 try { 666 MediaDescription desc = item.getDescription(); 667 Bundle extras = desc.getExtras(); 668 switch (attr) { 669 /* Title is mandatory attribute */ 670 case AvrcpConstants.ATTRID_TITLE: 671 attrValue = desc.getTitle().toString(); 672 break; 673 674 case AvrcpConstants.ATTRID_ARTIST: 675 attrValue = extras.getString(MediaMetadata.METADATA_KEY_ARTIST); 676 break; 677 678 case AvrcpConstants.ATTRID_ALBUM: 679 attrValue = extras.getString(MediaMetadata.METADATA_KEY_ALBUM); 680 break; 681 682 case AvrcpConstants.ATTRID_TRACK_NUM: 683 attrValue = extras.getString(MediaMetadata.METADATA_KEY_TRACK_NUMBER); 684 break; 685 686 case AvrcpConstants.ATTRID_NUM_TRACKS: 687 attrValue = extras.getString(MediaMetadata.METADATA_KEY_NUM_TRACKS); 688 break; 689 690 case AvrcpConstants.ATTRID_GENRE: 691 attrValue = extras.getString(MediaMetadata.METADATA_KEY_GENRE); 692 break; 693 694 case AvrcpConstants.ATTRID_PLAY_TIME: 695 attrValue = extras.getString(MediaMetadata.METADATA_KEY_DURATION); 696 break; 697 698 case AvrcpConstants.ATTRID_COVER_ART: 699 Log.e(TAG, "getAttrValue: Cover art attribute not supported"); 700 return null; 701 702 default: 703 Log.e(TAG, "getAttrValue: Unknown attribute ID requested: " + attr); 704 return null; 705 } 706 } catch (NullPointerException ex) { 707 Log.w(TAG, "getAttrValue: attr id not found in result"); 708 /* checking if attribute is title, then it is mandatory and cannot send null */ 709 if (attr == AvrcpConstants.ATTRID_TITLE) { 710 attrValue = "<Unknown Title>"; 711 } else { 712 return null; 713 } 714 } 715 if (DEBUG) Log.d(TAG, "getAttrValue: attrvalue = " + attrValue + "attr id:" + attr); 716 return attrValue; 717 } 718 719 720 public String getPackageName() { 721 return mPackageName; 722 } 723 724 /* Helper methods */ 725 726 /* check if item is browsable Down*/ 727 private boolean isBrowsableFolderDn(String uid) { 728 for (MediaBrowser.MediaItem item : mFolderItems) { 729 if (item.getMediaId().equals(uid) && 730 ((item.getFlags() & MediaBrowser.MediaItem.FLAG_BROWSABLE) == 731 MediaBrowser.MediaItem.FLAG_BROWSABLE)) 732 return true; 733 } 734 return false; 735 } 736 737 /* check if browsable Up*/ 738 private boolean isBrowsableFolderUp() { 739 if (mPathStack.peek().equals(mRootFolderUid)) { 740 /* Already on the root, cannot go up */ 741 return false; 742 } 743 return true; 744 } 745 746 /* convert uid to mediaId */ 747 private String byteToString(byte[] byteArray) { 748 int uid = new BigInteger(byteArray).intValue(); 749 String mediaId = mHmap.get(uid); 750 return mediaId; 751 } 752 753 /* convert mediaId to uid */ 754 private byte[] stringToByte(String mediaId) { 755 /* check if this mediaId already exists in hashmap */ 756 if (!mHmap.containsValue(mediaId)) { /* add to hashmap */ 757 // Offset by one as uid 0 is reserved 758 int uid = mHmap.size() + 1; 759 mHmap.put(uid, mediaId); 760 return intToByteArray(uid); 761 } else { /* search key for give mediaId */ 762 for (int uid : mHmap.keySet()) { 763 if (mHmap.get(uid).equals(mediaId)) { 764 return intToByteArray(uid); 765 } 766 } 767 } 768 return null; 769 } 770 771 /* converts queue item received from getQueue call, to MediaItem used by FilterAttr method */ 772 private List<MediaBrowser.MediaItem> queueItem2MediaItem( 773 List<MediaSession.QueueItem> tempItems) { 774 775 List<MediaBrowser.MediaItem> tempMedia = new ArrayList<MediaBrowser.MediaItem>(); 776 for (int itemCount = 0; itemCount < tempItems.size(); itemCount++) { 777 MediaDescription.Builder build = new MediaDescription.Builder(); 778 build.setMediaId(Long.toString(tempItems.get(itemCount).getQueueId())); 779 build.setTitle(tempItems.get(itemCount).getDescription().getTitle()); 780 build.setExtras(tempItems.get(itemCount).getDescription().getExtras()); 781 MediaDescription des = build.build(); 782 MediaItem item = new MediaItem((des), MediaItem.FLAG_PLAYABLE); 783 tempMedia.add(item); 784 } 785 return tempMedia; 786 } 787 788 /* convert integer to byte array of size 8 bytes */ 789 public byte[] intToByteArray(int value) { 790 int index = 0; 791 byte[] encodedValue = new byte[AvrcpConstants.UID_SIZE]; 792 793 encodedValue[index++] = (byte)0x00; 794 encodedValue[index++] = (byte)0x00; 795 encodedValue[index++] = (byte)0x00; 796 encodedValue[index++] = (byte)0x00; 797 encodedValue[index++] = (byte)(value >> 24); 798 encodedValue[index++] = (byte)(value >> 16); 799 encodedValue[index++] = (byte)(value >> 8); 800 encodedValue[index++] = (byte)value; 801 802 return encodedValue; 803 } 804 } 805