/* * 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.BluetoothAvrcpPlayerSettings; 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.MediaDescription; import android.media.MediaMetadata; import android.media.browse.MediaBrowser.MediaItem; import android.media.session.PlaybackState; import android.os.Bundle; import android.os.Message; import android.util.Log; 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_NOW_PLAYING_LIST = 5; static final int MESSAGE_GET_FOLDER_LIST = 6; static final int MESSAGE_GET_PLAYER_LIST = 7; static final int MESSAGE_CHANGE_FOLDER_PATH = 8; static final int MESSAGE_FETCH_ATTR_AND_PLAY_ITEM = 9; static final int MESSAGE_SET_BROWSED_PLAYER = 10; // 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; // commands from A2DP sink static final int MESSAGE_STOP_METADATA_BROADCASTS = 201; static final int MESSAGE_START_METADATA_BROADCASTS = 202; // 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_BROWSE_DEPTH_INCREMENT = 401; static final int MESSAGE_INTERNAL_MOVE_N_LEVELS_UP = 402; 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 SetBrowsedPlayer mSetBrowsedPlayer; private final SetAddresedPlayerAndPlayItem mSetAddrPlayer; private final ChangeFolderPath mChangeFolderPath; private final GetFolderList mGetFolderList; private final GetPlayerListing mGetPlayerListing; private final MoveToRoot mMoveToRoot; private final Object mLock = new Object(); private static final ArrayList EMPTY_MEDIA_ITEM_LIST = new ArrayList<>(); 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; // Depth from root of current browsing. This can be used to move to root directly. private int mBrowseDepth = 0; // 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. mSetBrowsedPlayer = new SetBrowsedPlayer(); mSetAddrPlayer = new SetAddresedPlayerAndPlayItem(); mChangeFolderPath = new ChangeFolderPath(); mGetFolderList = new GetFolderList(); mGetPlayerListing = new GetPlayerListing(); mMoveToRoot = new MoveToRoot(); 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(mSetBrowsedPlayer, mConnected); addState(mSetAddrPlayer, mConnected); addState(mChangeFolderPath, mConnected); addState(mGetFolderList, mConnected); addState(mGetPlayerListing, mConnected); addState(mMoveToRoot, 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.init(); 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_NOW_PLAYING_LIST: mGetFolderList.setFolder((String) msg.obj); mGetFolderList.setBounds((int) msg.arg1, (int) msg.arg2); mGetFolderList.setScope(AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING); transitionTo(mGetFolderList); break; case MESSAGE_GET_FOLDER_LIST: // Whenever we transition we set the information for folder we need to // return result. mGetFolderList.setBounds(msg.arg1, msg.arg2); mGetFolderList.setFolder((String) msg.obj); mGetFolderList.setScope(AvrcpControllerService.BROWSE_SCOPE_VFS); transitionTo(mGetFolderList); break; case MESSAGE_GET_PLAYER_LIST: AvrcpControllerService.getPlayerListNative( mRemoteDevice.getBluetoothAddress(), (byte) msg.arg1, (byte) msg.arg2); transitionTo(mGetPlayerListing); sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS); break; case MESSAGE_CHANGE_FOLDER_PATH: { int direction = msg.arg1; Bundle b = (Bundle) msg.obj; String uid = b.getString(AvrcpControllerService.EXTRA_FOLDER_BT_ID); String fid = b.getString(AvrcpControllerService.EXTRA_FOLDER_ID); // String is encoded as a Hex String (mostly for display purposes) // hence convert this back to real byte string. AvrcpControllerService.changeFolderPathNative( mRemoteDevice.getBluetoothAddress(), (byte) msg.arg1, AvrcpControllerService.hexStringToByteUID(uid)); mChangeFolderPath.setFolder(fid); transitionTo(mChangeFolderPath); sendMessage(MESSAGE_INTERNAL_BROWSE_DEPTH_INCREMENT, (byte) msg.arg1); sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS); 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(); if (DBG) { Log.d(TAG, "currBrPlayer " + currBrPlayer + " currAddrPlayer " + currAddrPlayer); } if (currBrPlayer == null || currBrPlayer.equals(currAddrPlayer)) { // 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), (int) 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_SET_BROWSED_PLAYER: { AvrcpControllerService.setBrowsedPlayerNative( mRemoteDevice.getBluetoothAddress(), (int) msg.arg1); mSetBrowsedPlayer.setFolder((String) msg.obj); transitionTo(mSetBrowsedPlayer); break; } case MESSAGE_PROCESS_SET_ADDRESSED_PLAYER: AvrcpControllerService.getPlayerListNative( mRemoteDevice.getBluetoothAddress(), 0, 255); transitionTo(mGetPlayerListing); sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS); 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); // If browse is disconnected, the next time we connect we should // be at the ROOT. mBrowseDepth = 0; } 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 mAddressedPlayer.updateCurrentTrack((TrackInfo) msg.obj); 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); broadcastPlayBackStateChanged(getCurrentPlayBackState()); 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; default: return false; } } return true; } } // Handle the change folder path meta-action. // a) Send Change folder command // b) Once successful transition to folder fetch state. class ChangeFolderPath extends CmdState { private static final String STATE_TAG = "AVRCPSM.ChangeFolderPath"; private int mTmpIncrDirection; private String mID = ""; public void setFolder(String id) { mID = id; } @Override public void enter() { super.enter(); mTmpIncrDirection = -1; } @Override public boolean processMessage(Message msg) { if (DBG) Log.d(STATE_TAG, "processMessage " + msg.what); switch (msg.what) { case MESSAGE_INTERNAL_BROWSE_DEPTH_INCREMENT: mTmpIncrDirection = msg.arg1; break; case MESSAGE_PROCESS_FOLDER_PATH: { // Fetch the listing of objects in this folder. if (DBG) { Log.d(STATE_TAG, "MESSAGE_PROCESS_FOLDER_PATH returned " + msg.arg1 + " elements"); } // Update the folder depth. if (mTmpIncrDirection == AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP) { mBrowseDepth -= 1; } else if (mTmpIncrDirection == AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_DOWN) { mBrowseDepth += 1; } else { throw new IllegalStateException("incorrect nav " + mTmpIncrDirection); } if (DBG) Log.d(STATE_TAG, "New browse depth " + mBrowseDepth); if (msg.arg1 > 0) { sendMessage(MESSAGE_GET_FOLDER_LIST, 0, msg.arg1 - 1, mID); } else { // Return an empty response to the upper layer. broadcastFolderList(mID, EMPTY_MEDIA_ITEM_LIST); } mBrowseTree.setCurrentBrowsedFolder(mID); transitionTo(mConnected); break; } case MESSAGE_INTERNAL_CMD_TIMEOUT: // We timed out changing folders. It is imperative we tell // the upper layers that we failed by giving them an empty list. Log.e(STATE_TAG, "change folder failed, sending empty list."); broadcastFolderList(mID, EMPTY_MEDIA_ITEM_LIST); 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_STOP_METADATA_BROADCASTS: case MESSAGE_START_METADATA_BROADCASTS: 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 state."); } deferMessage(msg); } 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"; String mID = ""; int mStartInd; int mEndInd; int mCurrInd; int mScope; private ArrayList mFolderList = new ArrayList<>(); @Override public void enter() { // Setup the timeouts. super.enter(); mCurrInd = 0; mFolderList.clear(); callNativeFunctionForScope(mStartInd, Math.min(mEndInd, mStartInd + GET_FOLDER_ITEMS_PAGINATION_SIZE - 1)); } public void setScope(int scope) { mScope = scope; } public void setFolder(String id) { if (DBG) Log.d(STATE_TAG, "Setting folder to " + id); mID = id; } public void setBounds(int startInd, int endInd) { if (DBG) { Log.d(STATE_TAG, "startInd " + startInd + " endInd " + endInd); } mStartInd = startInd; mEndInd = Math.min(endInd, MAX_FOLDER_ITEMS); } @Override public boolean processMessage(Message msg) { Log.d(STATE_TAG, "processMessage " + msg.what); switch (msg.what) { case MESSAGE_PROCESS_GET_FOLDER_ITEMS: ArrayList folderList = (ArrayList) msg.obj; mFolderList.addAll(folderList); if (DBG) { Log.d(STATE_TAG, "Start " + mStartInd + " End " + mEndInd + " Curr " + mCurrInd + " received " + folderList.size()); } mCurrInd += folderList.size(); // Always update the node so that the user does not wait forever // for the list to populate. sendFolderBroadcastAndUpdateNode(); if (mCurrInd > mEndInd || folderList.size() == 0) { // 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. transitionTo(mConnected); } else { // Fetch the next set of items. callNativeFunctionForScope(mCurrInd, Math.min(mEndInd, mCurrInd + GET_FOLDER_ITEMS_PAGINATION_SIZE - 1)); // 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_INTERNAL_CMD_TIMEOUT: // We have timed out to execute the request, we should simply send // whatever listing we have gotten until now. sendFolderBroadcastAndUpdateNode(); 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. transitionTo(mConnected); break; case MESSAGE_CHANGE_FOLDER_PATH: case MESSAGE_FETCH_ATTR_AND_PLAY_ITEM: case MESSAGE_GET_PLAYER_LIST: case MESSAGE_GET_NOW_PLAYING_LIST: case MESSAGE_SET_BROWSED_PLAYER: // A new request has come in, no need to fetch more. mEndInd = 0; 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_STOP_METADATA_BROADCASTS: case MESSAGE_START_METADATA_BROADCASTS: 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 sendFolderBroadcastAndUpdateNode() { BrowseTree.BrowseNode bn = mBrowseTree.findBrowseNodeByID(mID); if (bn == null) { Log.e(TAG, "Can not find BrowseNode by ID: " + mID); return; } if (bn.isPlayer()) { // Add the now playing folder. MediaDescription.Builder mdb = new MediaDescription.Builder(); mdb.setMediaId(BrowseTree.NOW_PLAYING_PREFIX + ":" + bn.getPlayerID()); mdb.setTitle(BrowseTree.NOW_PLAYING_PREFIX); Bundle mdBundle = new Bundle(); mdBundle.putString(AvrcpControllerService.MEDIA_ITEM_UID_KEY, BrowseTree.NOW_PLAYING_PREFIX + ":" + bn.getID()); mdb.setExtras(mdBundle); mFolderList.add(new MediaItem(mdb.build(), MediaItem.FLAG_BROWSABLE)); } mBrowseTree.refreshChildren(bn, mFolderList); broadcastFolderList(mID, mFolderList); // For now playing we need to set the current browsed folder here. // For normal folders it is set after ChangeFolderPath. if (mScope == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) { mBrowseTree.setCurrentBrowsedFolder(mID); } } private void callNativeFunctionForScope(int start, int end) { switch (mScope) { 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 " + mScope + " cannot be handled here."); } } } // Handle the get player listing action // a) Fetch the listing of players // b) Once completed return the object listing class GetPlayerListing extends CmdState { private static final String STATE_TAG = "AVRCPSM.GetPlayerList"; @Override public boolean processMessage(Message msg) { if (DBG) Log.d(STATE_TAG, "processMessage " + msg.what); switch (msg.what) { case MESSAGE_PROCESS_GET_PLAYER_ITEMS: List playerList = (List) msg.obj; mBrowseTree.refreshChildren(BrowseTree.ROOT, playerList); ArrayList mediaItemList = new ArrayList<>(); for (BrowseTree.BrowseNode c : mBrowseTree.findBrowseNodeByID(BrowseTree.ROOT) .getChildren()) { mediaItemList.add(c.getMediaItem()); } broadcastFolderList(BrowseTree.ROOT, mediaItemList); mBrowseTree.setCurrentBrowsedFolder(BrowseTree.ROOT); transitionTo(mConnected); break; case MESSAGE_INTERNAL_CMD_TIMEOUT: // We have timed out to execute the request. // Send an empty list here. broadcastFolderList(BrowseTree.ROOT, EMPTY_MEDIA_ITEM_LIST); 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_STOP_METADATA_BROADCASTS: case MESSAGE_START_METADATA_BROADCASTS: 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 MoveToRoot extends CmdState { private static final String STATE_TAG = "AVRCPSM.MoveToRoot"; private String mID = ""; public void setFolder(String id) { if (DBG) Log.d(STATE_TAG, "setFolder " + id); mID = id; } @Override public void enter() { // Setup the timeouts. super.enter(); // We need to move mBrowseDepth levels up. The following message is // completely internal to this state. sendMessage(MESSAGE_INTERNAL_MOVE_N_LEVELS_UP); } @Override public boolean processMessage(Message msg) { if (DBG) { Log.d(STATE_TAG, "processMessage " + msg.what + " browse depth " + mBrowseDepth); } switch (msg.what) { case MESSAGE_INTERNAL_MOVE_N_LEVELS_UP: if (mBrowseDepth == 0) { Log.w(STATE_TAG, "Already in root!"); transitionTo(mConnected); sendMessage(MESSAGE_GET_FOLDER_LIST, 0, 0xff, mID); } else { AvrcpControllerService.changeFolderPathNative( mRemoteDevice.getBluetoothAddress(), (byte) AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP, AvrcpControllerService.hexStringToByteUID(null)); } break; case MESSAGE_PROCESS_FOLDER_PATH: mBrowseDepth -= 1; if (DBG) Log.d(STATE_TAG, "New browse depth " + mBrowseDepth); if (mBrowseDepth < 0) { throw new IllegalArgumentException("Browse depth negative!"); } sendMessage(MESSAGE_INTERNAL_MOVE_N_LEVELS_UP); break; case MESSAGE_INTERNAL_CMD_TIMEOUT: broadcastFolderList(BrowseTree.ROOT, EMPTY_MEDIA_ITEM_LIST); 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_STOP_METADATA_BROADCASTS: case MESSAGE_START_METADATA_BROADCASTS: 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 SetBrowsedPlayer extends CmdState { private static final String STATE_TAG = "AVRCPSM.SetBrowsedPlayer"; String mID = ""; public void setFolder(String id) { mID = id; } @Override public boolean processMessage(Message msg) { if (DBG) Log.d(STATE_TAG, "processMessage " + msg.what); switch (msg.what) { case MESSAGE_PROCESS_SET_BROWSED_PLAYER: // Set the new depth. if (DBG) Log.d(STATE_TAG, "player depth " + msg.arg2); mBrowseDepth = msg.arg2; // If we already on top of player and there is no content. // This should very rarely happen. if (mBrowseDepth == 0 && msg.arg1 == 0) { broadcastFolderList(mID, EMPTY_MEDIA_ITEM_LIST); transitionTo(mConnected); } else { // Otherwise move to root and fetch the listing. // the MoveToRoot#enter() function takes care of fetch. mMoveToRoot.setFolder(mID); transitionTo(mMoveToRoot); } mBrowseTree.setCurrentBrowsedFolder(mID); // Also set the browsed player here. mBrowseTree.setCurrentBrowsedPlayer(mID); break; case MESSAGE_INTERNAL_CMD_TIMEOUT: broadcastFolderList(mID, EMPTY_MEDIA_ITEM_LIST); 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_STOP_METADATA_BROADCASTS: case MESSAGE_START_METADATA_BROADCASTS: 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 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_STOP_METADATA_BROADCASTS: case MESSAGE_START_METADATA_BROADCASTS: 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) { ProfileService.println(sb, "StateMachine: " + 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; } } // Entry point to the state machine where the services should call to fetch children // for a specific node. It checks if the currently browsed node is the same as the one being // asked for, in that case it returns the currently cached children. This saves bandwidth and // also if we are already fetching elements for a current folder (since we need to batch // fetches) then we should not submit another request but simply return what we have fetched // until now. // // It handles fetches to all VFS, Now Playing and Media Player lists. void getChildren(String parentMediaId, int start, int items) { BrowseTree.BrowseNode bn = mBrowseTree.findBrowseNodeByID(parentMediaId); if (bn == null) { Log.e(TAG, "Invalid folder to browse " + mBrowseTree); broadcastFolderList(parentMediaId, EMPTY_MEDIA_ITEM_LIST); return; } if (DBG) { Log.d(TAG, "To Browse folder " + bn + " is cached " + bn.isCached() + " current folder " + mBrowseTree.getCurrentBrowsedFolder()); } if (bn.equals(mBrowseTree.getCurrentBrowsedFolder()) && bn.isCached()) { if (DBG) { Log.d(TAG, "Same cached folder -- returning existing children."); } BrowseTree.BrowseNode n = mBrowseTree.findBrowseNodeByID(parentMediaId); ArrayList childrenList = new ArrayList(); for (BrowseTree.BrowseNode cn : n.getChildren()) { childrenList.add(cn.getMediaItem()); } broadcastFolderList(parentMediaId, childrenList); return; } Message msg = null; int btDirection = mBrowseTree.getDirection(parentMediaId); BrowseTree.BrowseNode currFol = mBrowseTree.getCurrentBrowsedFolder(); if (DBG) { Log.d(TAG, "Browse direction parent " + mBrowseTree.getCurrentBrowsedFolder() + " req " + parentMediaId + " direction " + btDirection); } if (BrowseTree.ROOT.equals(parentMediaId)) { // Root contains the list of players. msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_GET_PLAYER_LIST, start, items); } else if (bn.isPlayer() && btDirection != BrowseTree.DIRECTION_SAME) { // Set browsed (and addressed player) as the new player. // This should fetch the list of folders. msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_SET_BROWSED_PLAYER, bn.getPlayerID(), 0, bn.getID()); } else if (bn.isNowPlaying()) { // Issue a request to fetch the items. msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_GET_NOW_PLAYING_LIST, start, items, parentMediaId); } else { // Only change folder if desired. If an app refreshes a folder // (because it resumed etc) and current folder does not change // then we can simply fetch list. // We exempt two conditions from change folder: // a) If the new folder is the same as current folder (refresh of UI) // b) If the new folder is ROOT and current folder is NOW_PLAYING (or vice-versa) // In this condition we 'fake' child-parent hierarchy but it does not exist in // bluetooth world. boolean isNowPlayingToRoot = currFol.isNowPlaying() && bn.getID().equals(BrowseTree.ROOT); if (!isNowPlayingToRoot) { // Find the direction of traversal. int direction = -1; if (DBG) Log.d(TAG, "Browse direction " + currFol + " " + bn + " = " + btDirection); if (btDirection == BrowseTree.DIRECTION_UNKNOWN) { Log.w(TAG, "parent " + bn + " is not a direct " + "successor or predeccessor of current folder " + currFol); broadcastFolderList(parentMediaId, EMPTY_MEDIA_ITEM_LIST); return; } if (btDirection == BrowseTree.DIRECTION_DOWN) { direction = AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_DOWN; } else if (btDirection == BrowseTree.DIRECTION_UP) { direction = AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP; } Bundle b = new Bundle(); b.putString(AvrcpControllerService.EXTRA_FOLDER_ID, bn.getID()); b.putString(AvrcpControllerService.EXTRA_FOLDER_BT_ID, bn.getFolderUID()); msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_CHANGE_FOLDER_PATH, direction, 0, b); } else { // Fetch the listing without changing paths. msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_GET_FOLDER_LIST, start, items, bn.getFolderUID()); } } if (msg != null) { sendMessage(msg); } } public void fetchAttrAndPlayItem(String uid) { BrowseTree.BrowseNode currItem = mBrowseTree.findFolderByIDLocked(uid); BrowseTree.BrowseNode currFolder = mBrowseTree.getCurrentBrowsedFolder(); if (DBG) Log.d(TAG, "fetchAttrAndPlayItem mediaId=" + uid + " node=" + currItem); if (currItem != null) { int scope = currFolder.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, ArrayList items) { Intent intent = new Intent(AvrcpControllerService.ACTION_FOLDER_LIST); if (VDBG) Log.d(TAG, "broadcastFolderList id " + id + " items " + items); intent.putExtra(AvrcpControllerService.EXTRA_FOLDER_ID, id); intent.putParcelableArrayListExtra(AvrcpControllerService.EXTRA_FOLDER_LIST, items); mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); } 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; } public static String displayBluetoothAvrcpSettings(BluetoothAvrcpPlayerSettings mSett) { StringBuffer sb = new StringBuffer(); int supportedSetting = mSett.getSettings(); if (VDBG) { Log.d(TAG, " setting: " + supportedSetting); } if ((supportedSetting & BluetoothAvrcpPlayerSettings.SETTING_EQUALIZER) != 0) { sb.append(" EQ : "); sb.append(Integer.toString(mSett.getSettingValue( BluetoothAvrcpPlayerSettings.SETTING_EQUALIZER))); } if ((supportedSetting & BluetoothAvrcpPlayerSettings.SETTING_REPEAT) != 0) { sb.append(" REPEAT : "); sb.append(Integer.toString(mSett.getSettingValue( BluetoothAvrcpPlayerSettings.SETTING_REPEAT))); } if ((supportedSetting & BluetoothAvrcpPlayerSettings.SETTING_SHUFFLE) != 0) { sb.append(" SHUFFLE : "); sb.append(Integer.toString(mSett.getSettingValue( BluetoothAvrcpPlayerSettings.SETTING_SHUFFLE))); } if ((supportedSetting & BluetoothAvrcpPlayerSettings.SETTING_SCAN) != 0) { sb.append(" SCAN : "); sb.append(Integer.toString(mSett.getSettingValue( BluetoothAvrcpPlayerSettings.SETTING_SCAN))); } return sb.toString(); } }