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