blob: ef1b184cf9080ef74b0f21d8bf0d6adee5216d3e [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.BluetoothAvrcpController;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Message;
import android.support.v4.media.MediaBrowserCompat.MediaItem;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import android.util.SparseArray;
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.R;
import com.android.bluetooth.Utils;
import com.android.bluetooth.a2dpsink.A2dpSinkService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.statemachine.State;
import com.android.bluetooth.statemachine.StateMachine;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Provides Bluetooth AVRCP Controller State Machine responsible for all remote control connections
* and interactions with a remote controlable device.
*/
class AvrcpControllerStateMachine extends StateMachine {
static final String TAG = "AvrcpControllerStateMachine";
static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
//0->99 Events from Outside
public static final int CONNECT = 1;
public static final int DISCONNECT = 2;
public static final int ACTIVE_DEVICE_CHANGE = 3;
//100->199 Internal Events
protected static final int CLEANUP = 100;
private static final int CONNECT_TIMEOUT = 101;
static final int MESSAGE_INTERNAL_ABS_VOL_TIMEOUT = 102;
//200->299 Events from Native
static final int STACK_EVENT = 200;
static final int MESSAGE_INTERNAL_CMD_TIMEOUT = 201;
static final int MESSAGE_PROCESS_SET_ABS_VOL_CMD = 203;
static final int MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION = 204;
static final int MESSAGE_PROCESS_TRACK_CHANGED = 205;
static final int MESSAGE_PROCESS_PLAY_POS_CHANGED = 206;
static final int MESSAGE_PROCESS_PLAY_STATUS_CHANGED = 207;
static final int MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION = 208;
static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS = 209;
static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE = 210;
static final int MESSAGE_PROCESS_GET_PLAYER_ITEMS = 211;
static final int MESSAGE_PROCESS_FOLDER_PATH = 212;
static final int MESSAGE_PROCESS_SET_BROWSED_PLAYER = 213;
static final int MESSAGE_PROCESS_SET_ADDRESSED_PLAYER = 214;
static final int MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED = 215;
static final int MESSAGE_PROCESS_NOW_PLAYING_CONTENTS_CHANGED = 216;
static final int MESSAGE_PROCESS_SUPPORTED_APPLICATION_SETTINGS = 217;
static final int MESSAGE_PROCESS_CURRENT_APPLICATION_SETTINGS = 218;
static final int MESSAGE_PROCESS_AVAILABLE_PLAYER_CHANGED = 219;
static final int MESSAGE_PROCESS_RECEIVED_COVER_ART_PSM = 220;
//300->399 Events for Browsing
static final int MESSAGE_GET_FOLDER_ITEMS = 300;
static final int MESSAGE_PLAY_ITEM = 301;
static final int MSG_AVRCP_PASSTHRU = 302;
static final int MSG_AVRCP_SET_SHUFFLE = 303;
static final int MSG_AVRCP_SET_REPEAT = 304;
//400->499 Events for Cover Artwork
static final int MESSAGE_PROCESS_IMAGE_DOWNLOADED = 400;
/*
* Base value for absolute volume from JNI
*/
private static final int ABS_VOL_BASE = 127;
/*
* Notification types for Avrcp protocol JNI.
*/
private static final byte NOTIFICATION_RSP_TYPE_INTERIM = 0x00;
private static final byte NOTIFICATION_RSP_TYPE_CHANGED = 0x01;
private static BluetoothDevice sActiveDevice;
private final AudioManager mAudioManager;
private final boolean mIsVolumeFixed;
protected final BluetoothDevice mDevice;
protected final byte[] mDeviceAddress;
protected final AvrcpControllerService mService;
protected int mCoverArtPsm;
protected final AvrcpCoverArtManager mCoverArtManager;
protected final Disconnected mDisconnected;
protected final Connecting mConnecting;
protected final Connected mConnected;
protected final Disconnecting mDisconnecting;
protected int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
boolean mRemoteControlConnected = false;
boolean mBrowsingConnected = false;
final BrowseTree mBrowseTree;
private AvrcpPlayer mAddressedPlayer;
private int mAddressedPlayerId;
private SparseArray<AvrcpPlayer> mAvailablePlayerList;
private int mVolumeChangedNotificationsToIgnore = 0;
private int mVolumeNotificationLabel = -1;
GetFolderList mGetFolderList = null;
//Number of items to get in a single fetch
static final int ITEM_PAGE_SIZE = 20;
static final int CMD_TIMEOUT_MILLIS = 10000;
static final int ABS_VOL_TIMEOUT_MILLIS = 1000; //1s
AvrcpControllerStateMachine(BluetoothDevice device, AvrcpControllerService service) {
super(TAG);
mDevice = device;
mDeviceAddress = Utils.getByteAddress(mDevice);
mService = service;
mCoverArtPsm = 0;
mCoverArtManager = service.getCoverArtManager();
logD(device.toString());
mAvailablePlayerList = new SparseArray<AvrcpPlayer>();
mAddressedPlayerId = AvrcpPlayer.DEFAULT_ID;
AvrcpPlayer.Builder apb = new AvrcpPlayer.Builder();
apb.setDevice(mDevice);
apb.setPlayerId(mAddressedPlayerId);
apb.setSupportedFeature(AvrcpPlayer.FEATURE_PLAY);
apb.setSupportedFeature(AvrcpPlayer.FEATURE_PAUSE);
apb.setSupportedFeature(AvrcpPlayer.FEATURE_STOP);
apb.setSupportedFeature(AvrcpPlayer.FEATURE_FORWARD);
apb.setSupportedFeature(AvrcpPlayer.FEATURE_PREVIOUS);
mAddressedPlayer = apb.build();
mAvailablePlayerList.put(mAddressedPlayerId, mAddressedPlayer);
mBrowseTree = new BrowseTree(mDevice);
mDisconnected = new Disconnected();
mConnecting = new Connecting();
mConnected = new Connected();
mDisconnecting = new Disconnecting();
addState(mDisconnected);
addState(mConnecting);
addState(mConnected);
addState(mDisconnecting);
mGetFolderList = new GetFolderList();
addState(mGetFolderList, mConnected);
mAudioManager = (AudioManager) service.getSystemService(Context.AUDIO_SERVICE);
mIsVolumeFixed = mAudioManager.isVolumeFixed();
setInitialState(mDisconnected);
}
BrowseTree.BrowseNode findNode(String parentMediaId) {
logD("FindNode");
return mBrowseTree.findBrowseNodeByID(parentMediaId);
}
/**
* Get the current connection state
*
* @return current State
*/
public int getState() {
return mMostRecentState;
}
/**
* Get the underlying device tracked by this state machine
*
* @return device in focus
*/
public BluetoothDevice getDevice() {
return mDevice;
}
/**
* send the connection event asynchronously
*/
public boolean connect(StackEvent event) {
if (event.mBrowsingConnected) {
onBrowsingConnected();
}
mRemoteControlConnected = event.mRemoteControlConnected;
sendMessage(CONNECT);
return true;
}
/**
* send the Disconnect command asynchronously
*/
public void disconnect() {
sendMessage(DISCONNECT);
}
/**
* Get the current playing track
*/
public AvrcpItem getCurrentTrack() {
return mAddressedPlayer.getCurrentTrack();
}
@VisibleForTesting
int getAddressedPlayerId() {
return mAddressedPlayerId;
}
@VisibleForTesting
SparseArray<AvrcpPlayer> getAvailablePlayers() {
return mAvailablePlayerList;
}
/**
* Dump the current State Machine to the string builder.
*
* @param sb output string
*/
public void dump(StringBuilder sb) {
ProfileService.println(sb, "mDevice: " + mDevice.getAddress() + "("
+ mDevice.getName() + ") " + this.toString());
ProfileService.println(sb, "isActive: " + isActive());
ProfileService.println(sb, "Control: " + mRemoteControlConnected);
ProfileService.println(sb, "Browsing: " + mBrowsingConnected);
ProfileService.println(sb, "Cover Art: "
+ (mCoverArtManager.getState(mDevice) == BluetoothProfile.STATE_CONNECTED));
ProfileService.println(sb, "Addressed Player ID: " + mAddressedPlayerId);
ProfileService.println(sb, "Available Players (" + mAvailablePlayerList.size() + "): ");
for (int i = 0; i < mAvailablePlayerList.size(); i++) {
AvrcpPlayer player = mAvailablePlayerList.valueAt(i);
boolean isAddressed = (player.getId() == mAddressedPlayerId);
ProfileService.println(sb, "\t" + (isAddressed ? "(Addressed) " : "") + player);
}
List<MediaItem> queue = null;
if (mBrowseTree.mNowPlayingNode != null) {
queue = mBrowseTree.mNowPlayingNode.getContents();
}
ProfileService.println(sb, "Queue (" + (queue == null ? 0 : queue.size()) + "): " + queue);
}
@VisibleForTesting
boolean isActive() {
return mDevice.equals(mService.getActiveDevice());
}
/**
* Attempt to set the active status for this device
*/
public void setDeviceState(int state) {
sendMessage(ACTIVE_DEVICE_CHANGE, state);
}
@Override
protected void unhandledMessage(Message msg) {
Log.w(TAG, "Unhandled message in state " + getCurrentState() + "msg.what=" + msg.what);
}
private static void logD(String message) {
if (DBG) {
Log.d(TAG, message);
}
}
synchronized void onBrowsingConnected() {
mBrowsingConnected = true;
requestContents(mBrowseTree.mRootNode);
}
synchronized void onBrowsingDisconnected() {
if (!mBrowsingConnected) return;
mAddressedPlayer.setPlayStatus(PlaybackStateCompat.STATE_ERROR);
AvrcpItem previousTrack = mAddressedPlayer.getCurrentTrack();
String previousTrackUuid = previousTrack != null ? previousTrack.getCoverArtUuid() : null;
mAddressedPlayer.updateCurrentTrack(null);
mBrowseTree.mNowPlayingNode.setCached(false);
mBrowseTree.mRootNode.setCached(false);
if (isActive()) {
BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mNowPlayingNode);
BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mRootNode);
}
removeUnusedArtwork(previousTrackUuid);
removeUnusedArtworkFromBrowseTree();
mBrowsingConnected = false;
}
synchronized void connectCoverArt() {
// Called from "connected" state, which assumes either control or browse is connected
if (mCoverArtManager != null && mCoverArtPsm != 0
&& mCoverArtManager.getState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
logD("Attempting to connect to AVRCP BIP, psm: " + mCoverArtPsm);
mCoverArtManager.connect(mDevice, /* psm */ mCoverArtPsm);
}
}
synchronized void refreshCoverArt() {
if (mCoverArtManager != null && mCoverArtPsm != 0
&& mCoverArtManager.getState(mDevice) == BluetoothProfile.STATE_CONNECTED) {
logD("Attempting to refresh AVRCP BIP OBEX session, psm: " + mCoverArtPsm);
mCoverArtManager.refreshSession(mDevice);
}
}
synchronized void disconnectCoverArt() {
// Safe to call even if we're not connected
if (mCoverArtManager != null) {
logD("Disconnect BIP cover artwork");
mCoverArtManager.disconnect(mDevice);
}
}
/**
* Remove an unused cover art image from storage if it's unused by the browse tree and the
* current track.
*/
synchronized void removeUnusedArtwork(String previousTrackUuid) {
logD("removeUnusedArtwork(" + previousTrackUuid + ")");
if (mCoverArtManager == null) return;
AvrcpItem currentTrack = getCurrentTrack();
String currentTrackUuid = currentTrack != null ? currentTrack.getCoverArtUuid() : null;
if (previousTrackUuid != null) {
if (!previousTrackUuid.equals(currentTrackUuid)
&& mBrowseTree.getNodesUsingCoverArt(previousTrackUuid).isEmpty()) {
mCoverArtManager.removeImage(mDevice, previousTrackUuid);
}
}
}
/**
* Queries the browse tree for unused uuids and removes the associated images from storage
* if the uuid is not used by the current track.
*/
synchronized void removeUnusedArtworkFromBrowseTree() {
logD("removeUnusedArtworkFromBrowseTree()");
if (mCoverArtManager == null) return;
AvrcpItem currentTrack = getCurrentTrack();
String currentTrackUuid = currentTrack != null ? currentTrack.getCoverArtUuid() : null;
ArrayList<String> unusedArtwork = mBrowseTree.getAndClearUnusedCoverArt();
for (String uuid : unusedArtwork) {
if (!uuid.equals(currentTrackUuid)) {
mCoverArtManager.removeImage(mDevice, uuid);
}
}
}
private void notifyChanged(BrowseTree.BrowseNode node) {
// We should only notify now playing content updates if we're the active device. VFS
// updates are fine at any time
int scope = node.getScope();
if (scope != AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING
|| (scope == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING
&& isActive())) {
BluetoothMediaBrowserService.notifyChanged(node);
}
}
private void notifyChanged(PlaybackStateCompat state) {
if (isActive()) {
BluetoothMediaBrowserService.notifyChanged(state);
}
}
void requestContents(BrowseTree.BrowseNode node) {
sendMessage(MESSAGE_GET_FOLDER_ITEMS, node);
logD("Fetching " + node);
}
public void playItem(BrowseTree.BrowseNode node) {
sendMessage(MESSAGE_PLAY_ITEM, node);
}
void nowPlayingContentChanged() {
mBrowseTree.mNowPlayingNode.setCached(false);
removeUnusedArtworkFromBrowseTree();
sendMessage(MESSAGE_GET_FOLDER_ITEMS, mBrowseTree.mNowPlayingNode);
}
protected class Disconnected extends State {
@Override
public void enter() {
logD("Enter Disconnected");
if (mMostRecentState != BluetoothProfile.STATE_DISCONNECTED) {
sendMessage(CLEANUP);
}
broadcastConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTED);
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case MESSAGE_PROCESS_RECEIVED_COVER_ART_PSM:
mCoverArtPsm = message.arg1;
break;
case CONNECT:
logD("Connect");
transitionTo(mConnecting);
break;
case CLEANUP:
mService.removeStateMachine(AvrcpControllerStateMachine.this);
break;
case ACTIVE_DEVICE_CHANGE:
// Wait until we're connected to process this
deferMessage(message);
break;
}
return true;
}
}
protected class Connecting extends State {
@Override
public void enter() {
logD("Enter Connecting");
broadcastConnectionStateChanged(BluetoothProfile.STATE_CONNECTING);
transitionTo(mConnected);
}
}
class Connected extends State {
private static final String STATE_TAG = "Avrcp.ConnectedAvrcpController";
private int mCurrentlyHeldKey = 0;
@Override
public void enter() {
if (mMostRecentState == BluetoothProfile.STATE_CONNECTING) {
broadcastConnectionStateChanged(BluetoothProfile.STATE_CONNECTED);
mService.sBrowseTree.mRootNode.addChild(mBrowseTree.mRootNode);
BluetoothMediaBrowserService.notifyChanged(mService.sBrowseTree.mRootNode);
connectCoverArt(); // only works if we have a valid PSM
} else {
logD("ReEnteringConnected");
}
super.enter();
}
@Override
public boolean processMessage(Message msg) {
logD(STATE_TAG + " processMessage " + msg.what);
switch (msg.what) {
case ACTIVE_DEVICE_CHANGE:
int state = msg.arg1;
if (state == AvrcpControllerService.DEVICE_STATE_ACTIVE) {
BluetoothMediaBrowserService.addressedPlayerChanged(mSessionCallbacks);
BluetoothMediaBrowserService.trackChanged(
mAddressedPlayer.getCurrentTrack());
BluetoothMediaBrowserService.notifyChanged(
mAddressedPlayer.getPlaybackState());
BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mNowPlayingNode);
} else {
sendMessage(MSG_AVRCP_PASSTHRU,
AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE);
}
return true;
case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
mVolumeChangedNotificationsToIgnore++;
removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT);
sendMessageDelayed(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT,
ABS_VOL_TIMEOUT_MILLIS);
handleAbsVolumeRequest(msg.arg1, msg.arg2);
return true;
case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
mVolumeNotificationLabel = msg.arg1;
mService.sendRegisterAbsVolRspNative(mDeviceAddress,
NOTIFICATION_RSP_TYPE_INTERIM,
getAbsVolume(), mVolumeNotificationLabel);
return true;
case MESSAGE_GET_FOLDER_ITEMS:
transitionTo(mGetFolderList);
return true;
case MESSAGE_PLAY_ITEM:
//Set Addressed Player
processPlayItem((BrowseTree.BrowseNode) msg.obj);
return true;
case MSG_AVRCP_PASSTHRU:
passThru(msg.arg1);
return true;
case MSG_AVRCP_SET_REPEAT:
setRepeat(msg.arg1);
return true;
case MSG_AVRCP_SET_SHUFFLE:
setShuffle(msg.arg1);
return true;
case MESSAGE_PROCESS_TRACK_CHANGED:
AvrcpItem track = (AvrcpItem) msg.obj;
AvrcpItem previousTrack = mAddressedPlayer.getCurrentTrack();
downloadImageIfNeeded(track);
mAddressedPlayer.updateCurrentTrack(track);
if (isActive()) {
BluetoothMediaBrowserService.trackChanged(track);
}
if (previousTrack != null) {
removeUnusedArtwork(previousTrack.getCoverArtUuid());
removeUnusedArtworkFromBrowseTree();
}
return true;
case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
mAddressedPlayer.setPlayStatus(msg.arg1);
if (!isActive()) {
sendMessage(MSG_AVRCP_PASSTHRU,
AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE);
return true;
}
PlaybackStateCompat playbackState = mAddressedPlayer.getPlaybackState();
BluetoothMediaBrowserService.notifyChanged(playbackState);
int focusState = AudioManager.ERROR;
A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();
if (a2dpSinkService != null) {
focusState = a2dpSinkService.getFocusState();
}
if (focusState == AudioManager.ERROR) {
sendMessage(MSG_AVRCP_PASSTHRU,
AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE);
return true;
}
if (playbackState.getState() == PlaybackStateCompat.STATE_PLAYING
&& focusState == AudioManager.AUDIOFOCUS_NONE) {
if (shouldRequestFocus()) {
mSessionCallbacks.onPrepare();
} else {
sendMessage(MSG_AVRCP_PASSTHRU,
AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE);
}
}
return true;
case MESSAGE_PROCESS_PLAY_POS_CHANGED:
if (msg.arg2 != -1) {
mAddressedPlayer.setPlayTime(msg.arg2);
notifyChanged(mAddressedPlayer.getPlaybackState());
}
return true;
case MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED:
int oldAddressedPlayerId = mAddressedPlayerId;
mAddressedPlayerId = msg.arg1;
logD("AddressedPlayer changed " + oldAddressedPlayerId + " -> "
+ mAddressedPlayerId);
// The now playing list is tied to the addressed player by specification in
// AVRCP 5.9.1. A new addressed player means our now playing content is now
// invalid
mBrowseTree.mNowPlayingNode.setCached(false);
if (isActive()) {
BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mNowPlayingNode);
}
removeUnusedArtworkFromBrowseTree();
// For devices that support browsing, we *may* have an AvrcpPlayer with player
// metadata already. We could also be in the middle fetching it. If the player
// isn't there then we need to ensure that a default Addressed AvrcpPlayer is
// created to represent it. It can be updated if/when we do fetch the player.
if (!mAvailablePlayerList.contains(mAddressedPlayerId)) {
logD("Available player set does not contain the new Addressed Player");
AvrcpPlayer.Builder apb = new AvrcpPlayer.Builder();
apb.setDevice(mDevice);
apb.setPlayerId(mAddressedPlayerId);
apb.setSupportedFeature(AvrcpPlayer.FEATURE_PLAY);
apb.setSupportedFeature(AvrcpPlayer.FEATURE_PAUSE);
apb.setSupportedFeature(AvrcpPlayer.FEATURE_STOP);
apb.setSupportedFeature(AvrcpPlayer.FEATURE_FORWARD);
apb.setSupportedFeature(AvrcpPlayer.FEATURE_PREVIOUS);
mAvailablePlayerList.put(mAddressedPlayerId, apb.build());
}
// Set our new addressed player object from our set of available players that's
// guaranteed to have the addressed player now.
mAddressedPlayer = mAvailablePlayerList.get(mAddressedPlayerId);
// Fetch metadata including the now playing list if the new player supports the
// now playing feature
mService.getCurrentMetadataNative(Utils.getByteAddress(mDevice));
mService.getPlaybackStateNative(Utils.getByteAddress(mDevice));
if (mAddressedPlayer.supportsFeature(AvrcpPlayer.FEATURE_NOW_PLAYING)) {
sendMessage(MESSAGE_GET_FOLDER_ITEMS, mBrowseTree.mNowPlayingNode);
}
logD("AddressedPlayer = " + mAddressedPlayer);
return true;
case MESSAGE_PROCESS_SUPPORTED_APPLICATION_SETTINGS:
mAddressedPlayer.setSupportedPlayerApplicationSettings(
(PlayerApplicationSettings) msg.obj);
notifyChanged(mAddressedPlayer.getPlaybackState());
return true;
case MESSAGE_PROCESS_CURRENT_APPLICATION_SETTINGS:
mAddressedPlayer.setCurrentPlayerApplicationSettings(
(PlayerApplicationSettings) msg.obj);
notifyChanged(mAddressedPlayer.getPlaybackState());
return true;
case MESSAGE_PROCESS_AVAILABLE_PLAYER_CHANGED:
processAvailablePlayerChanged();
return true;
case MESSAGE_PROCESS_RECEIVED_COVER_ART_PSM:
mCoverArtPsm = msg.arg1;
connectCoverArt();
return true;
case MESSAGE_PROCESS_IMAGE_DOWNLOADED:
AvrcpCoverArtManager.DownloadEvent event =
(AvrcpCoverArtManager.DownloadEvent) msg.obj;
String uuid = event.getUuid();
Uri uri = event.getUri();
logD("Received image for " + uuid + " at " + uri.toString());
// Let the addressed player know we got an image so it can see if the current
// track now has cover artwork
boolean addedArtwork = mAddressedPlayer.notifyImageDownload(uuid, uri);
if (addedArtwork && isActive()) {
BluetoothMediaBrowserService.trackChanged(
mAddressedPlayer.getCurrentTrack());
}
// Let the browse tree know of the newly downloaded image so it can attach it to
// all the items that need it. Notify of changed nodes accordingly
Set<BrowseTree.BrowseNode> nodes = mBrowseTree.notifyImageDownload(uuid, uri);
for (BrowseTree.BrowseNode node : nodes) {
notifyChanged(node);
}
// Delete images that were downloaded and entirely unused
if (!addedArtwork && nodes.isEmpty()) {
removeUnusedArtwork(uuid);
removeUnusedArtworkFromBrowseTree();
}
return true;
case DISCONNECT:
transitionTo(mDisconnecting);
return true;
default:
return super.processMessage(msg);
}
}
private void processPlayItem(BrowseTree.BrowseNode node) {
if (node == null) {
Log.w(TAG, "Invalid item to play");
} else {
mService.playItemNative(
mDeviceAddress, node.getScope(),
node.getBluetoothID(), 0);
}
}
private synchronized void passThru(int cmd) {
logD("msgPassThru " + cmd);
// Some keys should be held until the next event.
if (mCurrentlyHeldKey != 0) {
mService.sendPassThroughCommandNative(
mDeviceAddress, mCurrentlyHeldKey,
AvrcpControllerService.KEY_STATE_RELEASED);
if (mCurrentlyHeldKey == cmd) {
// Return to prevent starting FF/FR operation again
mCurrentlyHeldKey = 0;
return;
} else {
// FF/FR is in progress and other operation is desired
// so after stopping FF/FR, not returning so that command
// can be sent for the desired operation.
mCurrentlyHeldKey = 0;
}
}
// Send the pass through.
mService.sendPassThroughCommandNative(mDeviceAddress, cmd,
AvrcpControllerService.KEY_STATE_PRESSED);
if (isHoldableKey(cmd)) {
// Release cmd next time a command is sent.
mCurrentlyHeldKey = cmd;
} else {
mService.sendPassThroughCommandNative(mDeviceAddress,
cmd, AvrcpControllerService.KEY_STATE_RELEASED);
}
}
private boolean isHoldableKey(int cmd) {
return (cmd == AvrcpControllerService.PASS_THRU_CMD_ID_REWIND)
|| (cmd == AvrcpControllerService.PASS_THRU_CMD_ID_FF);
}
private void setRepeat(int repeatMode) {
mService.setPlayerApplicationSettingValuesNative(mDeviceAddress, (byte) 1,
new byte[]{PlayerApplicationSettings.REPEAT_STATUS}, new byte[]{
PlayerApplicationSettings.mapAvrcpPlayerSettingstoBTattribVal(
PlayerApplicationSettings.REPEAT_STATUS, repeatMode)});
}
private void setShuffle(int shuffleMode) {
mService.setPlayerApplicationSettingValuesNative(mDeviceAddress, (byte) 1,
new byte[]{PlayerApplicationSettings.SHUFFLE_STATUS}, new byte[]{
PlayerApplicationSettings.mapAvrcpPlayerSettingstoBTattribVal(
PlayerApplicationSettings.SHUFFLE_STATUS, shuffleMode)});
}
private void processAvailablePlayerChanged() {
logD("processAvailablePlayerChanged");
mBrowseTree.mRootNode.setCached(false);
mBrowseTree.mRootNode.setExpectedChildren(255);
BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mRootNode);
removeUnusedArtworkFromBrowseTree();
requestContents(mBrowseTree.mRootNode);
}
}
// Handle the get folder listing action
// a) Fetch the listing of folders
// b) Once completed return the object listing
class GetFolderList extends State {
private static final String STATE_TAG = "Avrcp.GetFolderList";
boolean mAbort;
BrowseTree.BrowseNode mBrowseNode;
BrowseTree.BrowseNode mNextStep;
@Override
public void enter() {
logD(STATE_TAG + " Entering GetFolderList");
// Setup the timeouts.
sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
super.enter();
mAbort = false;
Message msg = getCurrentMessage();
if (msg.what == MESSAGE_GET_FOLDER_ITEMS) {
{
logD(STATE_TAG + " new Get Request");
mBrowseNode = (BrowseTree.BrowseNode) msg.obj;
}
}
if (mBrowseNode == null) {
transitionTo(mConnected);
} else {
navigateToFolderOrRetrieve(mBrowseNode);
}
}
@Override
public boolean processMessage(Message msg) {
logD(STATE_TAG + " processMessage " + msg.what);
switch (msg.what) {
case MESSAGE_PROCESS_GET_FOLDER_ITEMS:
ArrayList<AvrcpItem> folderList = (ArrayList<AvrcpItem>) msg.obj;
int endIndicator = mBrowseNode.getExpectedChildren() - 1;
logD("GetFolderItems: End " + endIndicator
+ " received " + folderList.size());
// Queue up image download if the item has an image and we don't have it yet
// Only do this if the feature is enabled.
for (AvrcpItem track : folderList) {
if (shouldDownloadBrowsedImages()) {
downloadImageIfNeeded(track);
} else {
track.setCoverArtUuid(null);
}
}
// Always update the node so that the user does not wait forever
// for the list to populate.
int newSize = mBrowseNode.addChildren(folderList);
logD("Added " + newSize + " items to the browse tree");
notifyChanged(mBrowseNode);
if (mBrowseNode.getChildrenCount() >= endIndicator || folderList.size() == 0
|| mAbort) {
// If we have fetched all the elements or if the remotes sends us 0 elements
// (which can lead us into a loop since mCurrInd does not proceed) we simply
// abort.
mBrowseNode.setCached(true);
transitionTo(mConnected);
} else {
// Fetch the next set of items.
fetchContents(mBrowseNode);
// Reset the timeout message since we are doing a new fetch now.
removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT);
sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
}
break;
case MESSAGE_PROCESS_SET_BROWSED_PLAYER:
mBrowseTree.setCurrentBrowsedPlayer(mNextStep.getID(), msg.arg1, msg.arg2);
removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT);
sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
navigateToFolderOrRetrieve(mBrowseNode);
break;
case MESSAGE_PROCESS_FOLDER_PATH:
mBrowseTree.setCurrentBrowsedFolder(mNextStep.getID());
mBrowseTree.getCurrentBrowsedFolder().setExpectedChildren(msg.arg1);
// AVRCP Specification says, if we're not database aware, we must disconnect and
// reconnect our BIP client each time we successfully change path
refreshCoverArt();
if (mAbort) {
transitionTo(mConnected);
} else {
removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT);
sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
navigateToFolderOrRetrieve(mBrowseNode);
}
break;
case MESSAGE_PROCESS_GET_PLAYER_ITEMS:
logD("Received new available player items");
BrowseTree.BrowseNode rootNode = mBrowseTree.mRootNode;
// The specification is not firm on what receiving available player changes
// means relative to the existing player IDs, the addressed player and any
// currently saved play status, track or now playing list metadata. We're going
// to assume nothing and act verbosely, as some devices are known to reuse
// Player IDs.
if (!rootNode.isCached()) {
List<AvrcpPlayer> playerList = (List<AvrcpPlayer>) msg.obj;
// Since players hold metadata, including cover art handles that point to
// stored images, be sure to save image UUIDs so we can see if we can
// remove them from storage after setting our new player object
ArrayList<String> coverArtUuids = new ArrayList<String>();
for (int i = 0; i < mAvailablePlayerList.size(); i++) {
AvrcpPlayer player = mAvailablePlayerList.valueAt(i);
AvrcpItem track = player.getCurrentTrack();
if (track != null && track.getCoverArtUuid() != null) {
coverArtUuids.add(track.getCoverArtUuid());
}
}
mAvailablePlayerList.clear();
for (AvrcpPlayer player : playerList) {
mAvailablePlayerList.put(player.getId(), player);
}
// If our new set of players contains our addressed player again then we
// will replace it and re-download metadata. If not, we'll re-use the old
// player to save the metadata queries.
if (!mAvailablePlayerList.contains(mAddressedPlayerId)) {
logD("Available player set doesn't contain the addressed player");
mAvailablePlayerList.put(mAddressedPlayerId, mAddressedPlayer);
} else {
logD("Update addressed player with new available player metadata");
mAddressedPlayer = mAvailablePlayerList.get(mAddressedPlayerId);
mService.getCurrentMetadataNative(Utils.getByteAddress(mDevice));
mService.getPlaybackStateNative(Utils.getByteAddress(mDevice));
mBrowseTree.mNowPlayingNode.setCached(false);
if (mAddressedPlayer.supportsFeature(AvrcpPlayer.FEATURE_NOW_PLAYING)) {
sendMessage(MESSAGE_GET_FOLDER_ITEMS, mBrowseTree.mNowPlayingNode);
}
}
logD("AddressedPlayer = " + mAddressedPlayer);
// Check old cover art UUIDs for deletion
for (String uuid : coverArtUuids) {
removeUnusedArtwork(uuid);
}
// Make sure our browse tree matches our received Available Player set only
rootNode.addChildren(playerList);
mBrowseTree.setCurrentBrowsedFolder(BrowseTree.ROOT);
rootNode.setExpectedChildren(playerList.size());
rootNode.setCached(true);
notifyChanged(rootNode);
}
transitionTo(mConnected);
break;
case MESSAGE_INTERNAL_CMD_TIMEOUT:
// We have timed out to execute the request, we should simply send
// whatever listing we have gotten until now.
Log.w(TAG, "TIMEOUT");
transitionTo(mConnected);
break;
case MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE:
// If we have gotten an error for OUT OF RANGE we have
// already sent all the items to the client hence simply
// transition to Connected state here.
mBrowseNode.setCached(true);
transitionTo(mConnected);
break;
case MESSAGE_GET_FOLDER_ITEMS:
if (!mBrowseNode.equals(msg.obj)) {
if (shouldAbort(mBrowseNode.getScope(),
((BrowseTree.BrowseNode) msg.obj).getScope())) {
mAbort = true;
}
deferMessage(msg);
logD("GetFolderItems: Go Get Another Directory");
} else {
logD("GetFolderItems: Get The Same Directory, ignore");
}
break;
default:
// All of these messages should be handled by parent state immediately.
return false;
}
return true;
}
/**
* shouldAbort calculates the cases where fetching the current directory is no longer
* necessary.
*
* @return true: a new folder in the same scope
* a new player while fetching contents of a folder
* false: other cases, specifically Now Playing while fetching a folder
*/
private boolean shouldAbort(int currentScope, int fetchScope) {
if ((currentScope == fetchScope)
|| (currentScope == AvrcpControllerService.BROWSE_SCOPE_VFS
&& fetchScope == AvrcpControllerService.BROWSE_SCOPE_PLAYER_LIST)) {
return true;
}
return false;
}
private void fetchContents(BrowseTree.BrowseNode target) {
int start = target.getChildrenCount();
int end = Math.min(target.getExpectedChildren(), target.getChildrenCount()
+ ITEM_PAGE_SIZE) - 1;
logD("fetchContents(title=" + target.getID() + ", scope=" + target.getScope()
+ ", start=" + start + ", end=" + end + ", expected="
+ target.getExpectedChildren() + ")");
switch (target.getScope()) {
case AvrcpControllerService.BROWSE_SCOPE_PLAYER_LIST:
mService.getPlayerListNative(mDeviceAddress,
start, end);
break;
case AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING:
mService.getNowPlayingListNative(
mDeviceAddress, start, end);
break;
case AvrcpControllerService.BROWSE_SCOPE_VFS:
mService.getFolderListNative(mDeviceAddress,
start, end);
break;
default:
Log.e(TAG, STATE_TAG + " Scope " + target.getScope()
+ " cannot be handled here.");
}
}
/* One of several things can happen when trying to get a folder list
*
*
* 0: The folder handle is no longer valid
* 1: The folder contents can be retrieved directly (NowPlaying, Root, Current)
* 2: The folder is a browsable player
* 3: The folder is a non browsable player
* 4: The folder is not a child of the current folder
* 5: The folder is a child of the current folder
*
*/
private void navigateToFolderOrRetrieve(BrowseTree.BrowseNode target) {
mNextStep = mBrowseTree.getNextStepToFolder(target);
logD("NAVIGATING From "
+ mBrowseTree.getCurrentBrowsedFolder().toString());
logD("NAVIGATING Toward " + target.toString());
if (mNextStep == null) {
return;
} else if (target.equals(mBrowseTree.mNowPlayingNode)
|| target.equals(mBrowseTree.mRootNode)
|| mNextStep.equals(mBrowseTree.getCurrentBrowsedFolder())) {
fetchContents(mNextStep);
} else if (mNextStep.isPlayer()) {
logD("NAVIGATING Player " + mNextStep.toString());
if (mNextStep.isBrowsable()) {
mService.setBrowsedPlayerNative(
mDeviceAddress, (int) mNextStep.getBluetoothID());
} else {
logD("Player doesn't support browsing");
mNextStep.setCached(true);
transitionTo(mConnected);
}
} else if (mNextStep.equals(mBrowseTree.mNavigateUpNode)) {
logD("NAVIGATING UP " + mNextStep.toString());
mNextStep = mBrowseTree.getCurrentBrowsedFolder().getParent();
mBrowseTree.getCurrentBrowsedFolder().setCached(false);
removeUnusedArtworkFromBrowseTree();
mService.changeFolderPathNative(
mDeviceAddress,
AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP,
0);
} else {
logD("NAVIGATING DOWN " + mNextStep.toString());
mService.changeFolderPathNative(
mDeviceAddress,
AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_DOWN,
mNextStep.getBluetoothID());
}
}
@Override
public void exit() {
removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT);
mBrowseNode = null;
super.exit();
}
}
protected class Disconnecting extends State {
@Override
public void enter() {
disconnectCoverArt();
onBrowsingDisconnected();
if (mService.sBrowseTree != null) {
mService.sBrowseTree.mRootNode.removeChild(mBrowseTree.mRootNode);
BluetoothMediaBrowserService.notifyChanged(mService.sBrowseTree.mRootNode);
}
broadcastConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTING);
transitionTo(mDisconnected);
}
}
/**
* Handle a request to align our local volume with the volume of a remote device. If
* we're assuming the source volume is fixed then a response of ABS_VOL_MAX will always be
* sent and no volume adjustment action will be taken on the sink side.
*
* @param absVol A volume level based on a domain of [0, ABS_VOL_MAX]
* @param label Volume notification label
*/
private void handleAbsVolumeRequest(int absVol, int label) {
logD("handleAbsVolumeRequest: absVol = " + absVol + ", label = " + label);
if (mIsVolumeFixed) {
logD("Source volume is assumed to be fixed, responding with max volume");
absVol = ABS_VOL_BASE;
} else {
mVolumeChangedNotificationsToIgnore++;
removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT);
sendMessageDelayed(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT,
ABS_VOL_TIMEOUT_MILLIS);
setAbsVolume(absVol);
}
mService.sendAbsVolRspNative(mDeviceAddress, absVol, label);
}
/**
* Align our volume with a requested absolute volume level
*
* @param absVol A volume level based on a domain of [0, ABS_VOL_MAX]
*/
private void setAbsVolume(int absVol) {
int maxLocalVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
int curLocalVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
int reqLocalVolume = (maxLocalVolume * absVol) / ABS_VOL_BASE;
logD("setAbsVolme: absVol = " + absVol + ", reqLocal = " + reqLocalVolume
+ ", curLocal = " + curLocalVolume + ", maxLocal = " + maxLocalVolume);
/*
* In some cases change in percentage is not sufficient enough to warrant
* change in index values which are in range of 0-15. For such cases
* no action is required
*/
if (reqLocalVolume != curLocalVolume) {
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, reqLocalVolume,
AudioManager.FLAG_SHOW_UI);
}
}
private int getAbsVolume() {
if (mIsVolumeFixed) {
return ABS_VOL_BASE;
}
int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
int currIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
int newIndex = (currIndex * ABS_VOL_BASE) / maxVolume;
return newIndex;
}
private boolean shouldDownloadBrowsedImages() {
return mService.getResources()
.getBoolean(R.bool.avrcp_controller_cover_art_browsed_images);
}
private void downloadImageIfNeeded(AvrcpItem track) {
if (mCoverArtManager == null) return;
String uuid = track.getCoverArtUuid();
Uri imageUri = null;
if (uuid != null) {
imageUri = mCoverArtManager.getImageUri(mDevice, uuid);
if (imageUri != null) {
track.setCoverArtLocation(imageUri);
} else {
mCoverArtManager.downloadImage(mDevice, uuid);
}
}
}
MediaSessionCompat.Callback mSessionCallbacks = new MediaSessionCompat.Callback() {
@Override
public void onPlay() {
logD("onPlay");
onPrepare();
sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PLAY);
}
@Override
public void onPause() {
logD("onPause");
sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE);
}
@Override
public void onSkipToNext() {
logD("onSkipToNext");
onPrepare();
sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_FORWARD);
}
@Override
public void onSkipToPrevious() {
logD("onSkipToPrevious");
onPrepare();
sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_BACKWARD);
}
@Override
public void onSkipToQueueItem(long id) {
logD("onSkipToQueueItem id=" + id);
onPrepare();
BrowseTree.BrowseNode node = mBrowseTree.getTrackFromNowPlayingList((int) id);
if (node != null) {
sendMessage(MESSAGE_PLAY_ITEM, node);
}
}
@Override
public void onStop() {
logD("onStop");
sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_STOP);
}
@Override
public void onPrepare() {
logD("onPrepare");
A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();
if (a2dpSinkService != null) {
a2dpSinkService.requestAudioFocus(mDevice, true);
}
}
@Override
public void onRewind() {
logD("onRewind");
sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_REWIND);
}
@Override
public void onFastForward() {
logD("onFastForward");
sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_FF);
}
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
logD("onPlayFromMediaId");
// Play the item if possible.
onPrepare();
BrowseTree.BrowseNode node = mBrowseTree.findBrowseNodeByID(mediaId);
if (node != null) {
// node was found on this bluetooth device
sendMessage(MESSAGE_PLAY_ITEM, node);
} else {
// node was not found on this device, pause here, and play on another device
sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE);
mService.playItem(mediaId);
}
}
@Override
public void onSetRepeatMode(int repeatMode) {
logD("onSetRepeatMode");
sendMessage(MSG_AVRCP_SET_REPEAT, repeatMode);
}
@Override
public void onSetShuffleMode(int shuffleMode) {
logD("onSetShuffleMode");
sendMessage(MSG_AVRCP_SET_SHUFFLE, shuffleMode);
}
};
protected void broadcastConnectionStateChanged(int currentState) {
if (mMostRecentState == currentState) {
return;
}
if (currentState == BluetoothProfile.STATE_CONNECTED) {
MetricsLogger.logProfileConnectionEvent(
BluetoothMetricsProto.ProfileId.AVRCP_CONTROLLER);
}
logD("Connection state " + mDevice + ": " + mMostRecentState + "->" + currentState);
Intent intent = new Intent(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, mMostRecentState);
intent.putExtra(BluetoothProfile.EXTRA_STATE, currentState);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
mMostRecentState = currentState;
mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
}
private boolean shouldRequestFocus() {
return mService.getResources()
.getBoolean(R.bool.a2dp_sink_automatically_request_audio_focus);
}
}