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