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