blob: 923282d344011abb297ca18132a1ee9488db03a3 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.bluetooth.avrcpcontroller;
import android.bluetooth.BluetoothDevice;
import android.media.MediaDescription;
import android.media.browse.MediaBrowser;
import android.media.browse.MediaBrowser.MediaItem;
import android.os.Bundle;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
// Browsing hierarchy.
// Root:
// Player1:
// Now_Playing:
// MediaItem1
// MediaItem2
// Folder1
// Folder2
// ....
// Player2
// ....
public class BrowseTree {
private static final String TAG = "BrowseTree";
private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
public static final String ROOT = "__ROOT__";
public static final String UP = "__UP__";
public static final String NOW_PLAYING_PREFIX = "NOW_PLAYING";
public static final String PLAYER_PREFIX = "PLAYER";
// Static instance of Folder ID <-> Folder Instance (for navigation purposes)
private final HashMap<String, BrowseNode> mBrowseMap = new HashMap<String, BrowseNode>();
private BrowseNode mCurrentBrowseNode;
private BrowseNode mCurrentBrowsedPlayer;
private BrowseNode mCurrentAddressedPlayer;
private int mDepth = 0;
final BrowseNode mRootNode;
final BrowseNode mNavigateUpNode;
final BrowseNode mNowPlayingNode;
BrowseTree(BluetoothDevice device) {
if (device == null) {
mRootNode = new BrowseNode(new MediaItem(new MediaDescription.Builder()
.setMediaId(ROOT).setTitle(ROOT).build(), MediaItem.FLAG_BROWSABLE));
mRootNode.setCached(true);
} else {
mRootNode = new BrowseNode(new MediaItem(new MediaDescription.Builder()
.setMediaId(ROOT + device.getAddress().toString()).setTitle(
device.getName()).build(), MediaItem.FLAG_BROWSABLE));
mRootNode.mDevice = device;
}
mRootNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_PLAYER_LIST;
mRootNode.setExpectedChildren(255);
mNavigateUpNode = new BrowseNode(new MediaItem(new MediaDescription.Builder()
.setMediaId(UP).setTitle(UP).build(),
MediaItem.FLAG_BROWSABLE));
mNowPlayingNode = new BrowseNode(new MediaItem(new MediaDescription.Builder()
.setMediaId(NOW_PLAYING_PREFIX)
.setTitle(NOW_PLAYING_PREFIX).build(), MediaItem.FLAG_BROWSABLE));
mNowPlayingNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING;
mNowPlayingNode.setExpectedChildren(255);
mBrowseMap.put(ROOT, mRootNode);
mBrowseMap.put(NOW_PLAYING_PREFIX, mNowPlayingNode);
mCurrentBrowseNode = mRootNode;
}
public void clear() {
// Clearing the map should garbage collect everything.
mBrowseMap.clear();
}
void onConnected(BluetoothDevice device) {
BrowseNode browseNode = new BrowseNode(device);
mRootNode.addChild(browseNode);
}
BrowseNode getTrackFromNowPlayingList(int trackNumber) {
return mNowPlayingNode.getChild(trackNumber);
}
// Each node of the tree is represented by Folder ID, Folder Name and the children.
class BrowseNode {
// MediaItem to store the media related details.
MediaItem mItem;
BluetoothDevice mDevice;
long mBluetoothId;
// Type of this browse node.
// Since Media APIs do not define the player separately we define that
// distinction here.
boolean mIsPlayer = false;
// If this folder is currently cached, can be useful to return the contents
// without doing another fetch.
boolean mCached = false;
byte mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_VFS;
// List of children.
private BrowseNode mParent;
private final List<BrowseNode> mChildren = new ArrayList<BrowseNode>();
private int mExpectedChildrenCount;
BrowseNode(MediaItem item) {
mItem = item;
Bundle extras = mItem.getDescription().getExtras();
if (extras != null) {
mBluetoothId = extras.getLong(AvrcpControllerService.MEDIA_ITEM_UID_KEY);
}
}
BrowseNode(AvrcpPlayer player) {
mIsPlayer = true;
// Transform the player into a item.
MediaDescription.Builder mdb = new MediaDescription.Builder();
String playerKey = PLAYER_PREFIX + player.getId();
mBluetoothId = player.getId();
mdb.setMediaId(UUID.randomUUID().toString());
mdb.setTitle(player.getName());
int mediaItemFlags = player.supportsFeature(AvrcpPlayer.FEATURE_BROWSING)
? MediaBrowser.MediaItem.FLAG_BROWSABLE : 0;
mItem = new MediaBrowser.MediaItem(mdb.build(), mediaItemFlags);
}
BrowseNode(BluetoothDevice device) {
boolean mIsPlayer = true;
mDevice = device;
MediaDescription.Builder mdb = new MediaDescription.Builder();
String playerKey = PLAYER_PREFIX + device.getAddress().toString();
mdb.setMediaId(playerKey);
mdb.setTitle(device.getName());
int mediaItemFlags = MediaBrowser.MediaItem.FLAG_BROWSABLE;
mItem = new MediaBrowser.MediaItem(mdb.build(), mediaItemFlags);
}
private BrowseNode(String name) {
MediaDescription.Builder mdb = new MediaDescription.Builder();
mdb.setMediaId(name);
mdb.setTitle(name);
mItem = new MediaBrowser.MediaItem(mdb.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE);
}
synchronized void setExpectedChildren(int count) {
mExpectedChildrenCount = count;
}
synchronized int getExpectedChildren() {
return mExpectedChildrenCount;
}
synchronized <E> int addChildren(List<E> newChildren) {
for (E child : newChildren) {
BrowseNode currentNode = null;
if (child instanceof MediaItem) {
currentNode = new BrowseNode((MediaItem) child);
} else if (child instanceof AvrcpPlayer) {
currentNode = new BrowseNode((AvrcpPlayer) child);
}
addChild(currentNode);
}
return newChildren.size();
}
synchronized boolean addChild(BrowseNode node) {
if (node != null) {
node.mParent = this;
if (this.mBrowseScope == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
node.mBrowseScope = this.mBrowseScope;
}
if (node.mDevice == null) {
node.mDevice = this.mDevice;
}
mChildren.add(node);
mBrowseMap.put(node.getID(), node);
return true;
}
return false;
}
synchronized void removeChild(BrowseNode node) {
mChildren.remove(node);
mBrowseMap.remove(node.getID());
}
synchronized int getChildrenCount() {
return mChildren.size();
}
synchronized List<BrowseNode> getChildren() {
return mChildren;
}
synchronized BrowseNode getChild(int index) {
if (index < 0 || index >= mChildren.size()) {
return null;
}
return mChildren.get(index);
}
synchronized BrowseNode getParent() {
return mParent;
}
synchronized List<MediaItem> getContents() {
if (mChildren.size() > 0 || mCached) {
List<MediaItem> contents = new ArrayList<MediaItem>(mChildren.size());
for (BrowseNode child : mChildren) {
contents.add(child.getMediaItem());
}
return contents;
}
return null;
}
synchronized boolean isChild(BrowseNode node) {
return mChildren.contains(node);
}
synchronized boolean isCached() {
return mCached;
}
synchronized boolean isBrowsable() {
return mItem.isBrowsable();
}
synchronized void setCached(boolean cached) {
if (DBG) Log.d(TAG, "Set Cache" + cached + "Node" + toString());
mCached = cached;
if (!cached) {
for (BrowseNode child : mChildren) {
mBrowseMap.remove(child.getID());
}
mChildren.clear();
}
}
// Fetch the Unique UID for this item, this is unique across all elements in the tree.
synchronized String getID() {
return mItem.getDescription().getMediaId();
}
// Get the BT Player ID associated with this node.
synchronized int getPlayerID() {
return Integer.parseInt(getID().replace(PLAYER_PREFIX, ""));
}
synchronized byte getScope() {
return mBrowseScope;
}
// Fetch the Folder UID that can be used to fetch folder listing via bluetooth.
// This may not be unique hence this combined with direction will define the
// browsing here.
synchronized String getFolderUID() {
return getID();
}
synchronized long getBluetoothID() {
return mBluetoothId;
}
synchronized MediaItem getMediaItem() {
return mItem;
}
synchronized boolean isPlayer() {
return mIsPlayer;
}
synchronized boolean isNowPlaying() {
return getID().startsWith(NOW_PLAYING_PREFIX);
}
@Override
public boolean equals(Object other) {
if (!(other instanceof BrowseNode)) {
return false;
}
BrowseNode otherNode = (BrowseNode) other;
return getID().equals(otherNode.getID());
}
@Override
public synchronized String toString() {
if (VDBG) {
String serialized = "[ Name: " + mItem.getDescription().getTitle()
+ " Scope:" + mBrowseScope + " expected Children: "
+ mExpectedChildrenCount + "] ";
for (BrowseNode node : mChildren) {
serialized += node.toString();
}
return serialized;
} else {
return "ID: " + getID();
}
}
// Returns true if target is a descendant of this.
synchronized boolean isDescendant(BrowseNode target) {
return getEldestChild(this, target) == null ? false : true;
}
}
synchronized BrowseNode findBrowseNodeByID(String parentID) {
BrowseNode bn = mBrowseMap.get(parentID);
if (bn == null) {
Log.e(TAG, "folder " + parentID + " not found!");
return null;
}
if (VDBG) {
Log.d(TAG, "Size" + mBrowseMap.size());
}
return bn;
}
synchronized boolean setCurrentBrowsedFolder(String uid) {
BrowseNode bn = mBrowseMap.get(uid);
if (bn == null) {
Log.e(TAG, "Setting an unknown browsed folder, ignoring bn " + uid);
return false;
}
// Set the previous folder as not cached so that we fetch the contents again.
if (!bn.equals(mCurrentBrowseNode)) {
Log.d(TAG, "Set cache " + bn + " curr " + mCurrentBrowseNode);
}
mCurrentBrowseNode = bn;
return true;
}
synchronized BrowseNode getCurrentBrowsedFolder() {
return mCurrentBrowseNode;
}
synchronized boolean setCurrentBrowsedPlayer(String uid, int items, int depth) {
BrowseNode bn = mBrowseMap.get(uid);
if (bn == null) {
Log.e(TAG, "Setting an unknown browsed player, ignoring bn " + uid);
return false;
}
mCurrentBrowsedPlayer = bn;
mCurrentBrowseNode = mCurrentBrowsedPlayer;
for (Integer level = 0; level < depth; level++) {
BrowseNode dummyNode = new BrowseNode(level.toString());
dummyNode.mParent = mCurrentBrowseNode;
dummyNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_VFS;
mCurrentBrowseNode = dummyNode;
}
mCurrentBrowseNode.setExpectedChildren(items);
mDepth = depth;
return true;
}
synchronized BrowseNode getCurrentBrowsedPlayer() {
return mCurrentBrowsedPlayer;
}
synchronized boolean setCurrentAddressedPlayer(String uid) {
BrowseNode bn = mBrowseMap.get(uid);
if (bn == null) {
if (DBG) Log.d(TAG, "Setting an unknown addressed player, ignoring bn " + uid);
mRootNode.setCached(false);
mRootNode.mChildren.add(mNowPlayingNode);
mBrowseMap.put(NOW_PLAYING_PREFIX, mNowPlayingNode);
return false;
}
mCurrentAddressedPlayer = bn;
return true;
}
synchronized BrowseNode getCurrentAddressedPlayer() {
return mCurrentAddressedPlayer;
}
@Override
public String toString() {
String serialized = "Size: " + mBrowseMap.size();
if (VDBG) {
serialized += mRootNode.toString();
}
return serialized;
}
// Calculates the path to target node.
// Returns: UP node to go up
// Returns: target node if there
// Returns: named node to go down
// Returns: null node if unknown
BrowseNode getNextStepToFolder(BrowseNode target) {
if (target == null) {
return null;
} else if (target.equals(mCurrentBrowseNode)
|| target.equals(mNowPlayingNode)
|| target.equals(mRootNode)) {
return target;
} else if (target.isPlayer()) {
if (mDepth > 0) {
mDepth--;
return mNavigateUpNode;
} else {
return target;
}
} else if (mBrowseMap.get(target.getID()) == null) {
return null;
} else {
BrowseNode nextChild = getEldestChild(mCurrentBrowseNode, target);
if (nextChild == null) {
return mNavigateUpNode;
} else {
return nextChild;
}
}
}
static BrowseNode getEldestChild(BrowseNode ancestor, BrowseNode target) {
// ancestor is an ancestor of target
BrowseNode descendant = target;
if (DBG) {
Log.d(TAG, "NAVIGATING ancestor" + ancestor.toString() + "Target"
+ target.toString());
}
while (!ancestor.equals(descendant.mParent)) {
descendant = descendant.mParent;
if (descendant == null) {
return null;
}
}
if (DBG) Log.d(TAG, "NAVIGATING Descendant" + descendant.toString());
return descendant;
}
}