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