blob: e3abc4d8f30f41ebb4947665ed0712d7f1a5b7c7 [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.media.MediaMetadata;
import android.media.browse.MediaBrowser.MediaItem;
import android.os.Bundle;
import android.os.Message;
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.internal.util.State;
import com.android.internal.util.StateMachine;
import java.util.ArrayList;
import java.util.List;
/**
* 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;
//100->199 Internal Events
protected static final int CLEANUP = 100;
private static final int CONNECT_TIMEOUT = 101;
//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;
//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;
static final int MESSAGE_INTERNAL_ABS_VOL_TIMEOUT = 404;
/*
* 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 final AudioManager mAudioManager;
private final boolean mIsVolumeFixed;
protected final BluetoothDevice mDevice;
protected final byte[] mDeviceAddress;
protected final AvrcpControllerService mService;
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 = new AvrcpPlayer();
private int mAddressedPlayerId = -1;
private SparseArray<AvrcpPlayer> mAvailablePlayerList = new SparseArray<AvrcpPlayer>();
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;
logD(device.toString());
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 synchronized 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);
}
/**
* 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());
}
@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() {
if (mBrowsingConnected) return;
mService.sBrowseTree.mRootNode.addChild(mBrowseTree.mRootNode);
BluetoothMediaBrowserService.notifyChanged(mService
.sBrowseTree.mRootNode);
mBrowsingConnected = true;
}
synchronized void onBrowsingDisconnected() {
if (!mBrowsingConnected) return;
mAddressedPlayer.setPlayStatus(PlaybackStateCompat.STATE_ERROR);
mAddressedPlayer.updateCurrentTrack(null);
mBrowseTree.mNowPlayingNode.setCached(false);
BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mNowPlayingNode);
mService.sBrowseTree.mRootNode.removeChild(
mBrowseTree.mRootNode);
BluetoothMediaBrowserService.notifyChanged(mService
.sBrowseTree.mRootNode);
mBrowsingConnected = false;
}
private void notifyChanged(BrowseTree.BrowseNode node) {
BluetoothMediaBrowserService.notifyChanged(node);
}
void requestContents(BrowseTree.BrowseNode node) {
sendMessage(MESSAGE_GET_FOLDER_ITEMS, node);
logD("Fetching " + node);
}
void nowPlayingContentChanged() {
mBrowseTree.mNowPlayingNode.setCached(false);
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 CONNECT:
logD("Connect");
transitionTo(mConnecting);
break;
case CLEANUP:
mService.removeStateMachine(AvrcpControllerStateMachine.this);
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) {
BluetoothMediaBrowserService.notifyChanged(mAddressedPlayer.getPlaybackState());
broadcastConnectionStateChanged(BluetoothProfile.STATE_CONNECTED);
BluetoothMediaBrowserService.addressedPlayerChanged(mSessionCallbacks);
} else {
logD("ReEnteringConnected");
}
super.enter();
}
@Override
public boolean processMessage(Message msg) {
logD(STATE_TAG + " processMessage " + msg.what);
switch (msg.what) {
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
playItem((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:
mAddressedPlayer.updateCurrentTrack((MediaMetadata) msg.obj);
BluetoothMediaBrowserService.trackChanged((MediaMetadata) msg.obj);
return true;
case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
mAddressedPlayer.setPlayStatus(msg.arg1);
BluetoothMediaBrowserService.notifyChanged(mAddressedPlayer.getPlaybackState());
if (mAddressedPlayer.getPlaybackState().getState()
== PlaybackStateCompat.STATE_PLAYING
&& A2dpSinkService.getFocusState() == 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);
BluetoothMediaBrowserService.notifyChanged(
mAddressedPlayer.getPlaybackState());
}
return true;
case MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED:
mAddressedPlayerId = msg.arg1;
logD("AddressedPlayer = " + mAddressedPlayerId);
AvrcpPlayer updatedPlayer = mAvailablePlayerList.get(mAddressedPlayerId);
if (updatedPlayer != null) {
mAddressedPlayer = updatedPlayer;
logD("AddressedPlayer = " + mAddressedPlayer.getName());
} else {
mBrowseTree.mRootNode.setCached(false);
mBrowseTree.mRootNode.setExpectedChildren(255);
BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mRootNode);
}
return true;
case MESSAGE_PROCESS_SUPPORTED_APPLICATION_SETTINGS:
mAddressedPlayer.setSupportedPlayerApplicationSettings(
(PlayerApplicationSettings) msg.obj);
BluetoothMediaBrowserService.notifyChanged(mAddressedPlayer.getPlaybackState());
return true;
case MESSAGE_PROCESS_CURRENT_APPLICATION_SETTINGS:
mAddressedPlayer.setCurrentPlayerApplicationSettings(
(PlayerApplicationSettings) msg.obj);
BluetoothMediaBrowserService.notifyChanged(mAddressedPlayer.getPlaybackState());
return true;
case DISCONNECT:
transitionTo(mDisconnecting);
return true;
default:
return super.processMessage(msg);
}
}
private void playItem(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)});
}
}
// 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<MediaItem> folderList = (ArrayList<MediaItem>) msg.obj;
int endIndicator = mBrowseNode.getExpectedChildren() - 1;
logD("GetFolderItems: End " + endIndicator
+ " received " + folderList.size());
// Always update the node so that the user does not wait forever
// for the list to populate.
mBrowseNode.addChildren(folderList);
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);
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:
BrowseTree.BrowseNode rootNode = mBrowseTree.mRootNode;
if (!rootNode.isCached()) {
List<AvrcpPlayer> playerList = (List<AvrcpPlayer>) msg.obj;
mAvailablePlayerList.clear();
for (AvrcpPlayer player : playerList) {
mAvailablePlayerList.put(player.getId(), player);
}
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;
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);
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() {
onBrowsingDisconnected();
BluetoothMediaBrowserService.trackChanged(null);
BluetoothMediaBrowserService.addressedPlayerChanged(null);
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;
}
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);
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);
sendMessage(MESSAGE_PLAY_ITEM, node);
}
@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)
|| !mAudioManager.isMusicActive();
}
}