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