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