blob: f13f0c43cf2960ec746cbdccc16a002383a0e6c0 [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.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.media.MediaMetadata;
import android.media.browse.MediaBrowser.MediaItem;
import android.media.session.PlaybackState;
import android.os.Message;
import android.util.Log;
import android.util.SparseArray;
import com.android.bluetooth.BluetoothMetricsProto;
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 {
// commands from Binder service
static final int MESSAGE_SEND_PASS_THROUGH_CMD = 1;
static final int MESSAGE_SEND_GROUP_NAVIGATION_CMD = 3;
static final int MESSAGE_GET_FOLDER_LIST = 6;
static final int MESSAGE_FETCH_ATTR_AND_PLAY_ITEM = 9;
// commands from native layer
static final int MESSAGE_PROCESS_SET_ABS_VOL_CMD = 103;
static final int MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION = 104;
static final int MESSAGE_PROCESS_TRACK_CHANGED = 105;
static final int MESSAGE_PROCESS_PLAY_POS_CHANGED = 106;
static final int MESSAGE_PROCESS_PLAY_STATUS_CHANGED = 107;
static final int MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION = 108;
static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS = 109;
static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE = 110;
static final int MESSAGE_PROCESS_GET_PLAYER_ITEMS = 111;
static final int MESSAGE_PROCESS_FOLDER_PATH = 112;
static final int MESSAGE_PROCESS_SET_BROWSED_PLAYER = 113;
static final int MESSAGE_PROCESS_SET_ADDRESSED_PLAYER = 114;
static final int MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED = 115;
static final int MESSAGE_PROCESS_NOW_PLAYING_CONTENTS_CHANGED = 116;
// commands for connection
static final int MESSAGE_PROCESS_RC_FEATURES = 301;
static final int MESSAGE_PROCESS_CONNECTION_CHANGE = 302;
static final int MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE = 303;
// Interal messages
static final int MESSAGE_INTERNAL_CMD_TIMEOUT = 403;
static final int MESSAGE_INTERNAL_ABS_VOL_TIMEOUT = 404;
static final int ABS_VOL_TIMEOUT_MILLIS = 1000; //1s
static final int CMD_TIMEOUT_MILLIS = 5000; // 5s
// Fetch only 20 items at a time.
static final int GET_FOLDER_ITEMS_PAGINATION_SIZE = 20;
// Fetch no more than 1000 items per directory.
static final int MAX_FOLDER_ITEMS = 1000;
/*
* 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 final String TAG = "AvrcpControllerSM";
private static final boolean DBG = true;
private static final boolean VDBG = false;
private final Context mContext;
private final AudioManager mAudioManager;
private final State mDisconnected;
private final State mConnected;
private final SetAddresedPlayerAndPlayItem mSetAddrPlayer;
private final GetFolderList mGetFolderList;
private final Object mLock = new Object();
private static final MediaMetadata EMPTY_MEDIA_METADATA = new MediaMetadata.Builder().build();
// APIs exist to access these so they must be thread safe
private Boolean mIsConnected = false;
private RemoteDevice mRemoteDevice;
private AvrcpPlayer mAddressedPlayer;
// Only accessed from State Machine processMessage
private int mVolumeChangedNotificationsToIgnore = 0;
private int mPreviousPercentageVol = -1;
private int mAddressedPlayerID = -1;
private SparseArray<AvrcpPlayer> mAvailablePlayerList = new SparseArray<AvrcpPlayer>();
// Browse tree.
private BrowseTree mBrowseTree = new BrowseTree();
AvrcpControllerStateMachine(Context context) {
super(TAG);
mContext = context;
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
mContext.registerReceiver(mBroadcastReceiver, filter);
mDisconnected = new Disconnected();
mConnected = new Connected();
// Used to change folder path and fetch the new folder listing.
mSetAddrPlayer = new SetAddresedPlayerAndPlayItem();
mGetFolderList = new GetFolderList();
addState(mDisconnected);
addState(mConnected);
// Any action that needs blocking other requests to the state machine will be implemented as
// a separate substate of the mConnected state. Once transtition to the sub-state we should
// only handle the messages that are relevant to the sub-action. Everything else should be
// deferred so that once we transition to the mConnected we can process them hence.
addState(mSetAddrPlayer, mConnected);
addState(mGetFolderList, mConnected);
setInitialState(mDisconnected);
}
class Disconnected extends State {
@Override
public boolean processMessage(Message msg) {
if (DBG) Log.d(TAG, " HandleMessage: " + dumpMessageString(msg.what));
switch (msg.what) {
case MESSAGE_PROCESS_CONNECTION_CHANGE:
if (msg.arg1 == BluetoothProfile.STATE_CONNECTED) {
mBrowseTree = new BrowseTree();
transitionTo(mConnected);
BluetoothDevice rtDevice = (BluetoothDevice) msg.obj;
synchronized (mLock) {
mRemoteDevice = new RemoteDevice(rtDevice);
mAddressedPlayer = new AvrcpPlayer();
mIsConnected = true;
}
MetricsLogger.logProfileConnectionEvent(
BluetoothMetricsProto.ProfileId.AVRCP_CONTROLLER);
Intent intent = new Intent(
BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
BluetoothProfile.STATE_DISCONNECTED);
intent.putExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_CONNECTED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, rtDevice);
mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
}
break;
default:
Log.w(TAG,
"Currently Disconnected not handling " + dumpMessageString(msg.what));
return false;
}
return true;
}
}
class Connected extends State {
@Override
public boolean processMessage(Message msg) {
if (DBG) Log.d(TAG, " HandleMessage: " + dumpMessageString(msg.what));
A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();
synchronized (mLock) {
switch (msg.what) {
case MESSAGE_SEND_PASS_THROUGH_CMD:
BluetoothDevice device = (BluetoothDevice) msg.obj;
AvrcpControllerService.sendPassThroughCommandNative(
Utils.getByteAddress(device), msg.arg1, msg.arg2);
if (a2dpSinkService != null) {
if (DBG) Log.d(TAG, " inform AVRCP Commands to A2DP Sink ");
a2dpSinkService.informAvrcpPassThroughCmd(device, msg.arg1, msg.arg2);
}
break;
case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
AvrcpControllerService.sendGroupNavigationCommandNative(
mRemoteDevice.getBluetoothAddress(), msg.arg1, msg.arg2);
break;
case MESSAGE_GET_FOLDER_LIST:
// Whenever we transition we set the information for folder we need to
// return result.
if (DBG) Log.d(TAG, "Message_GET_FOLDER_LIST" + (String) msg.obj);
mGetFolderList.setFolder((String) msg.obj);
transitionTo(mGetFolderList);
break;
case MESSAGE_FETCH_ATTR_AND_PLAY_ITEM: {
int scope = msg.arg1;
String playItemUid = (String) msg.obj;
BrowseTree.BrowseNode currBrPlayer = mBrowseTree.getCurrentBrowsedPlayer();
BrowseTree.BrowseNode currAddrPlayer =
mBrowseTree.getCurrentAddressedPlayer();
BrowseTree.BrowseNode itemToPlay =
mBrowseTree.findBrowseNodeByID(playItemUid);
if (DBG) {
Log.d(TAG, "currBrPlayer " + currBrPlayer + " currAddrPlayer "
+ currAddrPlayer);
}
if (currBrPlayer == null
|| currBrPlayer.equals(currAddrPlayer)
|| scope == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
// String is encoded as a Hex String (mostly for display purposes)
// hence convert this back to real byte string.
// NOTE: It may be possible that sending play while the same item is
// playing leads to reset of track.
AvrcpControllerService.playItemNative(
mRemoteDevice.getBluetoothAddress(), (byte) scope,
AvrcpControllerService.hexStringToByteUID(playItemUid), 0);
} else {
// Send out the request for setting addressed player.
AvrcpControllerService.setAddressedPlayerNative(
mRemoteDevice.getBluetoothAddress(),
currBrPlayer.getPlayerID());
mSetAddrPlayer.setItemAndScope(currBrPlayer.getID(), playItemUid,
scope);
transitionTo(mSetAddrPlayer);
}
break;
}
case MESSAGE_PROCESS_CONNECTION_CHANGE:
if (msg.arg1 == BluetoothProfile.STATE_DISCONNECTED) {
synchronized (mLock) {
mIsConnected = false;
mRemoteDevice = null;
}
mBrowseTree.clear();
transitionTo(mDisconnected);
BluetoothDevice rtDevice = (BluetoothDevice) msg.obj;
Intent intent = new Intent(
BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
BluetoothProfile.STATE_CONNECTED);
intent.putExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, rtDevice);
mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
}
break;
case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
// Service tells us if the browse is connected or disconnected.
// This is useful only for deciding whether to send browse commands rest of
// the connection state handling should be done via the message
// MESSAGE_PROCESS_CONNECTION_CHANGE.
Intent intent = new Intent(
AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, (BluetoothDevice) msg.obj);
if (DBG) {
Log.d(TAG, "Browse connection state " + msg.arg1);
}
if (msg.arg1 == 1) {
intent.putExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_CONNECTED);
} else if (msg.arg1 == 0) {
intent.putExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
} else {
Log.w(TAG, "Incorrect browse state " + msg.arg1);
}
mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
break;
case MESSAGE_PROCESS_RC_FEATURES:
mRemoteDevice.setRemoteFeatures(msg.arg1);
break;
case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
mVolumeChangedNotificationsToIgnore++;
removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT);
sendMessageDelayed(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT,
ABS_VOL_TIMEOUT_MILLIS);
setAbsVolume(msg.arg1, msg.arg2);
break;
case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION: {
mRemoteDevice.setNotificationLabel(msg.arg1);
mRemoteDevice.setAbsVolNotificationRequested(true);
int percentageVol = getVolumePercentage();
if (DBG) {
Log.d(TAG, " Sending Interim Response = " + percentageVol + " label "
+ msg.arg1);
}
AvrcpControllerService.sendRegisterAbsVolRspNative(
mRemoteDevice.getBluetoothAddress(), NOTIFICATION_RSP_TYPE_INTERIM,
percentageVol, mRemoteDevice.getNotificationLabel());
}
break;
case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION: {
if (mVolumeChangedNotificationsToIgnore > 0) {
mVolumeChangedNotificationsToIgnore--;
if (mVolumeChangedNotificationsToIgnore == 0) {
removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT);
}
} else {
if (mRemoteDevice.getAbsVolNotificationRequested()) {
int percentageVol = getVolumePercentage();
if (percentageVol != mPreviousPercentageVol) {
AvrcpControllerService.sendRegisterAbsVolRspNative(
mRemoteDevice.getBluetoothAddress(),
NOTIFICATION_RSP_TYPE_CHANGED, percentageVol,
mRemoteDevice.getNotificationLabel());
mPreviousPercentageVol = percentageVol;
mRemoteDevice.setAbsVolNotificationRequested(false);
}
}
}
}
break;
case MESSAGE_INTERNAL_ABS_VOL_TIMEOUT:
// Volume changed notifications should come back promptly from the
// AudioManager, if for some reason some notifications were squashed don't
// prevent future notifications.
if (DBG) Log.d(TAG, "Timed out on volume changed notification");
mVolumeChangedNotificationsToIgnore = 0;
break;
case MESSAGE_PROCESS_TRACK_CHANGED:
// Music start playing automatically and update Metadata
boolean updateTrack =
mAddressedPlayer.updateCurrentTrack((TrackInfo) msg.obj);
if (updateTrack) {
broadcastMetaDataChanged(
mAddressedPlayer.getCurrentTrack().getMediaMetaData());
}
break;
case MESSAGE_PROCESS_PLAY_POS_CHANGED:
if (msg.arg2 != -1) {
mAddressedPlayer.setPlayTime(msg.arg2);
broadcastPlayBackStateChanged(getCurrentPlayBackState());
}
break;
case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
int status = msg.arg1;
mAddressedPlayer.setPlayStatus(status);
if (status == PlaybackState.STATE_PLAYING) {
a2dpSinkService.informTGStatePlaying(mRemoteDevice.mBTDevice, true);
} else if (status == PlaybackState.STATE_PAUSED
|| status == PlaybackState.STATE_STOPPED) {
a2dpSinkService.informTGStatePlaying(mRemoteDevice.mBTDevice, false);
}
break;
case MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED:
mAddressedPlayerID = msg.arg1;
if (DBG) Log.d(TAG, "AddressedPlayer = " + mAddressedPlayerID);
AvrcpPlayer updatedPlayer = mAvailablePlayerList.get(mAddressedPlayerID);
if (updatedPlayer != null) {
mAddressedPlayer = updatedPlayer;
if (DBG) Log.d(TAG, "AddressedPlayer = " + mAddressedPlayer.getName());
} else {
mBrowseTree.mRootNode.setCached(false);
}
sendMessage(MESSAGE_PROCESS_SET_ADDRESSED_PLAYER);
break;
case MESSAGE_PROCESS_NOW_PLAYING_CONTENTS_CHANGED:
mBrowseTree.mNowPlayingNode.setCached(false);
mGetFolderList.setFolder(mBrowseTree.mNowPlayingNode.getID());
transitionTo(mGetFolderList);
break;
default:
Log.d(TAG, "Unhandled message" + msg.what);
return false;
}
}
return true;
}
}
// Handle the get folder listing action
// a) Fetch the listing of folders
// b) Once completed return the object listing
class GetFolderList extends CmdState {
private static final String STATE_TAG = "AVRCPSM.GetFolderList";
boolean mAbort;
BrowseTree.BrowseNode mBrowseNode;
BrowseTree.BrowseNode mNextStep;
@Override
public void enter() {
// Setup the timeouts.
super.enter();
mAbort = false;
if (mBrowseNode == null) {
transitionTo(mConnected);
} else {
navigateToFolderOrRetrieve(mBrowseNode);
}
}
public void setFolder(String id) {
if (DBG) Log.d(STATE_TAG, "Setting folder to " + id);
mBrowseNode = mBrowseTree.findBrowseNodeByID(id);
}
@Override
public boolean processMessage(Message msg) {
Log.d(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;
if (DBG) {
Log.d(STATE_TAG,
" 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);
broadcastFolderList(mBrowseNode.getID());
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);
broadcastFolderList(BrowseTree.ROOT);
}
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.
broadcastFolderList(mBrowseNode.getID());
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);
broadcastFolderList(mBrowseNode.getID());
transitionTo(mConnected);
break;
case MESSAGE_GET_FOLDER_LIST:
if (!mBrowseNode.equals((String) msg.obj)) {
mAbort = true;
deferMessage(msg);
Log.d(STATE_TAG, "Go Get Another Directory");
} else {
Log.d(STATE_TAG, "Get The Same Directory, ignore");
}
break;
case MESSAGE_FETCH_ATTR_AND_PLAY_ITEM:
// A new request has come in, no need to fetch more.
mAbort = true;
deferMessage(msg);
break;
case MESSAGE_SEND_PASS_THROUGH_CMD:
case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
case MESSAGE_PROCESS_TRACK_CHANGED:
case MESSAGE_PROCESS_PLAY_POS_CHANGED:
case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
case MESSAGE_PROCESS_CONNECTION_CHANGE:
case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
// All of these messages should be handled by parent state immediately.
return false;
default:
if (DBG) Log.d(STATE_TAG, "deferring message " + msg.what + " to connected!");
deferMessage(msg);
}
return true;
}
private void fetchContents(BrowseTree.BrowseNode target) {
int start = target.getChildrenCount();
int end = Math.min(target.getExpectedChildren(), target.getChildrenCount()
+ GET_FOLDER_ITEMS_PAGINATION_SIZE) - 1;
switch (target.getScope()) {
case AvrcpControllerService.BROWSE_SCOPE_PLAYER_LIST:
AvrcpControllerService.getPlayerListNative(mRemoteDevice.getBluetoothAddress(),
start, end);
break;
case AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING:
AvrcpControllerService.getNowPlayingListNative(
mRemoteDevice.getBluetoothAddress(), start, end);
break;
case AvrcpControllerService.BROWSE_SCOPE_VFS:
AvrcpControllerService.getFolderListNative(mRemoteDevice.getBluetoothAddress(),
start, end);
break;
default:
Log.e(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);
if (DBG) {
Log.d(TAG, "NAVIGATING From " + mBrowseTree.getCurrentBrowsedFolder().toString());
Log.d(TAG, "NAVIGATING Toward " + target.toString());
}
if (mNextStep == null) {
sendMessage(MESSAGE_INTERNAL_CMD_TIMEOUT);
} else if (target.equals(mBrowseTree.mNowPlayingNode)
|| target.equals(mBrowseTree.mRootNode)
|| mNextStep.equals(mBrowseTree.getCurrentBrowsedFolder())) {
fetchContents(mNextStep);
} else if (mNextStep.isPlayer()) {
if (DBG) Log.d(TAG, "NAVIGATING Player " + mNextStep.toString());
if (mNextStep.isBrowsable()) {
AvrcpControllerService.setBrowsedPlayerNative(
mRemoteDevice.getBluetoothAddress(), mNextStep.getPlayerID());
} else {
if (DBG) Log.d(TAG, "Player doesn't support browsing");
mNextStep.setCached(true);
broadcastFolderList(mNextStep.getID());
transitionTo(mConnected);
}
} else if (mNextStep.equals(mBrowseTree.mNavigateUpNode)) {
if (DBG) Log.d(TAG, "NAVIGATING UP " + mNextStep.toString());
mNextStep = mBrowseTree.getCurrentBrowsedFolder().getParent();
mBrowseTree.getCurrentBrowsedFolder().setCached(false);
AvrcpControllerService.changeFolderPathNative(
mRemoteDevice.getBluetoothAddress(),
AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP,
AvrcpControllerService.hexStringToByteUID(null));
} else {
if (DBG) Log.d(TAG, "NAVIGATING DOWN " + mNextStep.toString());
AvrcpControllerService.changeFolderPathNative(
mRemoteDevice.getBluetoothAddress(),
AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_DOWN,
AvrcpControllerService.hexStringToByteUID(mNextStep.getFolderUID()));
}
}
@Override
public void exit() {
mBrowseNode = null;
super.exit();
}
}
class SetAddresedPlayerAndPlayItem extends CmdState {
private static final String STATE_TAG = "AVRCPSM.SetAddresedPlayerAndPlayItem";
int mScope;
String mPlayItemId;
String mAddrPlayerId;
public void setItemAndScope(String addrPlayerId, String playItemId, int scope) {
mAddrPlayerId = addrPlayerId;
mPlayItemId = playItemId;
mScope = scope;
}
@Override
public boolean processMessage(Message msg) {
if (DBG) Log.d(STATE_TAG, "processMessage " + msg.what);
switch (msg.what) {
case MESSAGE_PROCESS_SET_ADDRESSED_PLAYER:
// Set the new addressed player.
mBrowseTree.setCurrentAddressedPlayer(mAddrPlayerId);
// And now play the item.
AvrcpControllerService.playItemNative(mRemoteDevice.getBluetoothAddress(),
(byte) mScope, AvrcpControllerService.hexStringToByteUID(mPlayItemId),
(int) 0);
// Transition to connected state here.
transitionTo(mConnected);
break;
case MESSAGE_INTERNAL_CMD_TIMEOUT:
transitionTo(mConnected);
break;
case MESSAGE_SEND_PASS_THROUGH_CMD:
case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
case MESSAGE_PROCESS_TRACK_CHANGED:
case MESSAGE_PROCESS_PLAY_POS_CHANGED:
case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
case MESSAGE_PROCESS_CONNECTION_CHANGE:
case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
// All of these messages should be handled by parent state immediately.
return false;
default:
if (DBG) Log.d(STATE_TAG, "deferring message " + msg.what + " to connected!");
deferMessage(msg);
}
return true;
}
}
// Class template for commands. Each state should do the following:
// (a) In enter() send a timeout message which could be tracked in the
// processMessage() stage.
// (b) In exit() remove all the timeouts.
//
// Essentially the lifecycle of a timeout should be bounded to a CmdState always.
abstract class CmdState extends State {
@Override
public void enter() {
sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
}
@Override
public void exit() {
removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT);
}
}
// Interface APIs
boolean isConnected() {
synchronized (mLock) {
return mIsConnected;
}
}
void doQuit() {
try {
mContext.unregisterReceiver(mBroadcastReceiver);
} catch (IllegalArgumentException expected) {
// If the receiver was never registered unregister will throw an
// IllegalArgumentException.
}
quit();
}
void dump(StringBuilder sb) {
if (mRemoteDevice == null) return;
BluetoothDevice device = mRemoteDevice.mBTDevice;
if (device == null) return;
ProfileService.println(sb, "mCurrentDevice: " + device.getAddress() + "("
+ device.getName() + ") " + this.toString());
}
MediaMetadata getCurrentMetaData() {
synchronized (mLock) {
if (mAddressedPlayer != null && mAddressedPlayer.getCurrentTrack() != null) {
MediaMetadata mmd = mAddressedPlayer.getCurrentTrack().getMediaMetaData();
if (DBG) {
Log.d(TAG, "getCurrentMetaData mmd " + mmd);
}
}
return EMPTY_MEDIA_METADATA;
}
}
PlaybackState getCurrentPlayBackState() {
return getCurrentPlayBackState(true);
}
PlaybackState getCurrentPlayBackState(boolean cached) {
if (cached) {
synchronized (mLock) {
if (mAddressedPlayer == null) {
return new PlaybackState.Builder().setState(PlaybackState.STATE_ERROR,
PlaybackState.PLAYBACK_POSITION_UNKNOWN, 0).build();
}
return mAddressedPlayer.getPlaybackState();
}
} else {
// Issue a native request, we return NULL since this is only for PTS.
AvrcpControllerService.getPlaybackStateNative(mRemoteDevice.getBluetoothAddress());
return null;
}
}
List<MediaItem> getContents(String uid) {
BrowseTree.BrowseNode currentNode = mBrowseTree.findBrowseNodeByID(uid);
if (DBG) Log.d(TAG, "getContents(" + uid + ") currentNode = " + currentNode);
if (currentNode != null) {
if (!currentNode.isCached()) {
sendMessage(AvrcpControllerStateMachine.MESSAGE_GET_FOLDER_LIST, uid);
}
return currentNode.getContents();
}
return null;
}
public void fetchAttrAndPlayItem(String uid) {
BrowseTree.BrowseNode currItem = mBrowseTree.findBrowseNodeByID(uid);
BrowseTree.BrowseNode currFolder = mBrowseTree.getCurrentBrowsedFolder();
if (DBG) Log.d(TAG, "fetchAttrAndPlayItem mediaId=" + uid + " node=" + currItem);
if (currItem != null) {
int scope = currItem.getParent().isNowPlaying()
? AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING
: AvrcpControllerService.BROWSE_SCOPE_VFS;
Message msg =
obtainMessage(AvrcpControllerStateMachine.MESSAGE_FETCH_ATTR_AND_PLAY_ITEM,
scope, 0, currItem.getFolderUID());
sendMessage(msg);
}
}
private void broadcastMetaDataChanged(MediaMetadata metadata) {
Intent intent = new Intent(AvrcpControllerService.ACTION_TRACK_EVENT);
intent.putExtra(AvrcpControllerService.EXTRA_METADATA, metadata);
if (VDBG) {
Log.d(TAG, " broadcastMetaDataChanged = " + metadata.getDescription());
}
mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
}
private void broadcastFolderList(String id) {
Intent intent = new Intent(AvrcpControllerService.ACTION_FOLDER_LIST);
if (VDBG) Log.d(TAG, "broadcastFolderList id " + id);
intent.putExtra(AvrcpControllerService.EXTRA_FOLDER_ID, id);
BluetoothMediaBrowserService bluetoothMediaBrowserService =
BluetoothMediaBrowserService.getBluetoothMediaBrowserService();
if (bluetoothMediaBrowserService != null) {
bluetoothMediaBrowserService.processInternalEvent(intent);
}
}
private void broadcastPlayBackStateChanged(PlaybackState state) {
Intent intent = new Intent(AvrcpControllerService.ACTION_TRACK_EVENT);
intent.putExtra(AvrcpControllerService.EXTRA_PLAYBACK, state);
if (DBG) {
Log.d(TAG, " broadcastPlayBackStateChanged = " + state.toString());
}
mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
}
private void setAbsVolume(int absVol, int label) {
int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
int currIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
// Ignore first volume command since phone may not know difference between stream volume
// and amplifier volume.
if (mRemoteDevice.getFirstAbsVolCmdRecvd()) {
int newIndex = (maxVolume * absVol) / ABS_VOL_BASE;
if (DBG) {
Log.d(TAG, " setAbsVolume =" + absVol + " maxVol = " + maxVolume
+ " cur = " + currIndex + " new = " + newIndex);
}
/*
* 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 (newIndex != currIndex) {
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newIndex,
AudioManager.FLAG_SHOW_UI);
}
} else {
mRemoteDevice.setFirstAbsVolCmdRecvd();
absVol = (currIndex * ABS_VOL_BASE) / maxVolume;
if (DBG) Log.d(TAG, " SetAbsVol recvd for first time, respond with " + absVol);
}
AvrcpControllerService.sendAbsVolRspNative(mRemoteDevice.getBluetoothAddress(), absVol,
label);
}
private int getVolumePercentage() {
int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
int currIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
int percentageVol = ((currIndex * ABS_VOL_BASE) / maxVolume);
return percentageVol;
}
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) {
int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
if (streamType == AudioManager.STREAM_MUSIC) {
sendMessage(MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION);
}
}
}
};
public static String dumpMessageString(int message) {
String str = "UNKNOWN";
switch (message) {
case MESSAGE_SEND_PASS_THROUGH_CMD:
str = "REQ_PASS_THROUGH_CMD";
break;
case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
str = "REQ_GRP_NAV_CMD";
break;
case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
str = "CB_SET_ABS_VOL_CMD";
break;
case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
str = "CB_REGISTER_ABS_VOL";
break;
case MESSAGE_PROCESS_TRACK_CHANGED:
str = "CB_TRACK_CHANGED";
break;
case MESSAGE_PROCESS_PLAY_POS_CHANGED:
str = "CB_PLAY_POS_CHANGED";
break;
case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
str = "CB_PLAY_STATUS_CHANGED";
break;
case MESSAGE_PROCESS_RC_FEATURES:
str = "CB_RC_FEATURES";
break;
case MESSAGE_PROCESS_CONNECTION_CHANGE:
str = "CB_CONN_CHANGED";
break;
default:
str = Integer.toString(message);
break;
}
return str;
}
}