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