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