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