BrowseTree.java revision a7c3906cd449598a2960f9574a79edbadb96f034
1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.bluetooth.avrcpcontroller;
18
19import android.media.browse.MediaBrowser;
20import android.media.browse.MediaBrowser.MediaItem;
21import android.media.MediaDescription;
22import android.os.Bundle;
23import android.os.ResultReceiver;
24import android.service.media.MediaBrowserService.Result;
25import android.util.Log;
26
27import java.util.ArrayList;
28import java.util.HashMap;
29import java.util.List;
30import java.util.Stack;
31
32// Browsing hierarchy.
33// Root:
34//      Player1:
35//        Now_Playing:
36//          MediaItem1
37//          MediaItem2
38//        Folder1
39//        Folder2
40//        ....
41//      Player2
42//      ....
43public class BrowseTree {
44    private static final String TAG = "BrowseTree";
45    private static final boolean DBG = true;
46
47    public static final int DIRECTION_DOWN = 0;
48    public static final int DIRECTION_UP = 1;
49    public static final int DIRECTION_SAME = 2;
50    public static final int DIRECTION_UNKNOWN = -1;
51
52    public static final String ROOT = "__ROOT__";
53    public static final String NOW_PLAYING_PREFIX = "NOW_PLAYING";
54    public static final String PLAYER_PREFIX = "PLAYER";
55
56    // Static instance of Folder ID <-> Folder Instance (for navigation purposes)
57    private final HashMap<String, BrowseNode> mBrowseMap = new HashMap<String, BrowseNode>();
58    private BrowseNode mCurrentBrowseNode;
59    private BrowseNode mCurrentBrowsedPlayer;
60    private BrowseNode mCurrentAddressedPlayer;
61
62    BrowseTree() {
63    }
64
65    public void init() {
66        MediaDescription.Builder mdb = new MediaDescription.Builder();
67        mdb.setMediaId(ROOT);
68        mdb.setTitle(ROOT);
69        Bundle mdBundle = new Bundle();
70        mdBundle.putString(AvrcpControllerService.MEDIA_ITEM_UID_KEY, ROOT);
71        mdb.setExtras(mdBundle);
72        mBrowseMap.put(ROOT, new BrowseNode(new MediaItem(mdb.build(), MediaItem.FLAG_BROWSABLE)));
73        mCurrentBrowseNode = mBrowseMap.get(ROOT);
74    }
75
76    public void clear() {
77        // Clearing the map should garbage collect everything.
78        mBrowseMap.clear();
79    }
80
81    // Each node of the tree is represented by Folder ID, Folder Name and the children.
82    class BrowseNode {
83        // MediaItem to store the media related details.
84        MediaItem mItem;
85
86        // Type of this browse node.
87        // Since Media APIs do not define the player separately we define that
88        // distinction here.
89        boolean mIsPlayer = false;
90
91        // If this folder is currently cached, can be useful to return the contents
92        // without doing another fetch.
93        boolean mCached = false;
94
95        // Result object if this node is not loaded yet. This result object will be used
96        // once loading is finished.
97        Result<List<MediaItem>> mResult = null;
98
99        // List of children.
100        final List<BrowseNode> mChildren = new ArrayList<BrowseNode>();
101
102        BrowseNode(MediaItem item) {
103            mItem = item;
104        }
105
106        BrowseNode(AvrcpPlayer player) {
107            mIsPlayer = true;
108
109            // Transform the player into a item.
110            MediaDescription.Builder mdb = new MediaDescription.Builder();
111            Bundle mdExtra = new Bundle();
112            String playerKey = PLAYER_PREFIX + player.getId();
113            mdExtra.putString(AvrcpControllerService.MEDIA_ITEM_UID_KEY, playerKey);
114            mdb.setExtras(mdExtra);
115            mdb.setMediaId(playerKey);
116            mdb.setTitle(player.getName());
117            mItem = new MediaBrowser.MediaItem(mdb.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE);
118        }
119
120        synchronized List<BrowseNode> getChildren() {
121            return mChildren;
122        }
123
124        synchronized boolean isChild(BrowseNode node) {
125            for (BrowseNode bn : mChildren) {
126                if (bn.equals(node)) {
127                    return true;
128                }
129            }
130            return false;
131        }
132
133        synchronized boolean isCached() {
134            return mCached;
135        }
136
137        synchronized void setCached(boolean cached) {
138            mCached = cached;
139        }
140
141        // Fetch the Unique UID for this item, this is unique across all elements in the tree.
142        synchronized String getID() {
143            return mItem.getDescription().getMediaId();
144        }
145
146        // Get the BT Player ID associated with this node.
147        synchronized int getPlayerID() {
148            return Integer.parseInt(getID().replace(PLAYER_PREFIX, ""));
149        }
150
151        // Fetch the Folder UID that can be used to fetch folder listing via bluetooth.
152        // This may not be unique hence this combined with direction will define the
153        // browsing here.
154        synchronized String getFolderUID() {
155            return mItem.getDescription().getExtras().getString(
156                AvrcpControllerService.MEDIA_ITEM_UID_KEY);
157        }
158
159        synchronized MediaItem getMediaItem() {
160            return mItem;
161        }
162
163        synchronized boolean isPlayer() {
164            return mIsPlayer;
165        }
166
167        synchronized boolean isNowPlaying() {
168            return getID().startsWith(NOW_PLAYING_PREFIX);
169        }
170
171        @Override
172        public boolean equals(Object other) {
173            if (!(other instanceof BrowseNode)) {
174                return false;
175            }
176            BrowseNode otherNode = (BrowseNode) other;
177            return getID().equals(otherNode.getID());
178        }
179
180        @Override
181        public String toString() {
182            return "ID: " + getID() + " desc: " + mItem;
183        }
184    }
185
186    synchronized <E> void refreshChildren(String parentID, List<E> children) {
187        BrowseNode parent = findFolderByIDLocked(parentID);
188        if (parent == null) {
189            Log.w(TAG, "parent not found for parentID " + parentID);
190            return;
191        }
192        refreshChildren(parent, children);
193    }
194
195    synchronized <E> void refreshChildren(BrowseNode parent, List<E> children) {
196        if (children == null) {
197            Log.e(TAG, "children cannot be null ");
198            return;
199        }
200
201        List<BrowseNode> bnList = new ArrayList<BrowseNode>();
202        for (E child : children) {
203            if (child instanceof MediaItem) {
204                bnList.add(new BrowseNode((MediaItem) child));
205            } else if (child instanceof AvrcpPlayer) {
206                bnList.add(new BrowseNode((AvrcpPlayer) child));
207            }
208        }
209
210        String parentID = parent.getID();
211        // Make sure that the child list is clean.
212        if (DBG) {
213            Log.d(TAG, "parent " + parentID + " child list " + parent.getChildren());
214        }
215
216        addChildrenLocked(parent, bnList);
217        List<MediaItem> childrenList = new ArrayList<MediaItem>();
218        for (BrowseNode bn : parent.getChildren()) {
219            childrenList.add(bn.getMediaItem());
220        }
221
222        parent.setCached(true);
223    }
224
225    synchronized BrowseNode findBrowseNodeByID(String parentID) {
226        BrowseNode bn = mBrowseMap.get(parentID);
227        if (bn == null) {
228            Log.e(TAG, "folder " + parentID + " not found!");
229            return null;
230        }
231        if (DBG) {
232            Log.d(TAG, "Browse map: " + mBrowseMap);
233        }
234        return bn;
235    }
236
237    BrowseNode findFolderByIDLocked(String parentID) {
238        return mBrowseMap.get(parentID);
239    }
240
241    void addChildrenLocked(BrowseNode parent, List<BrowseNode> items) {
242        // Remove existing children and then add the new children.
243        for (BrowseNode c : parent.getChildren()) {
244            mBrowseMap.remove(c.getID());
245        }
246        parent.getChildren().clear();
247
248        for (BrowseNode bn : items) {
249            parent.getChildren().add(bn);
250            mBrowseMap.put(bn.getID(), bn);
251        }
252    }
253
254    synchronized int getDirection(String toUID) {
255        BrowseNode fromFolder = mCurrentBrowseNode;
256        BrowseNode toFolder = findFolderByIDLocked(toUID);
257        if (fromFolder == null || toFolder == null) {
258            Log.e(TAG, "from folder " + mCurrentBrowseNode + " or to folder " + toUID + " null!");
259        }
260
261        // Check the relationship.
262        if (fromFolder.isChild(toFolder)) {
263            return DIRECTION_DOWN;
264        } else if (toFolder.isChild(fromFolder)) {
265            return DIRECTION_UP;
266        } else if (fromFolder.equals(toFolder)) {
267            return DIRECTION_SAME;
268        } else {
269            Log.w(TAG, "from folder " + mCurrentBrowseNode + " children " +
270                fromFolder.getChildren() + "to folder " + toUID + " children " +
271                toFolder.getChildren());
272            return DIRECTION_UNKNOWN;
273        }
274    }
275
276    synchronized boolean setCurrentBrowsedFolder(String uid) {
277        BrowseNode bn = findFolderByIDLocked(uid);
278        if (bn == null) {
279            Log.e(TAG, "Setting an unknown browsed folder, ignoring bn " + uid);
280            return false;
281        }
282
283        // Set the previous folder as not cached so that we fetch the contents again.
284        mCurrentBrowseNode.setCached(false);
285
286        mCurrentBrowseNode = bn;
287        return true;
288    }
289
290    synchronized BrowseNode getCurrentBrowsedFolder() {
291        return mCurrentBrowseNode;
292    }
293
294    synchronized boolean setCurrentBrowsedPlayer(String uid) {
295        BrowseNode bn = findFolderByIDLocked(uid);
296        if (bn == null) {
297            Log.e(TAG, "Setting an unknown browsed player, ignoring bn " + uid);
298            return false;
299        }
300        mCurrentBrowsedPlayer = bn;
301        return true;
302    }
303
304    synchronized BrowseNode getCurrentBrowsedPlayer() {
305        return mCurrentBrowsedPlayer;
306    }
307
308    synchronized boolean setCurrentAddressedPlayer(String uid) {
309        BrowseNode bn = findFolderByIDLocked(uid);
310        if (bn == null) {
311            Log.e(TAG, "Setting an unknown addressed player, ignoring bn " + uid);
312            return false;
313        }
314        mCurrentAddressedPlayer = bn;
315        return true;
316    }
317
318    synchronized BrowseNode getCurrentAddressedPlayer() {
319        return mCurrentAddressedPlayer;
320    }
321
322    @Override
323    public String toString() {
324        return mBrowseMap.toString();
325    }
326}
327