| /* |
| * 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 5 items at a time. |
| static final int GET_FOLDER_ITEMS_PAGINATION_SIZE = 5; |
| |
| /* |
| * 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<MediaItem> 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<MediaItem> 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 = endInd; |
| } |
| |
| @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; |
| 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<AvrcpPlayer> playerList = (List<AvrcpPlayer>) msg.obj; |
| mBrowseTree.refreshChildren(BrowseTree.ROOT, playerList); |
| ArrayList<MediaItem> 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<MediaItem> childrenList = new ArrayList<MediaItem>(); |
| 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<MediaItem> 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(); |
| } |
| } |