• 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.net.Uri;
21 import android.support.v4.media.MediaBrowserCompat.MediaItem;
22 import android.util.Log;
23 
24 import com.android.bluetooth.Utils;
25 
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.HashSet;
29 import java.util.List;
30 import java.util.Set;
31 import java.util.UUID;
32 
33 /**
34  * An object that holds the browse tree of available media from a remote device.
35  *
36  * Browsing hierarchy follows the AVRCP specification's description of various scopes and
37  * looks like follows:
38  *    Root:
39  *      Player1:
40  *        Now_Playing:
41  *           MediaItem1
42  *           MediaItem2
43  *        Folder1
44  *        Folder2
45  *          ....
46  *        Player2
47  *          ....
48  */
49 public class BrowseTree {
50     private static final String TAG = "BrowseTree";
51     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
52     private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
53 
54     public static final String ROOT = "__ROOT__";
55     public static final String UP = "__UP__";
56     public static final String NOW_PLAYING_PREFIX = "NOW_PLAYING";
57     public static final String PLAYER_PREFIX = "PLAYER";
58 
59     // Static instance of Folder ID <-> Folder Instance (for navigation purposes)
60     private final HashMap<String, BrowseNode> mBrowseMap = new HashMap<String, BrowseNode>();
61     private BrowseNode mCurrentBrowseNode;
62     private BrowseNode mCurrentBrowsedPlayer;
63     private BrowseNode mCurrentAddressedPlayer;
64     private int mDepth = 0;
65     final BrowseNode mRootNode;
66     final BrowseNode mNavigateUpNode;
67     final BrowseNode mNowPlayingNode;
68 
69     // In support of Cover Artwork, Cover Art URI <-> List of UUIDs using that artwork
70     private final HashMap<String, ArrayList<String>> mCoverArtMap =
71             new HashMap<String, ArrayList<String>>();
72 
BrowseTree(BluetoothDevice device)73     BrowseTree(BluetoothDevice device) {
74         if (device == null) {
75             mRootNode = new BrowseNode(new AvrcpItem.Builder()
76                     .setUuid(ROOT).setTitle(ROOT).setBrowsable(true).build());
77             mRootNode.setCached(true);
78         } else {
79             mRootNode = new BrowseNode(new AvrcpItem.Builder().setDevice(device)
80                     .setUuid(ROOT + device.getAddress().toString())
81                     .setTitle(Utils.getName(device)).setBrowsable(true).build());
82         }
83 
84         mRootNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_PLAYER_LIST;
85         mRootNode.setExpectedChildren(255);
86 
87         mNavigateUpNode = new BrowseNode(new AvrcpItem.Builder()
88                 .setUuid(UP).setTitle(UP).setBrowsable(true).build());
89 
90         mNowPlayingNode = new BrowseNode(new AvrcpItem.Builder()
91                 .setUuid(NOW_PLAYING_PREFIX).setTitle(NOW_PLAYING_PREFIX)
92                 .setBrowsable(true).build());
93         mNowPlayingNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING;
94         mNowPlayingNode.setExpectedChildren(255);
95         mBrowseMap.put(ROOT, mRootNode);
96         mBrowseMap.put(NOW_PLAYING_PREFIX, mNowPlayingNode);
97 
98         mCurrentBrowseNode = mRootNode;
99     }
100 
clear()101     public void clear() {
102         // Clearing the map should garbage collect everything.
103         mBrowseMap.clear();
104         mCoverArtMap.clear();
105     }
106 
onConnected(BluetoothDevice device)107     void onConnected(BluetoothDevice device) {
108         BrowseNode browseNode = new BrowseNode(device);
109         mRootNode.addChild(browseNode);
110     }
111 
getTrackFromNowPlayingList(int trackNumber)112     BrowseNode getTrackFromNowPlayingList(int trackNumber) {
113         return mNowPlayingNode.getChild(trackNumber);
114     }
115 
116     // Each node of the tree is represented by Folder ID, Folder Name and the children.
117     class BrowseNode {
118         // AvrcpItem to store the media related details.
119         AvrcpItem mItem;
120 
121         // Type of this browse node.
122         // Since Media APIs do not define the player separately we define that
123         // distinction here.
124         boolean mIsPlayer = false;
125 
126         // If this folder is currently cached, can be useful to return the contents
127         // without doing another fetch.
128         boolean mCached = false;
129 
130         byte mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_VFS;
131 
132         // List of children.
133         private BrowseNode mParent;
134         private final List<BrowseNode> mChildren = new ArrayList<BrowseNode>();
135         private int mExpectedChildrenCount;
136 
BrowseNode(AvrcpItem item)137         BrowseNode(AvrcpItem item) {
138             mItem = item;
139         }
140 
BrowseNode(AvrcpPlayer player)141         BrowseNode(AvrcpPlayer player) {
142             mIsPlayer = true;
143 
144             // Transform the player into a item.
145             AvrcpItem.Builder aid = new AvrcpItem.Builder();
146             aid.setDevice(player.getDevice());
147             aid.setUid(player.getId());
148             aid.setUuid(UUID.randomUUID().toString());
149             aid.setDisplayableName(player.getName());
150             aid.setTitle(player.getName());
151             aid.setBrowsable(player.supportsFeature(AvrcpPlayer.FEATURE_BROWSING));
152             mItem = aid.build();
153         }
154 
BrowseNode(BluetoothDevice device)155         BrowseNode(BluetoothDevice device) {
156             mIsPlayer = true;
157             String playerKey = PLAYER_PREFIX + device.getAddress().toString();
158 
159             AvrcpItem.Builder aid = new AvrcpItem.Builder();
160             aid.setDevice(device);
161             aid.setUuid(playerKey);
162             aid.setDisplayableName(Utils.getName(device));
163             aid.setTitle(Utils.getName(device));
164             aid.setBrowsable(true);
165             mItem = aid.build();
166         }
167 
BrowseNode(String name)168         private BrowseNode(String name) {
169             AvrcpItem.Builder aid = new AvrcpItem.Builder();
170             aid.setUuid(name);
171             aid.setDisplayableName(name);
172             aid.setTitle(name);
173             mItem = aid.build();
174         }
175 
setExpectedChildren(int count)176         synchronized void setExpectedChildren(int count) {
177             mExpectedChildrenCount = count;
178         }
179 
getExpectedChildren()180         synchronized int getExpectedChildren() {
181             return mExpectedChildrenCount;
182         }
183 
addChildren(List<E> newChildren)184         synchronized <E> int addChildren(List<E> newChildren) {
185             for (E child : newChildren) {
186                 BrowseNode currentNode = null;
187                 if (child instanceof AvrcpItem) {
188                     currentNode = new BrowseNode((AvrcpItem) child);
189                 } else if (child instanceof AvrcpPlayer) {
190                     currentNode = new BrowseNode((AvrcpPlayer) child);
191                 }
192                 addChild(currentNode);
193             }
194             return newChildren.size();
195         }
196 
addChild(BrowseNode node)197         synchronized boolean addChild(BrowseNode node) {
198             if (node != null) {
199                 node.mParent = this;
200                 if (this.mBrowseScope == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
201                     node.mBrowseScope = this.mBrowseScope;
202                 }
203                 mChildren.add(node);
204                 mBrowseMap.put(node.getID(), node);
205 
206                 // Each time we add a node to the tree, check for an image handle so we can add
207                 // the artwork URI once it has been downloaded
208                 String imageUuid = node.getCoverArtUuid();
209                 if (imageUuid != null) {
210                     indicateCoverArtUsed(node.getID(), imageUuid);
211                 }
212                 return true;
213             }
214             return false;
215         }
216 
removeChild(BrowseNode node)217         synchronized void removeChild(BrowseNode node) {
218             mChildren.remove(node);
219             mBrowseMap.remove(node.getID());
220             indicateCoverArtUnused(node.getID(), node.getCoverArtUuid());
221         }
222 
getChildrenCount()223         synchronized int getChildrenCount() {
224             return mChildren.size();
225         }
226 
getChildren()227         synchronized List<BrowseNode> getChildren() {
228             return mChildren;
229         }
230 
getChild(int index)231         synchronized BrowseNode getChild(int index) {
232             if (index < 0 || index >= mChildren.size()) {
233                 return null;
234             }
235             return mChildren.get(index);
236         }
237 
getParent()238         synchronized BrowseNode getParent() {
239             return mParent;
240         }
241 
getDevice()242         synchronized BluetoothDevice getDevice() {
243             return mItem.getDevice();
244         }
245 
getCoverArtUuid()246         synchronized String getCoverArtUuid() {
247             return mItem.getCoverArtUuid();
248         }
249 
setCoverArtUri(Uri uri)250         synchronized void setCoverArtUri(Uri uri) {
251             mItem.setCoverArtLocation(uri);
252         }
253 
getContents()254         synchronized List<MediaItem> getContents() {
255             if (mChildren.size() > 0 || mCached) {
256                 List<MediaItem> contents = new ArrayList<MediaItem>(mChildren.size());
257                 for (BrowseNode child : mChildren) {
258                     contents.add(child.getMediaItem());
259                 }
260                 return contents;
261             }
262             return null;
263         }
264 
isChild(BrowseNode node)265         synchronized boolean isChild(BrowseNode node) {
266             return mChildren.contains(node);
267         }
268 
isCached()269         synchronized boolean isCached() {
270             return mCached;
271         }
272 
isBrowsable()273         synchronized boolean isBrowsable() {
274             return mItem.isBrowsable();
275         }
276 
setCached(boolean cached)277         synchronized void setCached(boolean cached) {
278             if (DBG) Log.d(TAG, "Set Cache" + cached + "Node" + toString());
279             mCached = cached;
280             if (!cached) {
281                 for (BrowseNode child : mChildren) {
282                     mBrowseMap.remove(child.getID());
283                     indicateCoverArtUnused(child.getID(), child.getCoverArtUuid());
284                 }
285                 mChildren.clear();
286             }
287         }
288 
289         // Fetch the Unique UID for this item, this is unique across all elements in the tree.
getID()290         synchronized String getID() {
291             return mItem.getUuid();
292         }
293 
294         // Get the BT Player ID associated with this node.
getPlayerID()295         synchronized int getPlayerID() {
296             return Integer.parseInt(getID().replace(PLAYER_PREFIX, ""));
297         }
298 
getScope()299         synchronized byte getScope() {
300             return mBrowseScope;
301         }
302 
303         // Fetch the Folder UID that can be used to fetch folder listing via bluetooth.
304         // This may not be unique hence this combined with direction will define the
305         // browsing here.
getFolderUID()306         synchronized String getFolderUID() {
307             return getID();
308         }
309 
getBluetoothID()310         synchronized long getBluetoothID() {
311             return mItem.getUid();
312         }
313 
getMediaItem()314         synchronized MediaItem getMediaItem() {
315             return mItem.toMediaItem();
316         }
317 
isPlayer()318         synchronized boolean isPlayer() {
319             return mIsPlayer;
320         }
321 
isNowPlaying()322         synchronized boolean isNowPlaying() {
323             return getID().startsWith(NOW_PLAYING_PREFIX);
324         }
325 
326         @Override
equals(Object other)327         public boolean equals(Object other) {
328             if (!(other instanceof BrowseNode)) {
329                 return false;
330             }
331             BrowseNode otherNode = (BrowseNode) other;
332             return getID().equals(otherNode.getID());
333         }
334 
335         @Override
toString()336         public synchronized String toString() {
337             if (VDBG) {
338                 String serialized = "[ Name: " + mItem.getTitle()
339                         + " Scope:" + mBrowseScope + " expected Children: "
340                         + mExpectedChildrenCount + "] ";
341                 for (BrowseNode node : mChildren) {
342                     serialized += node.toString();
343                 }
344                 return serialized;
345             } else {
346                 return "ID: " + getID();
347             }
348         }
349 
350         // Returns true if target is a descendant of this.
isDescendant(BrowseNode target)351         synchronized boolean isDescendant(BrowseNode target) {
352             return getEldestChild(this, target) == null ? false : true;
353         }
354     }
355 
findBrowseNodeByID(String parentID)356     synchronized BrowseNode findBrowseNodeByID(String parentID) {
357         BrowseNode bn = mBrowseMap.get(parentID);
358         if (bn == null) {
359             Log.e(TAG, "folder " + parentID + " not found!");
360             return null;
361         }
362         if (VDBG) {
363             Log.d(TAG, "Size" + mBrowseMap.size());
364         }
365         return bn;
366     }
367 
setCurrentBrowsedFolder(String uid)368     synchronized boolean setCurrentBrowsedFolder(String uid) {
369         BrowseNode bn = mBrowseMap.get(uid);
370         if (bn == null) {
371             Log.e(TAG, "Setting an unknown browsed folder, ignoring bn " + uid);
372             return false;
373         }
374 
375         // Set the previous folder as not cached so that we fetch the contents again.
376         if (!bn.equals(mCurrentBrowseNode)) {
377             Log.d(TAG, "Set cache  " + bn + " curr " + mCurrentBrowseNode);
378         }
379         mCurrentBrowseNode = bn;
380         return true;
381     }
382 
getCurrentBrowsedFolder()383     synchronized BrowseNode getCurrentBrowsedFolder() {
384         return mCurrentBrowseNode;
385     }
386 
setCurrentBrowsedPlayer(String uid, int items, int depth)387     synchronized boolean setCurrentBrowsedPlayer(String uid, int items, int depth) {
388         BrowseNode bn = mBrowseMap.get(uid);
389         if (bn == null) {
390             Log.e(TAG, "Setting an unknown browsed player, ignoring bn " + uid);
391             return false;
392         }
393         mCurrentBrowsedPlayer = bn;
394         mCurrentBrowseNode = mCurrentBrowsedPlayer;
395         for (Integer level = 0; level < depth; level++) {
396             BrowseNode dummyNode = new BrowseNode(level.toString());
397             dummyNode.mParent = mCurrentBrowseNode;
398             dummyNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_VFS;
399             mCurrentBrowseNode = dummyNode;
400         }
401         mCurrentBrowseNode.setExpectedChildren(items);
402         mDepth = depth;
403         return true;
404     }
405 
getCurrentBrowsedPlayer()406     synchronized BrowseNode getCurrentBrowsedPlayer() {
407         return mCurrentBrowsedPlayer;
408     }
409 
setCurrentAddressedPlayer(String uid)410     synchronized boolean setCurrentAddressedPlayer(String uid) {
411         BrowseNode bn = mBrowseMap.get(uid);
412         if (bn == null) {
413             if (DBG) Log.d(TAG, "Setting an unknown addressed player, ignoring bn " + uid);
414             mRootNode.setCached(false);
415             mRootNode.mChildren.add(mNowPlayingNode);
416             mBrowseMap.put(NOW_PLAYING_PREFIX, mNowPlayingNode);
417             return false;
418         }
419         mCurrentAddressedPlayer = bn;
420         return true;
421     }
422 
getCurrentAddressedPlayer()423     synchronized BrowseNode getCurrentAddressedPlayer() {
424         return mCurrentAddressedPlayer;
425     }
426 
427     /**
428      * Indicate that a node in the tree is using a specific piece of cover art, identified by the
429      * given image handle.
430      */
indicateCoverArtUsed(String nodeId, String handle)431     synchronized void indicateCoverArtUsed(String nodeId, String handle) {
432         mCoverArtMap.putIfAbsent(handle, new ArrayList<String>());
433         mCoverArtMap.get(handle).add(nodeId);
434     }
435 
436     /**
437      * Indicate that a node in the tree no longer needs a specific piece of cover art.
438      */
indicateCoverArtUnused(String nodeId, String handle)439     synchronized void indicateCoverArtUnused(String nodeId, String handle) {
440         if (mCoverArtMap.containsKey(handle) && mCoverArtMap.get(handle).contains(nodeId)) {
441             mCoverArtMap.get(handle).remove(nodeId);
442         }
443     }
444 
445     /**
446      * Get a list of items using the piece of cover art identified by the given handle.
447      */
getNodesUsingCoverArt(String handle)448     synchronized ArrayList<String> getNodesUsingCoverArt(String handle) {
449         if (!mCoverArtMap.containsKey(handle)) return new ArrayList<String>();
450         return (ArrayList<String>) mCoverArtMap.get(handle).clone();
451     }
452 
453     /**
454      * Get a list of Cover Art UUIDs that are no longer being used by the tree. Clear that list.
455      */
getAndClearUnusedCoverArt()456     synchronized ArrayList<String> getAndClearUnusedCoverArt() {
457         ArrayList<String> unused = new ArrayList<String>();
458         for (String uuid : mCoverArtMap.keySet()) {
459             if (mCoverArtMap.get(uuid).isEmpty()) {
460                 unused.add(uuid);
461             }
462         }
463         for (String uuid : unused) {
464             mCoverArtMap.remove(uuid);
465         }
466         return unused;
467     }
468 
469     /**
470      * Adds the Uri of a newly downloaded image to all tree nodes using that specific handle.
471      * Returns the set of parent nodes that have children impacted by the new art so clients can
472      * be notified of the change.
473      */
notifyImageDownload(String uuid, Uri uri)474     synchronized Set<BrowseNode> notifyImageDownload(String uuid, Uri uri) {
475         if (DBG) Log.d(TAG, "Received downloaded image handle to cascade to BrowseNodes using it");
476         ArrayList<String> nodes = getNodesUsingCoverArt(uuid);
477         HashSet<BrowseNode> parents = new HashSet<BrowseNode>();
478         for (String nodeId : nodes) {
479             BrowseNode node = findBrowseNodeByID(nodeId);
480             if (node == null) {
481                 Log.e(TAG, "Node was removed without clearing its cover art status");
482                 indicateCoverArtUnused(nodeId, uuid);
483                 continue;
484             }
485             node.setCoverArtUri(uri);
486             if (node.mParent != null) {
487                 parents.add(node.mParent);
488             }
489         }
490         return parents;
491     }
492 
493 
494     @Override
toString()495     public String toString() {
496         String serialized = "Size: " + mBrowseMap.size();
497         if (VDBG) {
498             serialized += mRootNode.toString();
499             serialized += "\n  Image handles in use (" + mCoverArtMap.size() + "):";
500             for (String handle : mCoverArtMap.keySet()) {
501                 serialized += "\n    " + handle + "\n";
502             }
503         }
504         return serialized;
505     }
506 
507     // Calculates the path to target node.
508     // Returns: UP node to go up
509     // Returns: target node if there
510     // Returns: named node to go down
511     // Returns: null node if unknown
getNextStepToFolder(BrowseNode target)512     BrowseNode getNextStepToFolder(BrowseNode target) {
513         if (target == null) {
514             return null;
515         } else if (target.equals(mCurrentBrowseNode)
516                 || target.equals(mNowPlayingNode)
517                 || target.equals(mRootNode)) {
518             return target;
519         } else if (target.isPlayer()) {
520             if (mDepth > 0) {
521                 mDepth--;
522                 return mNavigateUpNode;
523             } else {
524                 return target;
525             }
526         } else if (mBrowseMap.get(target.getID()) == null) {
527             return null;
528         } else {
529             BrowseNode nextChild = getEldestChild(mCurrentBrowseNode, target);
530             if (nextChild == null) {
531                 return mNavigateUpNode;
532             } else {
533                 return nextChild;
534             }
535         }
536     }
537 
getEldestChild(BrowseNode ancestor, BrowseNode target)538     static BrowseNode getEldestChild(BrowseNode ancestor, BrowseNode target) {
539         // ancestor is an ancestor of target
540         BrowseNode descendant = target;
541         if (DBG) {
542             Log.d(TAG, "NAVIGATING ancestor" + ancestor.toString() + "Target"
543                     + target.toString());
544         }
545         while (!ancestor.equals(descendant.mParent)) {
546             descendant = descendant.mParent;
547             if (descendant == null) {
548                 return null;
549             }
550         }
551         if (DBG) Log.d(TAG, "NAVIGATING Descendant" + descendant.toString());
552         return descendant;
553     }
554 }
555