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