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