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.media.MediaDescription; 21 import android.media.browse.MediaBrowser; 22 import android.media.browse.MediaBrowser.MediaItem; 23 import android.os.Bundle; 24 import android.util.Log; 25 26 import java.util.ArrayList; 27 import java.util.HashMap; 28 import java.util.List; 29 import java.util.UUID; 30 31 // Browsing hierarchy. 32 // Root: 33 // Player1: 34 // Now_Playing: 35 // MediaItem1 36 // MediaItem2 37 // Folder1 38 // Folder2 39 // .... 40 // Player2 41 // .... 42 public class BrowseTree { 43 private static final String TAG = "BrowseTree"; 44 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 45 private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE); 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 // Static instance of Folder ID <-> Folder Instance (for navigation purposes) 53 private final HashMap<String, BrowseNode> mBrowseMap = new HashMap<String, BrowseNode>(); 54 private BrowseNode mCurrentBrowseNode; 55 private BrowseNode mCurrentBrowsedPlayer; 56 private BrowseNode mCurrentAddressedPlayer; 57 private int mDepth = 0; 58 final BrowseNode mRootNode; 59 final BrowseNode mNavigateUpNode; 60 final BrowseNode mNowPlayingNode; 61 BrowseTree(BluetoothDevice device)62 BrowseTree(BluetoothDevice device) { 63 if (device == null) { 64 mRootNode = new BrowseNode(new MediaItem(new MediaDescription.Builder() 65 .setMediaId(ROOT).setTitle(ROOT).build(), MediaItem.FLAG_BROWSABLE)); 66 mRootNode.setCached(true); 67 } else { 68 mRootNode = new BrowseNode(new MediaItem(new MediaDescription.Builder() 69 .setMediaId(ROOT + device.getAddress().toString()).setTitle( 70 device.getName()).build(), MediaItem.FLAG_BROWSABLE)); 71 mRootNode.mDevice = device; 72 73 } 74 mRootNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_PLAYER_LIST; 75 mRootNode.setExpectedChildren(255); 76 77 mNavigateUpNode = new BrowseNode(new MediaItem(new MediaDescription.Builder() 78 .setMediaId(UP).setTitle(UP).build(), 79 MediaItem.FLAG_BROWSABLE)); 80 81 mNowPlayingNode = new BrowseNode(new MediaItem(new MediaDescription.Builder() 82 .setMediaId(NOW_PLAYING_PREFIX) 83 .setTitle(NOW_PLAYING_PREFIX).build(), MediaItem.FLAG_BROWSABLE)); 84 mNowPlayingNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING; 85 mNowPlayingNode.setExpectedChildren(255); 86 mBrowseMap.put(ROOT, mRootNode); 87 mBrowseMap.put(NOW_PLAYING_PREFIX, mNowPlayingNode); 88 89 mCurrentBrowseNode = mRootNode; 90 } 91 clear()92 public void clear() { 93 // Clearing the map should garbage collect everything. 94 mBrowseMap.clear(); 95 } 96 onConnected(BluetoothDevice device)97 void onConnected(BluetoothDevice device) { 98 BrowseNode browseNode = new BrowseNode(device); 99 mRootNode.addChild(browseNode); 100 } 101 getTrackFromNowPlayingList(int trackNumber)102 BrowseNode getTrackFromNowPlayingList(int trackNumber) { 103 return mNowPlayingNode.mChildren.get(trackNumber); 104 } 105 106 // Each node of the tree is represented by Folder ID, Folder Name and the children. 107 class BrowseNode { 108 // MediaItem to store the media related details. 109 MediaItem mItem; 110 111 BluetoothDevice mDevice; 112 long mBluetoothId; 113 114 // Type of this browse node. 115 // Since Media APIs do not define the player separately we define that 116 // distinction here. 117 boolean mIsPlayer = false; 118 119 // If this folder is currently cached, can be useful to return the contents 120 // without doing another fetch. 121 boolean mCached = false; 122 123 byte mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_VFS; 124 125 // List of children. 126 private BrowseNode mParent; 127 private final List<BrowseNode> mChildren = new ArrayList<BrowseNode>(); 128 private int mExpectedChildrenCount; 129 BrowseNode(MediaItem item)130 BrowseNode(MediaItem item) { 131 mItem = item; 132 Bundle extras = mItem.getDescription().getExtras(); 133 if (extras != null) { 134 mBluetoothId = extras.getLong(AvrcpControllerService.MEDIA_ITEM_UID_KEY); 135 } 136 } 137 BrowseNode(AvrcpPlayer player)138 BrowseNode(AvrcpPlayer player) { 139 mIsPlayer = true; 140 141 // Transform the player into a item. 142 MediaDescription.Builder mdb = new MediaDescription.Builder(); 143 String playerKey = PLAYER_PREFIX + player.getId(); 144 mBluetoothId = player.getId(); 145 146 mdb.setMediaId(UUID.randomUUID().toString()); 147 mdb.setTitle(player.getName()); 148 int mediaItemFlags = player.supportsFeature(AvrcpPlayer.FEATURE_BROWSING) 149 ? MediaBrowser.MediaItem.FLAG_BROWSABLE : 0; 150 mItem = new MediaBrowser.MediaItem(mdb.build(), mediaItemFlags); 151 } 152 BrowseNode(BluetoothDevice device)153 BrowseNode(BluetoothDevice device) { 154 boolean mIsPlayer = true; 155 mDevice = device; 156 MediaDescription.Builder mdb = new MediaDescription.Builder(); 157 String playerKey = PLAYER_PREFIX + device.getAddress().toString(); 158 mdb.setMediaId(playerKey); 159 mdb.setTitle(device.getName()); 160 int mediaItemFlags = MediaBrowser.MediaItem.FLAG_BROWSABLE; 161 mItem = new MediaBrowser.MediaItem(mdb.build(), mediaItemFlags); 162 } 163 BrowseNode(String name)164 private BrowseNode(String name) { 165 MediaDescription.Builder mdb = new MediaDescription.Builder(); 166 mdb.setMediaId(name); 167 mdb.setTitle(name); 168 mItem = new MediaBrowser.MediaItem(mdb.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE); 169 } 170 setExpectedChildren(int count)171 synchronized void setExpectedChildren(int count) { 172 mExpectedChildrenCount = count; 173 } 174 getExpectedChildren()175 synchronized int getExpectedChildren() { 176 return mExpectedChildrenCount; 177 } 178 addChildren(List<E> newChildren)179 synchronized <E> int addChildren(List<E> newChildren) { 180 for (E child : newChildren) { 181 BrowseNode currentNode = null; 182 if (child instanceof MediaItem) { 183 currentNode = new BrowseNode((MediaItem) child); 184 } else if (child instanceof AvrcpPlayer) { 185 currentNode = new BrowseNode((AvrcpPlayer) child); 186 } 187 addChild(currentNode); 188 } 189 return newChildren.size(); 190 } 191 addChild(BrowseNode node)192 synchronized boolean addChild(BrowseNode node) { 193 if (node != null) { 194 node.mParent = this; 195 if (this.mBrowseScope == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) { 196 node.mBrowseScope = this.mBrowseScope; 197 } 198 if (node.mDevice == null) { 199 node.mDevice = this.mDevice; 200 } 201 mChildren.add(node); 202 mBrowseMap.put(node.getID(), node); 203 return true; 204 } 205 return false; 206 } 207 removeChild(BrowseNode node)208 synchronized void removeChild(BrowseNode node) { 209 mChildren.remove(node); 210 mBrowseMap.remove(node.getID()); 211 } 212 getChildrenCount()213 synchronized int getChildrenCount() { 214 return mChildren.size(); 215 } 216 getChildren()217 synchronized List<BrowseNode> getChildren() { 218 return mChildren; 219 } 220 getParent()221 synchronized BrowseNode getParent() { 222 return mParent; 223 } 224 getContents()225 synchronized List<MediaItem> getContents() { 226 if (mChildren.size() > 0 || mCached) { 227 List<MediaItem> contents = new ArrayList<MediaItem>(mChildren.size()); 228 for (BrowseNode child : mChildren) { 229 contents.add(child.getMediaItem()); 230 } 231 return contents; 232 } 233 return null; 234 } 235 isChild(BrowseNode node)236 synchronized boolean isChild(BrowseNode node) { 237 return mChildren.contains(node); 238 } 239 isCached()240 synchronized boolean isCached() { 241 return mCached; 242 } 243 isBrowsable()244 synchronized boolean isBrowsable() { 245 return mItem.isBrowsable(); 246 } 247 setCached(boolean cached)248 synchronized void setCached(boolean cached) { 249 if (DBG) Log.d(TAG, "Set Cache" + cached + "Node" + toString()); 250 mCached = cached; 251 if (!cached) { 252 for (BrowseNode child : mChildren) { 253 mBrowseMap.remove(child.getID()); 254 } 255 mChildren.clear(); 256 } 257 } 258 259 // Fetch the Unique UID for this item, this is unique across all elements in the tree. getID()260 synchronized String getID() { 261 return mItem.getDescription().getMediaId(); 262 } 263 264 // Get the BT Player ID associated with this node. getPlayerID()265 synchronized int getPlayerID() { 266 return Integer.parseInt(getID().replace(PLAYER_PREFIX, "")); 267 } 268 getScope()269 synchronized byte getScope() { 270 return mBrowseScope; 271 } 272 273 // Fetch the Folder UID that can be used to fetch folder listing via bluetooth. 274 // This may not be unique hence this combined with direction will define the 275 // browsing here. getFolderUID()276 synchronized String getFolderUID() { 277 return getID(); 278 } 279 getBluetoothID()280 synchronized long getBluetoothID() { 281 return mBluetoothId; 282 } 283 getMediaItem()284 synchronized MediaItem getMediaItem() { 285 return mItem; 286 } 287 isPlayer()288 synchronized boolean isPlayer() { 289 return mIsPlayer; 290 } 291 isNowPlaying()292 synchronized boolean isNowPlaying() { 293 return getID().startsWith(NOW_PLAYING_PREFIX); 294 } 295 296 @Override equals(Object other)297 public boolean equals(Object other) { 298 if (!(other instanceof BrowseNode)) { 299 return false; 300 } 301 BrowseNode otherNode = (BrowseNode) other; 302 return getID().equals(otherNode.getID()); 303 } 304 305 @Override toString()306 public synchronized String toString() { 307 if (VDBG) { 308 String serialized = "[ Name: " + mItem.getDescription().getTitle() 309 + " Scope:" + mBrowseScope + " expected Children: " 310 + mExpectedChildrenCount + "] "; 311 for (BrowseNode node : mChildren) { 312 serialized += node.toString(); 313 } 314 return serialized; 315 } else { 316 return "ID: " + getID(); 317 } 318 } 319 320 // Returns true if target is a descendant of this. isDescendant(BrowseNode target)321 synchronized boolean isDescendant(BrowseNode target) { 322 return getEldestChild(this, target) == null ? false : true; 323 } 324 } 325 findBrowseNodeByID(String parentID)326 synchronized BrowseNode findBrowseNodeByID(String parentID) { 327 BrowseNode bn = mBrowseMap.get(parentID); 328 if (bn == null) { 329 Log.e(TAG, "folder " + parentID + " not found!"); 330 return null; 331 } 332 if (VDBG) { 333 Log.d(TAG, "Size" + mBrowseMap.size()); 334 } 335 return bn; 336 } 337 setCurrentBrowsedFolder(String uid)338 synchronized boolean setCurrentBrowsedFolder(String uid) { 339 BrowseNode bn = mBrowseMap.get(uid); 340 if (bn == null) { 341 Log.e(TAG, "Setting an unknown browsed folder, ignoring bn " + uid); 342 return false; 343 } 344 345 // Set the previous folder as not cached so that we fetch the contents again. 346 if (!bn.equals(mCurrentBrowseNode)) { 347 Log.d(TAG, "Set cache " + bn + " curr " + mCurrentBrowseNode); 348 } 349 mCurrentBrowseNode = bn; 350 return true; 351 } 352 getCurrentBrowsedFolder()353 synchronized BrowseNode getCurrentBrowsedFolder() { 354 return mCurrentBrowseNode; 355 } 356 setCurrentBrowsedPlayer(String uid, int items, int depth)357 synchronized boolean setCurrentBrowsedPlayer(String uid, int items, int depth) { 358 BrowseNode bn = mBrowseMap.get(uid); 359 if (bn == null) { 360 Log.e(TAG, "Setting an unknown browsed player, ignoring bn " + uid); 361 return false; 362 } 363 mCurrentBrowsedPlayer = bn; 364 mCurrentBrowseNode = mCurrentBrowsedPlayer; 365 for (Integer level = 0; level < depth; level++) { 366 BrowseNode dummyNode = new BrowseNode(level.toString()); 367 dummyNode.mParent = mCurrentBrowseNode; 368 dummyNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_VFS; 369 mCurrentBrowseNode = dummyNode; 370 } 371 mCurrentBrowseNode.setExpectedChildren(items); 372 mDepth = depth; 373 return true; 374 } 375 getCurrentBrowsedPlayer()376 synchronized BrowseNode getCurrentBrowsedPlayer() { 377 return mCurrentBrowsedPlayer; 378 } 379 setCurrentAddressedPlayer(String uid)380 synchronized boolean setCurrentAddressedPlayer(String uid) { 381 BrowseNode bn = mBrowseMap.get(uid); 382 if (bn == null) { 383 if (DBG) Log.d(TAG, "Setting an unknown addressed player, ignoring bn " + uid); 384 mRootNode.setCached(false); 385 mRootNode.mChildren.add(mNowPlayingNode); 386 mBrowseMap.put(NOW_PLAYING_PREFIX, mNowPlayingNode); 387 return false; 388 } 389 mCurrentAddressedPlayer = bn; 390 return true; 391 } 392 getCurrentAddressedPlayer()393 synchronized BrowseNode getCurrentAddressedPlayer() { 394 return mCurrentAddressedPlayer; 395 } 396 397 @Override toString()398 public String toString() { 399 String serialized = "Size: " + mBrowseMap.size(); 400 if (VDBG) { 401 serialized += mRootNode.toString(); 402 } 403 return serialized; 404 } 405 406 // Calculates the path to target node. 407 // Returns: UP node to go up 408 // Returns: target node if there 409 // Returns: named node to go down 410 // Returns: null node if unknown getNextStepToFolder(BrowseNode target)411 BrowseNode getNextStepToFolder(BrowseNode target) { 412 if (target == null) { 413 return null; 414 } else if (target.equals(mCurrentBrowseNode) 415 || target.equals(mNowPlayingNode) 416 || target.equals(mRootNode)) { 417 return target; 418 } else if (target.isPlayer()) { 419 if (mDepth > 0) { 420 mDepth--; 421 return mNavigateUpNode; 422 } else { 423 return target; 424 } 425 } else if (mBrowseMap.get(target.getID()) == null) { 426 return null; 427 } else { 428 BrowseNode nextChild = getEldestChild(mCurrentBrowseNode, target); 429 if (nextChild == null) { 430 return mNavigateUpNode; 431 } else { 432 return nextChild; 433 } 434 } 435 } 436 getEldestChild(BrowseNode ancestor, BrowseNode target)437 static BrowseNode getEldestChild(BrowseNode ancestor, BrowseNode target) { 438 // ancestor is an ancestor of target 439 BrowseNode descendant = target; 440 if (DBG) { 441 Log.d(TAG, "NAVIGATING ancestor" + ancestor.toString() + "Target" 442 + target.toString()); 443 } 444 while (!ancestor.equals(descendant.mParent)) { 445 descendant = descendant.mParent; 446 if (descendant == null) { 447 return null; 448 } 449 } 450 if (DBG) Log.d(TAG, "NAVIGATING Descendant" + descendant.toString()); 451 return descendant; 452 } 453 } 454