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.media.browse.MediaBrowser; 20 import android.media.browse.MediaBrowser.MediaItem; 21 import android.media.MediaDescription; 22 import android.os.Bundle; 23 import android.os.ResultReceiver; 24 import android.service.media.MediaBrowserService.Result; 25 import android.util.Log; 26 27 import java.util.ArrayList; 28 import java.util.HashMap; 29 import java.util.List; 30 import java.util.Stack; 31 32 // Browsing hierarchy. 33 // Root: 34 // Player1: 35 // Now_Playing: 36 // MediaItem1 37 // MediaItem2 38 // Folder1 39 // Folder2 40 // .... 41 // Player2 42 // .... 43 public class BrowseTree { 44 private static final String TAG = "BrowseTree"; 45 private static final boolean DBG = true; 46 47 public static final int DIRECTION_DOWN = 0; 48 public static final int DIRECTION_UP = 1; 49 public static final int DIRECTION_SAME = 2; 50 public static final int DIRECTION_UNKNOWN = -1; 51 52 public static final String ROOT = "__ROOT__"; 53 public static final String NOW_PLAYING_PREFIX = "NOW_PLAYING"; 54 public static final String PLAYER_PREFIX = "PLAYER"; 55 56 // Static instance of Folder ID <-> Folder Instance (for navigation purposes) 57 private final HashMap<String, BrowseNode> mBrowseMap = new HashMap<String, BrowseNode>(); 58 private BrowseNode mCurrentBrowseNode; 59 private BrowseNode mCurrentBrowsedPlayer; 60 private BrowseNode mCurrentAddressedPlayer; 61 BrowseTree()62 BrowseTree() { 63 } 64 init()65 public void init() { 66 MediaDescription.Builder mdb = new MediaDescription.Builder(); 67 mdb.setMediaId(ROOT); 68 mdb.setTitle(ROOT); 69 Bundle mdBundle = new Bundle(); 70 mdBundle.putString(AvrcpControllerService.MEDIA_ITEM_UID_KEY, ROOT); 71 mdb.setExtras(mdBundle); 72 mBrowseMap.put(ROOT, new BrowseNode(new MediaItem(mdb.build(), MediaItem.FLAG_BROWSABLE))); 73 mCurrentBrowseNode = mBrowseMap.get(ROOT); 74 } 75 clear()76 public void clear() { 77 // Clearing the map should garbage collect everything. 78 mBrowseMap.clear(); 79 } 80 81 // Each node of the tree is represented by Folder ID, Folder Name and the children. 82 class BrowseNode { 83 // MediaItem to store the media related details. 84 MediaItem mItem; 85 86 // Type of this browse node. 87 // Since Media APIs do not define the player separately we define that 88 // distinction here. 89 boolean mIsPlayer = false; 90 91 // If this folder is currently cached, can be useful to return the contents 92 // without doing another fetch. 93 boolean mCached = false; 94 95 // Result object if this node is not loaded yet. This result object will be used 96 // once loading is finished. 97 Result<List<MediaItem>> mResult = null; 98 99 // List of children. 100 final List<BrowseNode> mChildren = new ArrayList<BrowseNode>(); 101 BrowseNode(MediaItem item)102 BrowseNode(MediaItem item) { 103 mItem = item; 104 } 105 BrowseNode(AvrcpPlayer player)106 BrowseNode(AvrcpPlayer player) { 107 mIsPlayer = true; 108 109 // Transform the player into a item. 110 MediaDescription.Builder mdb = new MediaDescription.Builder(); 111 Bundle mdExtra = new Bundle(); 112 String playerKey = PLAYER_PREFIX + player.getId(); 113 mdExtra.putString(AvrcpControllerService.MEDIA_ITEM_UID_KEY, playerKey); 114 mdb.setExtras(mdExtra); 115 mdb.setMediaId(playerKey); 116 mdb.setTitle(player.getName()); 117 mItem = new MediaBrowser.MediaItem(mdb.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE); 118 } 119 getChildren()120 synchronized List<BrowseNode> getChildren() { 121 return mChildren; 122 } 123 isChild(BrowseNode node)124 synchronized boolean isChild(BrowseNode node) { 125 for (BrowseNode bn : mChildren) { 126 if (bn.equals(node)) { 127 return true; 128 } 129 } 130 return false; 131 } 132 isCached()133 synchronized boolean isCached() { 134 return mCached; 135 } 136 setCached(boolean cached)137 synchronized void setCached(boolean cached) { 138 mCached = cached; 139 } 140 141 // Fetch the Unique UID for this item, this is unique across all elements in the tree. getID()142 synchronized String getID() { 143 return mItem.getDescription().getMediaId(); 144 } 145 146 // Get the BT Player ID associated with this node. getPlayerID()147 synchronized int getPlayerID() { 148 return Integer.parseInt(getID().replace(PLAYER_PREFIX, "")); 149 } 150 151 // Fetch the Folder UID that can be used to fetch folder listing via bluetooth. 152 // This may not be unique hence this combined with direction will define the 153 // browsing here. getFolderUID()154 synchronized String getFolderUID() { 155 return mItem.getDescription().getExtras().getString( 156 AvrcpControllerService.MEDIA_ITEM_UID_KEY); 157 } 158 getMediaItem()159 synchronized MediaItem getMediaItem() { 160 return mItem; 161 } 162 isPlayer()163 synchronized boolean isPlayer() { 164 return mIsPlayer; 165 } 166 isNowPlaying()167 synchronized boolean isNowPlaying() { 168 return getID().startsWith(NOW_PLAYING_PREFIX); 169 } 170 171 @Override equals(Object other)172 public boolean equals(Object other) { 173 if (!(other instanceof BrowseNode)) { 174 return false; 175 } 176 BrowseNode otherNode = (BrowseNode) other; 177 return getID().equals(otherNode.getID()); 178 } 179 180 @Override toString()181 public String toString() { 182 return "ID: " + getID() + " desc: " + mItem; 183 } 184 } 185 refreshChildren(String parentID, List<E> children)186 synchronized <E> void refreshChildren(String parentID, List<E> children) { 187 BrowseNode parent = findFolderByIDLocked(parentID); 188 if (parent == null) { 189 Log.w(TAG, "parent not found for parentID " + parentID); 190 return; 191 } 192 refreshChildren(parent, children); 193 } 194 refreshChildren(BrowseNode parent, List<E> children)195 synchronized <E> void refreshChildren(BrowseNode parent, List<E> children) { 196 if (children == null) { 197 Log.e(TAG, "children cannot be null "); 198 return; 199 } 200 201 List<BrowseNode> bnList = new ArrayList<BrowseNode>(); 202 for (E child : children) { 203 if (child instanceof MediaItem) { 204 bnList.add(new BrowseNode((MediaItem) child)); 205 } else if (child instanceof AvrcpPlayer) { 206 bnList.add(new BrowseNode((AvrcpPlayer) child)); 207 } 208 } 209 210 String parentID = parent.getID(); 211 // Make sure that the child list is clean. 212 if (DBG) { 213 Log.d(TAG, "parent " + parentID + " child list " + parent.getChildren()); 214 } 215 216 addChildrenLocked(parent, bnList); 217 List<MediaItem> childrenList = new ArrayList<MediaItem>(); 218 for (BrowseNode bn : parent.getChildren()) { 219 childrenList.add(bn.getMediaItem()); 220 } 221 222 parent.setCached(true); 223 } 224 findBrowseNodeByID(String parentID)225 synchronized BrowseNode findBrowseNodeByID(String parentID) { 226 BrowseNode bn = mBrowseMap.get(parentID); 227 if (bn == null) { 228 Log.e(TAG, "folder " + parentID + " not found!"); 229 return null; 230 } 231 if (DBG) { 232 Log.d(TAG, "Browse map: " + mBrowseMap); 233 } 234 return bn; 235 } 236 findFolderByIDLocked(String parentID)237 BrowseNode findFolderByIDLocked(String parentID) { 238 return mBrowseMap.get(parentID); 239 } 240 addChildrenLocked(BrowseNode parent, List<BrowseNode> items)241 void addChildrenLocked(BrowseNode parent, List<BrowseNode> items) { 242 // Remove existing children and then add the new children. 243 for (BrowseNode c : parent.getChildren()) { 244 mBrowseMap.remove(c.getID()); 245 } 246 parent.getChildren().clear(); 247 248 for (BrowseNode bn : items) { 249 parent.getChildren().add(bn); 250 mBrowseMap.put(bn.getID(), bn); 251 } 252 } 253 getDirection(String toUID)254 synchronized int getDirection(String toUID) { 255 BrowseNode fromFolder = mCurrentBrowseNode; 256 BrowseNode toFolder = findFolderByIDLocked(toUID); 257 if (fromFolder == null || toFolder == null) { 258 Log.e(TAG, "from folder " + mCurrentBrowseNode + " or to folder " + toUID + " null!"); 259 } 260 261 // Check the relationship. 262 if (fromFolder.isChild(toFolder)) { 263 return DIRECTION_DOWN; 264 } else if (toFolder.isChild(fromFolder)) { 265 return DIRECTION_UP; 266 } else if (fromFolder.equals(toFolder)) { 267 return DIRECTION_SAME; 268 } else { 269 Log.w(TAG, "from folder " + mCurrentBrowseNode + " children " + 270 fromFolder.getChildren() + "to folder " + toUID + " children " + 271 toFolder.getChildren()); 272 return DIRECTION_UNKNOWN; 273 } 274 } 275 setCurrentBrowsedFolder(String uid)276 synchronized boolean setCurrentBrowsedFolder(String uid) { 277 BrowseNode bn = findFolderByIDLocked(uid); 278 if (bn == null) { 279 Log.e(TAG, "Setting an unknown browsed folder, ignoring bn " + uid); 280 return false; 281 } 282 283 // Set the previous folder as not cached so that we fetch the contents again. 284 if (!bn.equals(mCurrentBrowseNode)) { 285 Log.d(TAG, "Set cache false " + bn + " curr " + mCurrentBrowseNode); 286 mCurrentBrowseNode.setCached(false); 287 } 288 289 mCurrentBrowseNode = bn; 290 return true; 291 } 292 getCurrentBrowsedFolder()293 synchronized BrowseNode getCurrentBrowsedFolder() { 294 return mCurrentBrowseNode; 295 } 296 setCurrentBrowsedPlayer(String uid)297 synchronized boolean setCurrentBrowsedPlayer(String uid) { 298 BrowseNode bn = findFolderByIDLocked(uid); 299 if (bn == null) { 300 Log.e(TAG, "Setting an unknown browsed player, ignoring bn " + uid); 301 return false; 302 } 303 mCurrentBrowsedPlayer = bn; 304 return true; 305 } 306 getCurrentBrowsedPlayer()307 synchronized BrowseNode getCurrentBrowsedPlayer() { 308 return mCurrentBrowsedPlayer; 309 } 310 setCurrentAddressedPlayer(String uid)311 synchronized boolean setCurrentAddressedPlayer(String uid) { 312 BrowseNode bn = findFolderByIDLocked(uid); 313 if (bn == null) { 314 Log.e(TAG, "Setting an unknown addressed player, ignoring bn " + uid); 315 return false; 316 } 317 mCurrentAddressedPlayer = bn; 318 return true; 319 } 320 getCurrentAddressedPlayer()321 synchronized BrowseNode getCurrentAddressedPlayer() { 322 return mCurrentAddressedPlayer; 323 } 324 325 @Override toString()326 public String toString() { 327 return mBrowseMap.toString(); 328 } 329 } 330