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