OSDN Git Service

Implement non-blocking pagination for a long song list.
authorSanket Agarwal <sanketa@google.com>
Fri, 26 Aug 2016 00:56:33 +0000 (17:56 -0700)
committerSanket Agarwal <sanketa@google.com>
Mon, 31 Oct 2016 23:04:00 +0000 (16:04 -0700)
Bug: b/31253501

Change-Id: I39f7ba180a33729301618db8191821ad2745917f

jni/com_android_bluetooth_avrcp_controller.cpp
src/com/android/bluetooth/a2dpsink/mbs/A2dpMediaBrowserService.java
src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java
src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java
src/com/android/bluetooth/avrcpcontroller/BrowseTree.java

index 41ab260..c0d0dba 100644 (file)
@@ -371,7 +371,7 @@ static void btavrcp_play_status_changed_callback(bt_bdaddr_t *bd_addr,
 }
 
 static void btavrcp_get_folder_items_callback(bt_bdaddr_t *bd_addr,
-        const btrc_folder_items_t *folder_items, uint8_t count) {
+        btrc_status_t status, const btrc_folder_items_t *folder_items, uint8_t count) {
     /* Folder items are list of items that can be either BTRC_ITEM_PLAYER
      * BTRC_ITEM_MEDIA, BTRC_ITEM_FOLDER. Here we translate them to their java
      * counterparts by calling the java constructor for each of the items.
@@ -516,7 +516,7 @@ static void btavrcp_get_folder_items_callback(bt_bdaddr_t *bd_addr,
                                      playerItemArray);
     } else {
         sCallbackEnv->CallVoidMethod(sCallbacksObj, method_handleGetFolderItemsRsp,
-                                     folderItemArray);
+                                     status, folderItemArray);
     }
     if (isPlayerListing) {
         sCallbackEnv->DeleteLocalRef(playerItemArray);
@@ -600,7 +600,7 @@ static void classInitNative(JNIEnv* env, jclass clazz) {
         env->GetMethodID(clazz, "onPlayStatusChanged", "([BB)V");
 
     method_handleGetFolderItemsRsp =
-        env->GetMethodID(clazz, "handleGetFolderItemsRsp", "([Landroid/media/browse/MediaBrowser$MediaItem;)V");
+        env->GetMethodID(clazz, "handleGetFolderItemsRsp", "(I[Landroid/media/browse/MediaBrowser$MediaItem;)V");
     method_handleGetPlayerItemsRsp =
         env->GetMethodID(clazz, "handleGetPlayerItemsRsp",
                          "([Lcom/android/bluetooth/avrcpcontroller/AvrcpPlayer;)V");
index fec4cf4..36bdd9c 100644 (file)
@@ -477,12 +477,16 @@ public class A2dpMediaBrowserService extends MediaBrowserService {
         String id = intent.getStringExtra(AvrcpControllerService.EXTRA_FOLDER_ID);
         Log.d(TAG, "Parent: " + id + " Folder list: " + folderList);
         synchronized (this) {
+            // If we have a result object then we should send the result back
+            // to client since it is blocking otherwise we may have gotten more items
+            // from remote device, hence let client know to fetch again.
             Result<List<MediaItem>> results = mParentIdToRequestMap.remove(id);
             if (results == null) {
-                Log.w(TAG, "Request no longer exists, hence ignoring reply!");
-                return;
+                Log.w(TAG, "Request no longer exists, notifying that children changed.");
+                notifyChildrenChanged(id);
+            } else {
+                results.sendResult(folderList);
             }
-            results.sendResult(folderList);
         }
     }
 
index 7b5d7cc..a646576 100644 (file)
@@ -85,6 +85,13 @@ public class AvrcpControllerService extends ProfileService {
     private static final int JNI_FOLDER_TYPE_PLAYLISTS = 0x05;
     private static final int JNI_FOLDER_TYPE_YEARS = 0x06;
 
+    /*
+     * AVRCP Error types as defined in spec. Also they should be in sync with btrc_status_t.
+     * NOTE: Not all may be defined.
+     */
+    private static final int JNI_AVRC_STS_NO_ERROR = 0x04;
+    private static final int JNI_AVRC_INV_RANGE = 0x0b;
+
     /**
      * Intent used to broadcast the change in browse connection state of the AVRCP Controller
      * profile.
@@ -911,10 +918,22 @@ public class AvrcpControllerService extends ProfileService {
     }
 
     // Browsing related JNI callbacks.
-    void handleGetFolderItemsRsp(MediaItem[] items) {
+    void handleGetFolderItemsRsp(int status, MediaItem[] items) {
         if (DBG) {
-            Log.d(TAG, "handleGetFolderItemsRsp called with " + items.length + " items.");
+            Log.d(TAG, "handleGetFolderItemsRsp called with status " + status +
+                " items "  + items.length + " items.");
         }
+
+        if (status == JNI_AVRC_INV_RANGE) {
+            Log.w(TAG, "Sending out of range message.");
+            // Send a special message since this could be used by state machine
+            // to take as a signal that fetch is finished.
+            Message msg = mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.
+                MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE);
+            mAvrcpCtSm.sendMessage(msg);
+            return;
+        }
+
         for (MediaItem item : items) {
             if (DBG) {
                 Log.d(TAG, "media item: " + item + " uid: " + item.getDescription().getMediaId());
@@ -1081,11 +1100,11 @@ public class AvrcpControllerService extends ProfileService {
         int label);
 
     /* API used to fetch the current now playing list */
-    native static void getNowPlayingListNative(byte[] address, byte start, byte items);
+    native static void getNowPlayingListNative(byte[] address, byte start, byte end);
     /* API used to fetch the current folder's listing */
-    native static void getFolderListNative(byte[] address, byte start, byte items);
+    native static void getFolderListNative(byte[] address, byte start, byte end);
     /* API used to fetch the listing of players */
-    native static void getPlayerListNative(byte[] address, byte start, byte items);
+    native static void getPlayerListNative(byte[] address, byte start, byte end);
     /* API used to change the folder */
     native static void changeFolderPathNative(byte[] address, byte direction, byte[] uid);
     native static void playItemNative(
index 874512b..d7fa8b1 100644 (file)
@@ -69,8 +69,9 @@ class AvrcpControllerStateMachine extends StateMachine {
     static final int MESSAGE_PROCESS_PLAY_STATUS_CHANGED = 107;
     static final int MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION = 108;
     static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS = 109;
-    static final int MESSAGE_PROCESS_GET_PLAYER_ITEMS = 110;
-    static final int MESSAGE_PROCESS_FOLDER_PATH = 111;
+    static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE = 110;
+    static final int MESSAGE_PROCESS_GET_PLAYER_ITEMS = 111;
+    static final int MESSAGE_PROCESS_FOLDER_PATH = 112;
     static final int MESSAGE_PROCESS_SET_BROWSED_PLAYER = 113;
 
     // commands from A2DP sink
@@ -88,6 +89,8 @@ class AvrcpControllerStateMachine extends StateMachine {
     static final int MESSAGE_INTERNAL_CMD_TIMEOUT = 403;
 
     static final int CMD_TIMEOUT_MILLIS = 5000; // 5s
+    // Fetch only 5 items at a time.
+    static final int GET_FOLDER_ITEMS_PAGINATION_SIZE = 5;
 
     /*
      * Base value for absolute volume from JNI
@@ -102,8 +105,8 @@ class AvrcpControllerStateMachine extends StateMachine {
 
 
     private static final String TAG = "AvrcpControllerSM";
-    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
-    private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
+    private static final boolean DBG = true;
+    private static final boolean VDBG = true;
 
     private final Context mContext;
     private final AudioManager mAudioManager;
@@ -112,9 +115,8 @@ class AvrcpControllerStateMachine extends StateMachine {
     private final State mConnected;
     private final SetBrowsedPlayer mSetBrowsedPlayer;
     private final ChangeFolderPath mChangeFolderPath;
-    private final GetFolderList mGetFolderListing;
+    private final GetFolderList mGetFolderList;
     private final GetPlayerListing mGetPlayerListing;
-    private final GetNowPlayingList mGetNowPlayingList;
     private final MoveToRoot mMoveToRoot;
 
     private final Object mLock = new Object();
@@ -155,9 +157,8 @@ class AvrcpControllerStateMachine extends StateMachine {
         // Used to change folder path and fetch the new folder listing.
         mSetBrowsedPlayer = new SetBrowsedPlayer();
         mChangeFolderPath = new ChangeFolderPath();
-        mGetFolderListing = new GetFolderList();
+        mGetFolderList = new GetFolderList();
         mGetPlayerListing = new GetPlayerListing();
-        mGetNowPlayingList = new GetNowPlayingList();
         mMoveToRoot = new MoveToRoot();
 
         addState(mDisconnected);
@@ -169,9 +170,8 @@ class AvrcpControllerStateMachine extends StateMachine {
         // deferred so that once we transition to the mConnected we can process them hence.
         addState(mSetBrowsedPlayer, mConnected);
         addState(mChangeFolderPath, mConnected);
-        addState(mGetFolderListing, mConnected);
+        addState(mGetFolderList, mConnected);
         addState(mGetPlayerListing, mConnected);
-        addState(mGetNowPlayingList, mConnected);
         addState(mMoveToRoot, mConnected);
 
         setInitialState(mDisconnected);
@@ -185,6 +185,7 @@ class AvrcpControllerStateMachine extends StateMachine {
             switch (msg.what) {
                 case MESSAGE_PROCESS_CONNECTION_CHANGE:
                     if (msg.arg1 == BluetoothProfile.STATE_CONNECTED) {
+                        mBrowseTree.init();
                         transitionTo(mConnected);
                         BluetoothDevice rtDevice = (BluetoothDevice) msg.obj;
                         synchronized(mLock) {
@@ -212,7 +213,6 @@ class AvrcpControllerStateMachine extends StateMachine {
     }
 
     class Connected extends State {
-
         @Override
         public boolean processMessage(Message msg) {
             Log.d(TAG, " HandleMessage: " + dumpMessageString(msg.what));
@@ -252,23 +252,19 @@ class AvrcpControllerStateMachine extends StateMachine {
                         break;
 
                     case MESSAGE_GET_NOW_PLAYING_LIST:
-                        AvrcpControllerService.getNowPlayingListNative(
-                            mRemoteDevice.getBluetoothAddress(), (byte) msg.arg1,
-                            (byte) msg.arg2);
-                        mGetNowPlayingList.setFolder((String) msg.obj);
-                        transitionTo(mGetNowPlayingList);
+                        mGetFolderList.setFolder((String) msg.obj);
+                        mGetFolderList.setBounds((int) msg.arg1, (int) msg.arg2);
+                        mGetFolderList.setScope(AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING);
+                        transitionTo(mGetFolderList);
                         break;
 
                     case MESSAGE_GET_FOLDER_LIST:
-                        AvrcpControllerService.getFolderListNative(
-                            mRemoteDevice.getBluetoothAddress(), (byte) msg.arg1,
-                            (byte) msg.arg2);
-
                         // Whenever we transition we set the information for folder we need to
                         // return result.
-                        mGetFolderListing.setFolder((String) msg.obj);
-                        transitionTo(mGetFolderListing);
-                        sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
+                        mGetFolderList.setBounds(msg.arg1, msg.arg2);
+                        mGetFolderList.setFolder((String) msg.obj);
+                        mGetFolderList.setScope(AvrcpControllerService.BROWSE_SCOPE_VFS);
+                        transitionTo(mGetFolderList);
                         break;
 
                     case MESSAGE_GET_PLAYER_LIST:
@@ -322,6 +318,7 @@ class AvrcpControllerStateMachine extends StateMachine {
                                 mIsConnected = false;
                                 mRemoteDevice = null;
                             }
+                            mBrowseTree.clear();
                             transitionTo(mDisconnected);
                             BluetoothDevice rtDevice = (BluetoothDevice) msg.obj;
                             Intent intent = new Intent(
@@ -482,7 +479,7 @@ class AvrcpControllerStateMachine extends StateMachine {
                     Log.d(STATE_TAG, "New browse depth " + mBrowseDepth);
 
                     if (msg.arg1 > 0) {
-                        sendMessage(MESSAGE_GET_FOLDER_LIST, 0, 0xff, mID);
+                        sendMessage(MESSAGE_GET_FOLDER_LIST, 0, msg.arg1 -1, mID);
                     } else {
                         // Return an empty response to the upper layer.
                         broadcastFolderList(mID, mEmptyMediaItemList);
@@ -515,42 +512,80 @@ class AvrcpControllerStateMachine extends StateMachine {
         private String STATE_TAG = "AVRCPSM.GetFolderList";
 
         String mID = "";
+        int mStartInd;
+        int mEndInd;
+        int mCurrInd;
+        int mScope;
+        private ArrayList<MediaItem> mFolderList = new ArrayList<>();
+
+        @Override
+        public void enter() {
+            mCurrInd = 0;
+            mFolderList.clear();
+
+            callNativeFunctionForScope(
+                mStartInd, Math.min(mEndInd, mStartInd + GET_FOLDER_ITEMS_PAGINATION_SIZE - 1));
+        }
+
+        public void setScope(int scope) {
+            mScope = scope;
+        }
 
         public void setFolder(String id) {
             Log.d(STATE_TAG, "Setting folder to " + id);
             mID = id;
         }
 
+        public void setBounds(int startInd, int endInd) {
+            if (DBG) {
+                Log.d(STATE_TAG, "startInd " + startInd + " endInd " + endInd);
+            }
+            mStartInd = startInd;
+            mEndInd = endInd;
+        }
+
         @Override
         public boolean processMessage(Message msg) {
             Log.d(STATE_TAG, "processMessage " + msg);
             switch (msg.what) {
                 case MESSAGE_PROCESS_GET_FOLDER_ITEMS:
                     ArrayList<MediaItem> folderList = (ArrayList<MediaItem>) msg.obj;
-                    BrowseTree.BrowseNode bn = mBrowseTree.findBrowseNodeByID(mID);
-                    if (bn.isPlayer()) {
-                        // Add the now playing folder.
-                        MediaDescription.Builder mdb = new MediaDescription.Builder();
-                        mdb.setMediaId(BrowseTree.NOW_PLAYING_PREFIX + ":" +
-                            bn.getPlayerID());
-                        mdb.setTitle(BrowseTree.NOW_PLAYING_PREFIX);
-                        Bundle mdBundle = new Bundle();
-                        mdBundle.putString(
-                            AvrcpControllerService.MEDIA_ITEM_UID_KEY,
-                            BrowseTree.NOW_PLAYING_PREFIX + ":" + bn.getID());
-                        mdb.setExtras(mdBundle);
-                        folderList.add(new MediaItem(mdb.build(), MediaItem.FLAG_BROWSABLE));
+                    mFolderList.addAll(folderList);
+                    if (DBG) {
+                        Log.d(STATE_TAG, "Start " + mStartInd + " End " + mEndInd + " Curr " +
+                            mCurrInd + " received " + folderList.size());
                     }
+                    mCurrInd += folderList.size();
 
-                    mBrowseTree.refreshChildren(bn, folderList);
-                    broadcastFolderList(mID, folderList);
+                    // Always update the node so that the user does not wait forever
+                    // for the list to populate.
+                    sendFolderBroadcastAndUpdateNode();
 
-                    transitionTo(mConnected);
+                    if (mCurrInd > mEndInd) {
+                        transitionTo(mConnected);
+                    } else {
+                        // Fetch the next set of items.
+                        callNativeFunctionForScope(
+                            (byte) mCurrInd,
+                            (byte) Math.min(
+                                mEndInd, mCurrInd + GET_FOLDER_ITEMS_PAGINATION_SIZE - 1));
+                        // Reset the timeout message since we are doing a new fetch now.
+                        removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT);
+                        sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
+                    }
                     break;
 
                 case MESSAGE_INTERNAL_CMD_TIMEOUT:
-                    // We have timed out to execute the request.
-                    broadcastFolderList(mID, mEmptyMediaItemList);
+                    // We have timed out to execute the request, we should simply send
+                    // whatever listing we have gotten until now.
+                    sendFolderBroadcastAndUpdateNode();
+                    transitionTo(mConnected);
+                    break;
+
+                case MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE:
+                    // If we have gotten an error for OUT OF RANGE we have
+                    // already sent all the items to the client hence simply
+                    // transition to Connected state here.
                     transitionTo(mConnected);
                     break;
 
@@ -560,6 +595,46 @@ class AvrcpControllerStateMachine extends StateMachine {
             }
             return true;
         }
+
+        private void sendFolderBroadcastAndUpdateNode() {
+            BrowseTree.BrowseNode bn = mBrowseTree.findBrowseNodeByID(mID);
+            if (bn.isPlayer()) {
+                // Add the now playing folder.
+                MediaDescription.Builder mdb = new MediaDescription.Builder();
+                mdb.setMediaId(BrowseTree.NOW_PLAYING_PREFIX + ":" +
+                    bn.getPlayerID());
+                mdb.setTitle(BrowseTree.NOW_PLAYING_PREFIX);
+                Bundle mdBundle = new Bundle();
+                mdBundle.putString(
+                    AvrcpControllerService.MEDIA_ITEM_UID_KEY,
+                    BrowseTree.NOW_PLAYING_PREFIX + ":" + bn.getID());
+                mdb.setExtras(mdBundle);
+                mFolderList.add(new MediaItem(mdb.build(), MediaItem.FLAG_BROWSABLE));
+            }
+            mBrowseTree.refreshChildren(bn, mFolderList);
+            broadcastFolderList(mID, mFolderList);
+
+            // For now playing we need to set the current browsed folder here.
+            // For normal folders it is set after ChangeFolderPath.
+            if (mScope == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
+                mBrowseTree.setCurrentBrowsedFolder(mID);
+            }
+        }
+
+        private void callNativeFunctionForScope(int start, int end) {
+            switch (mScope) {
+                case AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING:
+                    AvrcpControllerService.getNowPlayingListNative(
+                        mRemoteDevice.getBluetoothAddress(), (byte) start, (byte) end);
+                    break;
+                case AvrcpControllerService.BROWSE_SCOPE_VFS:
+                    AvrcpControllerService.getFolderListNative(
+                        mRemoteDevice.getBluetoothAddress(), (byte) start, (byte) end);
+                    break;
+                default:
+                    Log.e(STATE_TAG, "Scope " + mScope + " cannot be handled here.");
+            }
+        }
     }
 
     // Handle the get player listing action
@@ -601,41 +676,6 @@ class AvrcpControllerStateMachine extends StateMachine {
         }
     }
 
-    class GetNowPlayingList extends CmdState {
-        private String STATE_TAG = "AVRCPSM.NowPlayingList";
-        private String mID = "";
-
-        public void setFolder(String id) {
-            mID = id;
-        }
-
-        @Override
-        public boolean processMessage(Message msg) {
-            Log.d(STATE_TAG, "processMessage " + msg);
-            switch (msg.what) {
-                case MESSAGE_PROCESS_GET_FOLDER_ITEMS:
-                    ArrayList<MediaItem> folderList = (ArrayList<MediaItem>) msg.obj;
-                    broadcastFolderList(mID, folderList);
-
-                    BrowseTree.BrowseNode bn = mBrowseTree.findBrowseNodeByID(mID);
-                    mBrowseTree.refreshChildren(bn, folderList);
-                    mBrowseTree.setCurrentBrowsedFolder(mID);
-                    transitionTo(mConnected);
-                    break;
-
-                case MESSAGE_INTERNAL_CMD_TIMEOUT:
-                    broadcastFolderList(mID, mEmptyMediaItemList);
-                    transitionTo(mConnected);
-                    break;
-
-                default:
-                    Log.d(STATE_TAG, "deferring message " + msg + " to connected!");
-                    deferMessage(msg);
-            }
-            return true;
-        }
-    }
-
     class MoveToRoot extends CmdState {
         private String STATE_TAG = "AVRCPSM.MoveToRoot";
         private String mID = "";
@@ -795,7 +835,14 @@ class AvrcpControllerStateMachine extends StateMachine {
         }
     }
 
-    // Browsing related functions.
+    // Entry point to the state machine where the services should call to fetch children
+    // for a specific node. It checks if the currently browsed node is the same as the one being
+    // asked for, in that case it returns the currently cached children. This saves bandwidth and
+    // also if we are already fetching elements for a current folder (since we need to batch
+    // fetches) then we should not submit another request but simply return what we have fetched
+    // until now.
+    //
+    // It handles fetches to all VFS, Now Playing and Media Player lists.
     void getChildren(String parentMediaId, int start, int items) {
         BrowseTree.BrowseNode bn = mBrowseTree.findBrowseNodeByID(parentMediaId);
         if (bn == null) {
@@ -804,17 +851,36 @@ class AvrcpControllerStateMachine extends StateMachine {
             return;
         }
 
+        if (bn.equals(mBrowseTree.getCurrentBrowsedFolder()) && bn.isCached()) {
+            if (DBG) {
+                Log.d(TAG, "Same cached folder -- returning existing children.");
+            }
+            BrowseTree.BrowseNode n = mBrowseTree.findBrowseNodeByID(parentMediaId);
+            ArrayList<MediaItem> childrenList = new ArrayList<MediaItem>();
+            for (BrowseTree.BrowseNode cn : n.getChildren()) {
+                childrenList.add(cn.getMediaItem());
+            }
+            broadcastFolderList(parentMediaId, childrenList);
+            return;
+        }
+
         Message msg = null;
+        int btDirection = mBrowseTree.getDirection(parentMediaId);
+        BrowseTree.BrowseNode currFol = mBrowseTree.getCurrentBrowsedFolder();
+        if (DBG) {
+            Log.d(TAG, "Browse direction parent " + mBrowseTree.getCurrentBrowsedFolder() +
+                " req " + parentMediaId + " direction " + btDirection);
+        }
         if (BrowseTree.ROOT.equals(parentMediaId)) {
             // Root contains the list of players.
             msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_GET_PLAYER_LIST, start, items);
-        } else if (bn.isPlayer()) {
+        } else if (bn.isPlayer() && btDirection != BrowseTree.DIRECTION_SAME) {
             // Set browsed (and addressed player) as the new player.
             // This should fetch the list of folders.
             msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_SET_BROWSED_PLAYER,
                 bn.getPlayerID(), 0, bn.getID());
         } else if (bn.isNowPlaying()) {
-            // Get all songs in the list.
+            // Issue a request to fetch the items.
             msg = obtainMessage(
                 AvrcpControllerStateMachine.MESSAGE_GET_NOW_PLAYING_LIST,
                 start, items, parentMediaId);
@@ -822,30 +888,31 @@ class AvrcpControllerStateMachine extends StateMachine {
             // Only change folder if desired. If an app refreshes a folder
             // (because it resumed etc) and current folder does not change
             // then we can simply fetch list.
-            BrowseTree.BrowseNode currFol = mBrowseTree.getCurrentBrowsedFolder();
 
             // We exempt two conditions from change folder:
             // a) If the new folder is the same as current folder (refresh of UI)
-            // b) If the new folder is ROOT and current folder is NOW_PLAYING. In this
-            // condition we 'fake' child-parent hierarchy but it does not exist in
+            // b) If the new folder is ROOT and current folder is NOW_PLAYING (or vice-versa)
+            // In this condition we 'fake' child-parent hierarchy but it does not exist in
             // bluetooth world.
             boolean isNowPlayingToRoot =
                 currFol.isNowPlaying() && bn.getID().equals(BrowseTree.ROOT);
-            if (!bn.equals(currFol) && !isNowPlayingToRoot) {
+            if (!isNowPlayingToRoot) {
                 // Find the direction of traversal.
-                int direction;
-                int btDirection = mBrowseTree.getDirection(parentMediaId);
+                int direction = -1;
                 Log.d(TAG, "Browse direction " + currFol + " " + bn + " = " + btDirection);
-                if (btDirection == BrowseTree.DIRECTION_DOWN) {
-                    direction = AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_DOWN;
-                } else if (btDirection == BrowseTree.DIRECTION_UP) {
-                    direction = AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP;
-                } else {
+                if (btDirection == BrowseTree.DIRECTION_UNKNOWN) {
                     Log.w(TAG, "parent " + bn + " is not a direct " +
                         "successor or predeccessor of current folder " + currFol);
                     broadcastFolderList(parentMediaId, mEmptyMediaItemList);
                     return;
                 }
+
+                if (btDirection == BrowseTree.DIRECTION_DOWN) {
+                    direction = AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_DOWN;
+                } else if (btDirection == BrowseTree.DIRECTION_UP) {
+                    direction = AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP;
+                }
+
                 Bundle b = new Bundle();
                 b.putString(AvrcpControllerService.EXTRA_FOLDER_ID, bn.getID());
                 b.putString(AvrcpControllerService.EXTRA_FOLDER_BT_ID, bn.getFolderUID());
@@ -858,6 +925,7 @@ class AvrcpControllerStateMachine extends StateMachine {
                     start, items, bn.getFolderUID());
             }
         }
+
         if (msg != null) {
             sendMessage(msg);
         }
index a11988d..3288c7b 100644 (file)
@@ -46,6 +46,7 @@ public class BrowseTree {
 
     public static final int DIRECTION_DOWN = 0;
     public static final int DIRECTION_UP = 1;
+    public static final int DIRECTION_SAME = 2;
     public static final int DIRECTION_UNKNOWN = -1;
 
     public static final String ROOT = "__ROOT__";
@@ -57,6 +58,9 @@ public class BrowseTree {
     private BrowseNode mCurrentBrowseNode;
 
     BrowseTree() {
+    }
+
+    public void init() {
         MediaDescription.Builder mdb = new MediaDescription.Builder();
         mdb.setMediaId(ROOT);
         mdb.setTitle(ROOT);
@@ -67,6 +71,11 @@ public class BrowseTree {
         mCurrentBrowseNode = mBrowseMap.get(ROOT);
     }
 
+    public void clear() {
+        // Clearing the map should garbage collect everything.
+        mBrowseMap.clear();
+    }
+
     // Each node of the tree is represented by Folder ID, Folder Name and the children.
     class BrowseNode {
         // MediaItem to store the media related details.
@@ -77,6 +86,10 @@ public class BrowseTree {
         // distinction here.
         boolean mIsPlayer = false;
 
+        // If this folder is currently cached, can be useful to return the contents
+        // without doing another fetch.
+        boolean mCached = false;
+
         // Result object if this node is not loaded yet. This result object will be used
         // once loading is finished.
         Result<List<MediaItem>> mResult = null;
@@ -115,6 +128,14 @@ public class BrowseTree {
             return false;
         }
 
+        synchronized boolean isCached() {
+            return mCached;
+        }
+
+        synchronized void setCached(boolean cached) {
+            mCached = cached;
+        }
+
         // Fetch the Unique UID for this item, this is unique across all elements in the tree.
         synchronized String getID() {
             return mItem.getDescription().getMediaId();
@@ -195,6 +216,8 @@ public class BrowseTree {
         for (BrowseNode bn : parent.getChildren()) {
             childrenList.add(bn.getMediaItem());
         }
+
+        parent.setCached(true);
     }
 
     synchronized BrowseNode findBrowseNodeByID(String parentID) {
@@ -238,6 +261,8 @@ public class BrowseTree {
             return DIRECTION_DOWN;
         } else if (toFolder.isChild(fromFolder)) {
             return DIRECTION_UP;
+        } else if (fromFolder.equals(toFolder)) {
+            return DIRECTION_SAME;
         } else {
             Log.w(TAG, "from folder " + mCurrentBrowseNode + " children " +
                 fromFolder.getChildren() + "to folder " + toUID + " children " +
@@ -252,6 +277,10 @@ public class BrowseTree {
             Log.e(TAG, "Setting an unknown browsed folder, ignoring bn " + uid);
             return false;
         }
+
+        // Set the previous folder as not cached so that we fetch the contents again.
+        mCurrentBrowseNode.setCached(false);
+
         mCurrentBrowseNode = bn;
         return true;
     }