• 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.avrcpcontroller;
18 
19 import android.bluetooth.BluetoothDevice;
20 import android.media.MediaDescription;
21 import android.media.browse.MediaBrowser;
22 import android.media.browse.MediaBrowser.MediaItem;
23 import android.os.Bundle;
24 import android.util.Log;
25 
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.UUID;
30 
31 // Browsing hierarchy.
32 // Root:
33 //      Player1:
34 //        Now_Playing:
35 //          MediaItem1
36 //          MediaItem2
37 //        Folder1
38 //        Folder2
39 //        ....
40 //      Player2
41 //      ....
42 public class BrowseTree {
43     private static final String TAG = "BrowseTree";
44     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
45     private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
46 
47     public static final String ROOT = "__ROOT__";
48     public static final String UP = "__UP__";
49     public static final String NOW_PLAYING_PREFIX = "NOW_PLAYING";
50     public static final String PLAYER_PREFIX = "PLAYER";
51 
52     // Static instance of Folder ID <-> Folder Instance (for navigation purposes)
53     private final HashMap<String, BrowseNode> mBrowseMap = new HashMap<String, BrowseNode>();
54     private BrowseNode mCurrentBrowseNode;
55     private BrowseNode mCurrentBrowsedPlayer;
56     private BrowseNode mCurrentAddressedPlayer;
57     private int mDepth = 0;
58     final BrowseNode mRootNode;
59     final BrowseNode mNavigateUpNode;
60     final BrowseNode mNowPlayingNode;
61 
BrowseTree(BluetoothDevice device)62     BrowseTree(BluetoothDevice device) {
63         if (device == null) {
64             mRootNode = new BrowseNode(new MediaItem(new MediaDescription.Builder()
65                     .setMediaId(ROOT).setTitle(ROOT).build(), MediaItem.FLAG_BROWSABLE));
66             mRootNode.setCached(true);
67         } else {
68             mRootNode = new BrowseNode(new MediaItem(new MediaDescription.Builder()
69                     .setMediaId(ROOT + device.getAddress().toString()).setTitle(
70                             device.getName()).build(), MediaItem.FLAG_BROWSABLE));
71             mRootNode.mDevice = device;
72 
73         }
74         mRootNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_PLAYER_LIST;
75         mRootNode.setExpectedChildren(255);
76 
77         mNavigateUpNode = new BrowseNode(new MediaItem(new MediaDescription.Builder()
78                 .setMediaId(UP).setTitle(UP).build(),
79                 MediaItem.FLAG_BROWSABLE));
80 
81         mNowPlayingNode = new BrowseNode(new MediaItem(new MediaDescription.Builder()
82                 .setMediaId(NOW_PLAYING_PREFIX)
83                 .setTitle(NOW_PLAYING_PREFIX).build(), MediaItem.FLAG_BROWSABLE));
84         mNowPlayingNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING;
85         mNowPlayingNode.setExpectedChildren(255);
86         mBrowseMap.put(ROOT, mRootNode);
87         mBrowseMap.put(NOW_PLAYING_PREFIX, mNowPlayingNode);
88 
89         mCurrentBrowseNode = mRootNode;
90     }
91 
clear()92     public void clear() {
93         // Clearing the map should garbage collect everything.
94         mBrowseMap.clear();
95     }
96 
onConnected(BluetoothDevice device)97     void onConnected(BluetoothDevice device) {
98         BrowseNode browseNode = new BrowseNode(device);
99         mRootNode.addChild(browseNode);
100     }
101 
getTrackFromNowPlayingList(int trackNumber)102     BrowseNode getTrackFromNowPlayingList(int trackNumber) {
103         return mNowPlayingNode.mChildren.get(trackNumber);
104     }
105 
106     // Each node of the tree is represented by Folder ID, Folder Name and the children.
107     class BrowseNode {
108         // MediaItem to store the media related details.
109         MediaItem mItem;
110 
111         BluetoothDevice mDevice;
112         long mBluetoothId;
113 
114         // Type of this browse node.
115         // Since Media APIs do not define the player separately we define that
116         // distinction here.
117         boolean mIsPlayer = false;
118 
119         // If this folder is currently cached, can be useful to return the contents
120         // without doing another fetch.
121         boolean mCached = false;
122 
123         byte mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_VFS;
124 
125         // List of children.
126         private BrowseNode mParent;
127         private final List<BrowseNode> mChildren = new ArrayList<BrowseNode>();
128         private int mExpectedChildrenCount;
129 
BrowseNode(MediaItem item)130         BrowseNode(MediaItem item) {
131             mItem = item;
132             Bundle extras = mItem.getDescription().getExtras();
133             if (extras != null) {
134                 mBluetoothId = extras.getLong(AvrcpControllerService.MEDIA_ITEM_UID_KEY);
135             }
136         }
137 
BrowseNode(AvrcpPlayer player)138         BrowseNode(AvrcpPlayer player) {
139             mIsPlayer = true;
140 
141             // Transform the player into a item.
142             MediaDescription.Builder mdb = new MediaDescription.Builder();
143             String playerKey = PLAYER_PREFIX + player.getId();
144             mBluetoothId = player.getId();
145 
146             mdb.setMediaId(UUID.randomUUID().toString());
147             mdb.setTitle(player.getName());
148             int mediaItemFlags = player.supportsFeature(AvrcpPlayer.FEATURE_BROWSING)
149                     ? MediaBrowser.MediaItem.FLAG_BROWSABLE : 0;
150             mItem = new MediaBrowser.MediaItem(mdb.build(), mediaItemFlags);
151         }
152 
BrowseNode(BluetoothDevice device)153         BrowseNode(BluetoothDevice device) {
154             boolean mIsPlayer = true;
155             mDevice = device;
156             MediaDescription.Builder mdb = new MediaDescription.Builder();
157             String playerKey = PLAYER_PREFIX + device.getAddress().toString();
158             mdb.setMediaId(playerKey);
159             mdb.setTitle(device.getName());
160             int mediaItemFlags = MediaBrowser.MediaItem.FLAG_BROWSABLE;
161             mItem = new MediaBrowser.MediaItem(mdb.build(), mediaItemFlags);
162         }
163 
BrowseNode(String name)164         private BrowseNode(String name) {
165             MediaDescription.Builder mdb = new MediaDescription.Builder();
166             mdb.setMediaId(name);
167             mdb.setTitle(name);
168             mItem = new MediaBrowser.MediaItem(mdb.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE);
169         }
170 
setExpectedChildren(int count)171         synchronized void setExpectedChildren(int count) {
172             mExpectedChildrenCount = count;
173         }
174 
getExpectedChildren()175         synchronized int getExpectedChildren() {
176             return mExpectedChildrenCount;
177         }
178 
addChildren(List<E> newChildren)179         synchronized <E> int addChildren(List<E> newChildren) {
180             for (E child : newChildren) {
181                 BrowseNode currentNode = null;
182                 if (child instanceof MediaItem) {
183                     currentNode = new BrowseNode((MediaItem) child);
184                 } else if (child instanceof AvrcpPlayer) {
185                     currentNode = new BrowseNode((AvrcpPlayer) child);
186                 }
187                 addChild(currentNode);
188             }
189             return newChildren.size();
190         }
191 
addChild(BrowseNode node)192         synchronized boolean addChild(BrowseNode node) {
193             if (node != null) {
194                 node.mParent = this;
195                 if (this.mBrowseScope == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
196                     node.mBrowseScope = this.mBrowseScope;
197                 }
198                 if (node.mDevice == null) {
199                     node.mDevice = this.mDevice;
200                 }
201                 mChildren.add(node);
202                 mBrowseMap.put(node.getID(), node);
203                 return true;
204             }
205             return false;
206         }
207 
removeChild(BrowseNode node)208         synchronized void removeChild(BrowseNode node) {
209             mChildren.remove(node);
210             mBrowseMap.remove(node.getID());
211         }
212 
getChildrenCount()213         synchronized int getChildrenCount() {
214             return mChildren.size();
215         }
216 
getChildren()217         synchronized List<BrowseNode> getChildren() {
218             return mChildren;
219         }
220 
getParent()221         synchronized BrowseNode getParent() {
222             return mParent;
223         }
224 
getContents()225         synchronized List<MediaItem> getContents() {
226             if (mChildren.size() > 0 || mCached) {
227                 List<MediaItem> contents = new ArrayList<MediaItem>(mChildren.size());
228                 for (BrowseNode child : mChildren) {
229                     contents.add(child.getMediaItem());
230                 }
231                 return contents;
232             }
233             return null;
234         }
235 
isChild(BrowseNode node)236         synchronized boolean isChild(BrowseNode node) {
237             return mChildren.contains(node);
238         }
239 
isCached()240         synchronized boolean isCached() {
241             return mCached;
242         }
243 
isBrowsable()244         synchronized boolean isBrowsable() {
245             return mItem.isBrowsable();
246         }
247 
setCached(boolean cached)248         synchronized void setCached(boolean cached) {
249             if (DBG) Log.d(TAG, "Set Cache" + cached + "Node" + toString());
250             mCached = cached;
251             if (!cached) {
252                 for (BrowseNode child : mChildren) {
253                     mBrowseMap.remove(child.getID());
254                 }
255                 mChildren.clear();
256             }
257         }
258 
259         // Fetch the Unique UID for this item, this is unique across all elements in the tree.
getID()260         synchronized String getID() {
261             return mItem.getDescription().getMediaId();
262         }
263 
264         // Get the BT Player ID associated with this node.
getPlayerID()265         synchronized int getPlayerID() {
266             return Integer.parseInt(getID().replace(PLAYER_PREFIX, ""));
267         }
268 
getScope()269         synchronized byte getScope() {
270             return mBrowseScope;
271         }
272 
273         // Fetch the Folder UID that can be used to fetch folder listing via bluetooth.
274         // This may not be unique hence this combined with direction will define the
275         // browsing here.
getFolderUID()276         synchronized String getFolderUID() {
277             return getID();
278         }
279 
getBluetoothID()280         synchronized long getBluetoothID() {
281             return mBluetoothId;
282         }
283 
getMediaItem()284         synchronized MediaItem getMediaItem() {
285             return mItem;
286         }
287 
isPlayer()288         synchronized boolean isPlayer() {
289             return mIsPlayer;
290         }
291 
isNowPlaying()292         synchronized boolean isNowPlaying() {
293             return getID().startsWith(NOW_PLAYING_PREFIX);
294         }
295 
296         @Override
equals(Object other)297         public boolean equals(Object other) {
298             if (!(other instanceof BrowseNode)) {
299                 return false;
300             }
301             BrowseNode otherNode = (BrowseNode) other;
302             return getID().equals(otherNode.getID());
303         }
304 
305         @Override
toString()306         public synchronized String toString() {
307             if (VDBG) {
308                 String serialized = "[ Name: " + mItem.getDescription().getTitle()
309                         + " Scope:" + mBrowseScope + " expected Children: "
310                         + mExpectedChildrenCount + "] ";
311                 for (BrowseNode node : mChildren) {
312                     serialized += node.toString();
313                 }
314                 return serialized;
315             } else {
316                 return "ID: " + getID();
317             }
318         }
319 
320         // Returns true if target is a descendant of this.
isDescendant(BrowseNode target)321         synchronized boolean isDescendant(BrowseNode target) {
322             return getEldestChild(this, target) == null ? false : true;
323         }
324     }
325 
findBrowseNodeByID(String parentID)326     synchronized BrowseNode findBrowseNodeByID(String parentID) {
327         BrowseNode bn = mBrowseMap.get(parentID);
328         if (bn == null) {
329             Log.e(TAG, "folder " + parentID + " not found!");
330             return null;
331         }
332         if (VDBG) {
333             Log.d(TAG, "Size" + mBrowseMap.size());
334         }
335         return bn;
336     }
337 
setCurrentBrowsedFolder(String uid)338     synchronized boolean setCurrentBrowsedFolder(String uid) {
339         BrowseNode bn = mBrowseMap.get(uid);
340         if (bn == null) {
341             Log.e(TAG, "Setting an unknown browsed folder, ignoring bn " + uid);
342             return false;
343         }
344 
345         // Set the previous folder as not cached so that we fetch the contents again.
346         if (!bn.equals(mCurrentBrowseNode)) {
347             Log.d(TAG, "Set cache  " + bn + " curr " + mCurrentBrowseNode);
348         }
349         mCurrentBrowseNode = bn;
350         return true;
351     }
352 
getCurrentBrowsedFolder()353     synchronized BrowseNode getCurrentBrowsedFolder() {
354         return mCurrentBrowseNode;
355     }
356 
setCurrentBrowsedPlayer(String uid, int items, int depth)357     synchronized boolean setCurrentBrowsedPlayer(String uid, int items, int depth) {
358         BrowseNode bn = mBrowseMap.get(uid);
359         if (bn == null) {
360             Log.e(TAG, "Setting an unknown browsed player, ignoring bn " + uid);
361             return false;
362         }
363         mCurrentBrowsedPlayer = bn;
364         mCurrentBrowseNode = mCurrentBrowsedPlayer;
365         for (Integer level = 0; level < depth; level++) {
366             BrowseNode dummyNode = new BrowseNode(level.toString());
367             dummyNode.mParent = mCurrentBrowseNode;
368             dummyNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_VFS;
369             mCurrentBrowseNode = dummyNode;
370         }
371         mCurrentBrowseNode.setExpectedChildren(items);
372         mDepth = depth;
373         return true;
374     }
375 
getCurrentBrowsedPlayer()376     synchronized BrowseNode getCurrentBrowsedPlayer() {
377         return mCurrentBrowsedPlayer;
378     }
379 
setCurrentAddressedPlayer(String uid)380     synchronized boolean setCurrentAddressedPlayer(String uid) {
381         BrowseNode bn = mBrowseMap.get(uid);
382         if (bn == null) {
383             if (DBG) Log.d(TAG, "Setting an unknown addressed player, ignoring bn " + uid);
384             mRootNode.setCached(false);
385             mRootNode.mChildren.add(mNowPlayingNode);
386             mBrowseMap.put(NOW_PLAYING_PREFIX, mNowPlayingNode);
387             return false;
388         }
389         mCurrentAddressedPlayer = bn;
390         return true;
391     }
392 
getCurrentAddressedPlayer()393     synchronized BrowseNode getCurrentAddressedPlayer() {
394         return mCurrentAddressedPlayer;
395     }
396 
397     @Override
toString()398     public String toString() {
399         String serialized = "Size: " + mBrowseMap.size();
400         if (VDBG) {
401             serialized += mRootNode.toString();
402         }
403         return serialized;
404     }
405 
406     // Calculates the path to target node.
407     // Returns: UP node to go up
408     // Returns: target node if there
409     // Returns: named node to go down
410     // Returns: null node if unknown
getNextStepToFolder(BrowseNode target)411     BrowseNode getNextStepToFolder(BrowseNode target) {
412         if (target == null) {
413             return null;
414         } else if (target.equals(mCurrentBrowseNode)
415                 || target.equals(mNowPlayingNode)
416                 || target.equals(mRootNode)) {
417             return target;
418         } else if (target.isPlayer()) {
419             if (mDepth > 0) {
420                 mDepth--;
421                 return mNavigateUpNode;
422             } else {
423                 return target;
424             }
425         } else if (mBrowseMap.get(target.getID()) == null) {
426             return null;
427         } else {
428             BrowseNode nextChild = getEldestChild(mCurrentBrowseNode, target);
429             if (nextChild == null) {
430                 return mNavigateUpNode;
431             } else {
432                 return nextChild;
433             }
434         }
435     }
436 
getEldestChild(BrowseNode ancestor, BrowseNode target)437     static BrowseNode getEldestChild(BrowseNode ancestor, BrowseNode target) {
438         // ancestor is an ancestor of target
439         BrowseNode descendant = target;
440         if (DBG) {
441             Log.d(TAG, "NAVIGATING ancestor" + ancestor.toString() + "Target"
442                     + target.toString());
443         }
444         while (!ancestor.equals(descendant.mParent)) {
445             descendant = descendant.mParent;
446             if (descendant == null) {
447                 return null;
448             }
449         }
450         if (DBG) Log.d(TAG, "NAVIGATING Descendant" + descendant.toString());
451         return descendant;
452     }
453 }
454