• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
MediaConnectionCallback(String packageName)91         public MediaConnectionCallback(String packageName) {
92             this.mCallbackPackageName = packageName;
93         }
94 
setBrowser(MediaBrowser b)95         public void setBrowser(MediaBrowser b) {
96             mBrowser = b;
97         }
98 
99         @Override
onConnected()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
onConnectionFailed()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
onConnectionSuspended()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 
ItemAttribSubscriber( @onNull AvrcpCmd.ItemAttrCmd attrReq, @NonNull String mediaId)169         public ItemAttribSubscriber(
170                 @NonNull AvrcpCmd.ItemAttrCmd attrReq, @NonNull String mediaId) {
171             mAttrReq = attrReq;
172             mMediaId = mediaId;
173         }
174 
175         @Override
onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children)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
onError(String id)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 */
getItemAttrFilterAttr(@onNull MediaBrowser.MediaItem mediaItem)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 */
BrowsedMediaPlayer(byte[] address, Context context, AvrcpMediaRspInterface mAvrcpMediaRspInterface)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. */
onBrowseConnect(String connectedPackage, MediaBrowser browser)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 
setBrowsed(String packageName, String cls)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 */
cleanup()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 
isPlayerConnected()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 */
changePath(byte[] folderUid, byte direction)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 
getItemAttr(AvrcpCmd.ItemAttrCmd itemAttr)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 
getTotalNumOfItems(byte scope)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 
getFolderItemsVFS(AvrcpCmd.FolderItemsCmd reqObj)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 */
playItem(byte[] uid, byte scope)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      */
checkIndexOutofBounds( byte[] bdaddr, List<MediaBrowser.MediaItem> children, long startItem, long endItem)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      */
getFolderItemsFilterAttr(byte[] bdaddr, AvrcpCmd.FolderItemsCmd mFolderItemsReqObj, List<MediaBrowser.MediaItem> children, byte scope, long startItem, long endItem)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 
getAttrValue(int attr, MediaBrowser.MediaItem item)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 
getPackageName()720     public String getPackageName() {
721         return mPackageName;
722     }
723 
724     /* Helper methods */
725 
726     /* check if item is browsable Down*/
isBrowsableFolderDn(String uid)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*/
isBrowsableFolderUp()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 */
byteToString(byte[] byteArray)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 */
stringToByte(String mediaId)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 */
queueItem2MediaItem( List<MediaSession.QueueItem> tempItems)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 */
intToByteArray(int value)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