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.avrcpcontroller;
18 
19 import android.media.browse.MediaBrowser;
20 import android.media.browse.MediaBrowser.MediaItem;
21 import android.media.MediaDescription;
22 import android.os.Bundle;
23 import android.os.ResultReceiver;
24 import android.service.media.MediaBrowserService.Result;
25 import android.util.Log;
26 
27 import java.util.ArrayList;
28 import java.util.HashMap;
29 import java.util.List;
30 import 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 //      ....
43 public 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 
BrowseTree()62     BrowseTree() {
63     }
64 
init()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 
clear()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 
BrowseNode(MediaItem item)102         BrowseNode(MediaItem item) {
103             mItem = item;
104         }
105 
BrowseNode(AvrcpPlayer player)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 
getChildren()120         synchronized List<BrowseNode> getChildren() {
121             return mChildren;
122         }
123 
isChild(BrowseNode node)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 
isCached()133         synchronized boolean isCached() {
134             return mCached;
135         }
136 
setCached(boolean cached)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.
getID()142         synchronized String getID() {
143             return mItem.getDescription().getMediaId();
144         }
145 
146         // Get the BT Player ID associated with this node.
getPlayerID()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.
getFolderUID()154         synchronized String getFolderUID() {
155             return mItem.getDescription().getExtras().getString(
156                 AvrcpControllerService.MEDIA_ITEM_UID_KEY);
157         }
158 
getMediaItem()159         synchronized MediaItem getMediaItem() {
160             return mItem;
161         }
162 
isPlayer()163         synchronized boolean isPlayer() {
164             return mIsPlayer;
165         }
166 
isNowPlaying()167         synchronized boolean isNowPlaying() {
168             return getID().startsWith(NOW_PLAYING_PREFIX);
169         }
170 
171         @Override
equals(Object other)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
toString()181         public String toString() {
182             return "ID: " + getID() + " desc: " + mItem;
183         }
184     }
185 
refreshChildren(String parentID, List<E> children)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 
refreshChildren(BrowseNode parent, List<E> children)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 
findBrowseNodeByID(String parentID)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 
findFolderByIDLocked(String parentID)237     BrowseNode findFolderByIDLocked(String parentID) {
238         return mBrowseMap.get(parentID);
239     }
240 
addChildrenLocked(BrowseNode parent, List<BrowseNode> items)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 
getDirection(String toUID)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 
setCurrentBrowsedFolder(String uid)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         if (!bn.equals(mCurrentBrowseNode)) {
285             Log.d(TAG, "Set cache false " + bn + " curr " + mCurrentBrowseNode);
286             mCurrentBrowseNode.setCached(false);
287         }
288 
289         mCurrentBrowseNode = bn;
290         return true;
291     }
292 
getCurrentBrowsedFolder()293     synchronized BrowseNode getCurrentBrowsedFolder() {
294         return mCurrentBrowseNode;
295     }
296 
setCurrentBrowsedPlayer(String uid)297     synchronized boolean setCurrentBrowsedPlayer(String uid) {
298         BrowseNode bn = findFolderByIDLocked(uid);
299         if (bn == null) {
300             Log.e(TAG, "Setting an unknown browsed player, ignoring bn " + uid);
301             return false;
302         }
303         mCurrentBrowsedPlayer = bn;
304         return true;
305     }
306 
getCurrentBrowsedPlayer()307     synchronized BrowseNode getCurrentBrowsedPlayer() {
308         return mCurrentBrowsedPlayer;
309     }
310 
setCurrentAddressedPlayer(String uid)311     synchronized boolean setCurrentAddressedPlayer(String uid) {
312         BrowseNode bn = findFolderByIDLocked(uid);
313         if (bn == null) {
314             Log.e(TAG, "Setting an unknown addressed player, ignoring bn " + uid);
315             return false;
316         }
317         mCurrentAddressedPlayer = bn;
318         return true;
319     }
320 
getCurrentAddressedPlayer()321     synchronized BrowseNode getCurrentAddressedPlayer() {
322         return mCurrentAddressedPlayer;
323     }
324 
325     @Override
toString()326     public String toString() {
327         return mBrowseMap.toString();
328     }
329 }
330