Home | History | Annotate | Download | only in avrcpcontroller
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.bluetooth.avrcpcontroller;
     18 
     19 import android.bluetooth.BluetoothAvrcpController;
     20 import android.bluetooth.BluetoothAvrcpPlayerSettings;
     21 import android.bluetooth.BluetoothDevice;
     22 import android.bluetooth.BluetoothProfile;
     23 import android.content.BroadcastReceiver;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.IntentFilter;
     27 import android.media.AudioManager;
     28 import android.media.browse.MediaBrowser;
     29 import android.media.browse.MediaBrowser.MediaItem;
     30 import android.media.MediaDescription;
     31 import android.media.MediaMetadata;
     32 import android.media.session.PlaybackState;
     33 import android.os.Bundle;
     34 import android.os.Message;
     35 import android.util.Log;
     36 
     37 import com.android.bluetooth.Utils;
     38 import com.android.bluetooth.a2dpsink.A2dpSinkService;
     39 import com.android.bluetooth.btservice.ProfileService;
     40 import com.android.internal.util.State;
     41 import com.android.internal.util.StateMachine;
     42 import java.util.ArrayList;
     43 import java.util.Arrays;
     44 import java.util.LinkedList;
     45 import java.util.List;
     46 import java.util.Queue;
     47 
     48 /**
     49  * Provides Bluetooth AVRCP Controller State Machine responsible for all remote control connections
     50  * and interactions with a remote controlable device.
     51  */
     52 class AvrcpControllerStateMachine extends StateMachine {
     53 
     54     // commands from Binder service
     55     static final int MESSAGE_SEND_PASS_THROUGH_CMD = 1;
     56     static final int MESSAGE_SEND_GROUP_NAVIGATION_CMD = 3;
     57     static final int MESSAGE_GET_NOW_PLAYING_LIST = 5;
     58     static final int MESSAGE_GET_FOLDER_LIST = 6;
     59     static final int MESSAGE_GET_PLAYER_LIST = 7;
     60     static final int MESSAGE_CHANGE_FOLDER_PATH = 8;
     61     static final int MESSAGE_FETCH_ATTR_AND_PLAY_ITEM = 9;
     62     static final int MESSAGE_SET_BROWSED_PLAYER = 10;
     63 
     64     // commands from native layer
     65     static final int MESSAGE_PROCESS_SET_ABS_VOL_CMD = 103;
     66     static final int MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION = 104;
     67     static final int MESSAGE_PROCESS_TRACK_CHANGED = 105;
     68     static final int MESSAGE_PROCESS_PLAY_POS_CHANGED = 106;
     69     static final int MESSAGE_PROCESS_PLAY_STATUS_CHANGED = 107;
     70     static final int MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION = 108;
     71     static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS = 109;
     72     static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE = 110;
     73     static final int MESSAGE_PROCESS_GET_PLAYER_ITEMS = 111;
     74     static final int MESSAGE_PROCESS_FOLDER_PATH = 112;
     75     static final int MESSAGE_PROCESS_SET_BROWSED_PLAYER = 113;
     76     static final int MESSAGE_PROCESS_SET_ADDRESSED_PLAYER = 114;
     77 
     78     // commands from A2DP sink
     79     static final int MESSAGE_STOP_METADATA_BROADCASTS = 201;
     80     static final int MESSAGE_START_METADATA_BROADCASTS = 202;
     81 
     82     // commands for connection
     83     static final int MESSAGE_PROCESS_RC_FEATURES = 301;
     84     static final int MESSAGE_PROCESS_CONNECTION_CHANGE = 302;
     85     static final int MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE = 303;
     86 
     87     // Interal messages
     88     static final int MESSAGE_INTERNAL_BROWSE_DEPTH_INCREMENT = 401;
     89     static final int MESSAGE_INTERNAL_MOVE_N_LEVELS_UP = 402;
     90     static final int MESSAGE_INTERNAL_CMD_TIMEOUT = 403;
     91 
     92     static final int CMD_TIMEOUT_MILLIS = 5000; // 5s
     93     // Fetch only 5 items at a time.
     94     static final int GET_FOLDER_ITEMS_PAGINATION_SIZE = 5;
     95 
     96     /*
     97      * Base value for absolute volume from JNI
     98      */
     99     private static final int ABS_VOL_BASE = 127;
    100 
    101     /*
    102      * Notification types for Avrcp protocol JNI.
    103      */
    104     private static final byte NOTIFICATION_RSP_TYPE_INTERIM = 0x00;
    105     private static final byte NOTIFICATION_RSP_TYPE_CHANGED = 0x01;
    106 
    107 
    108     private static final String TAG = "AvrcpControllerSM";
    109     private static final boolean DBG = true;
    110     private static final boolean VDBG = true;
    111 
    112     private final Context mContext;
    113     private final AudioManager mAudioManager;
    114 
    115     private final State mDisconnected;
    116     private final State mConnected;
    117     private final SetBrowsedPlayer mSetBrowsedPlayer;
    118     private final SetAddresedPlayerAndPlayItem mSetAddrPlayer;
    119     private final ChangeFolderPath mChangeFolderPath;
    120     private final GetFolderList mGetFolderList;
    121     private final GetPlayerListing mGetPlayerListing;
    122     private final MoveToRoot mMoveToRoot;
    123 
    124     private final Object mLock = new Object();
    125     private static final ArrayList<MediaItem> mEmptyMediaItemList = new ArrayList<>();
    126     private static final MediaMetadata mEmptyMMD = new MediaMetadata.Builder().build();
    127 
    128     // APIs exist to access these so they must be thread safe
    129     private Boolean mIsConnected = false;
    130     private RemoteDevice mRemoteDevice;
    131     private AvrcpPlayer mAddressedPlayer;
    132 
    133     // Only accessed from State Machine processMessage
    134     private boolean mAbsoluteVolumeChangeInProgress = false;
    135     private boolean mBroadcastMetadata = false;
    136     private int previousPercentageVol = -1;
    137 
    138     // Depth from root of current browsing. This can be used to move to root directly.
    139     private int mBrowseDepth = 0;
    140 
    141     // Browse tree.
    142     private BrowseTree mBrowseTree = new BrowseTree();
    143 
    144     AvrcpControllerStateMachine(Context context) {
    145         super(TAG);
    146         mContext = context;
    147 
    148         mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
    149         IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
    150         mContext.registerReceiver(mBroadcastReceiver, filter);
    151 
    152         mDisconnected = new Disconnected();
    153         mConnected = new Connected();
    154 
    155         // Used to change folder path and fetch the new folder listing.
    156         mSetBrowsedPlayer = new SetBrowsedPlayer();
    157         mSetAddrPlayer = new SetAddresedPlayerAndPlayItem();
    158         mChangeFolderPath = new ChangeFolderPath();
    159         mGetFolderList = new GetFolderList();
    160         mGetPlayerListing = new GetPlayerListing();
    161         mMoveToRoot = new MoveToRoot();
    162 
    163         addState(mDisconnected);
    164         addState(mConnected);
    165 
    166         // Any action that needs blocking other requests to the state machine will be implemented as
    167         // a separate substate of the mConnected state. Once transtition to the sub-state we should
    168         // only handle the messages that are relevant to the sub-action. Everything else should be
    169         // deferred so that once we transition to the mConnected we can process them hence.
    170         addState(mSetBrowsedPlayer, mConnected);
    171         addState(mSetAddrPlayer, mConnected);
    172         addState(mChangeFolderPath, mConnected);
    173         addState(mGetFolderList, mConnected);
    174         addState(mGetPlayerListing, mConnected);
    175         addState(mMoveToRoot, mConnected);
    176 
    177         setInitialState(mDisconnected);
    178     }
    179 
    180     class Disconnected extends State {
    181 
    182         @Override
    183         public boolean processMessage(Message msg) {
    184             Log.d(TAG, " HandleMessage: " + dumpMessageString(msg.what));
    185             switch (msg.what) {
    186                 case MESSAGE_PROCESS_CONNECTION_CHANGE:
    187                     if (msg.arg1 == BluetoothProfile.STATE_CONNECTED) {
    188                         mBrowseTree.init();
    189                         transitionTo(mConnected);
    190                         BluetoothDevice rtDevice = (BluetoothDevice) msg.obj;
    191                         synchronized(mLock) {
    192                             mRemoteDevice = new RemoteDevice(rtDevice);
    193                             mAddressedPlayer = new AvrcpPlayer();
    194                             mIsConnected = true;
    195                         }
    196                         Intent intent = new Intent(
    197                             BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
    198                         intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
    199                             BluetoothProfile.STATE_DISCONNECTED);
    200                         intent.putExtra(BluetoothProfile.EXTRA_STATE,
    201                             BluetoothProfile.STATE_CONNECTED);
    202                         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, rtDevice);
    203                         mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
    204                     }
    205                     break;
    206 
    207                 default:
    208                     Log.w(TAG,"Currently Disconnected not handling " + dumpMessageString(msg.what));
    209                     return false;
    210             }
    211             return true;
    212         }
    213     }
    214 
    215     class Connected extends State {
    216         @Override
    217         public boolean processMessage(Message msg) {
    218             Log.d(TAG, " HandleMessage: " + dumpMessageString(msg.what));
    219             A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();
    220             synchronized (mLock) {
    221                 switch (msg.what) {
    222                     case MESSAGE_STOP_METADATA_BROADCASTS:
    223                         mBroadcastMetadata = false;
    224                         broadcastPlayBackStateChanged(new PlaybackState.Builder().setState(
    225                             PlaybackState.STATE_PAUSED, mAddressedPlayer.getPlayTime(),
    226                             0).build());
    227                         break;
    228 
    229                     case MESSAGE_START_METADATA_BROADCASTS:
    230                         mBroadcastMetadata = true;
    231                         broadcastPlayBackStateChanged(mAddressedPlayer.getPlaybackState());
    232                         if (mAddressedPlayer.getCurrentTrack() != null) {
    233                             broadcastMetaDataChanged(
    234                                 mAddressedPlayer.getCurrentTrack().getMediaMetaData());
    235                         }
    236                         break;
    237 
    238                     case MESSAGE_SEND_PASS_THROUGH_CMD:
    239                         BluetoothDevice device = (BluetoothDevice) msg.obj;
    240                         AvrcpControllerService
    241                             .sendPassThroughCommandNative(Utils.getByteAddress(device), msg.arg1,
    242                                 msg.arg2);
    243                         if (a2dpSinkService != null) {
    244                             Log.d(TAG, " inform AVRCP Commands to A2DP Sink ");
    245                             a2dpSinkService.informAvrcpPassThroughCmd(device, msg.arg1, msg.arg2);
    246                         }
    247                         break;
    248 
    249                     case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
    250                         AvrcpControllerService.sendGroupNavigationCommandNative(
    251                             mRemoteDevice.getBluetoothAddress(), msg.arg1, msg.arg2);
    252                         break;
    253 
    254                     case MESSAGE_GET_NOW_PLAYING_LIST:
    255                         mGetFolderList.setFolder((String) msg.obj);
    256                         mGetFolderList.setBounds((int) msg.arg1, (int) msg.arg2);
    257                         mGetFolderList.setScope(AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING);
    258                         transitionTo(mGetFolderList);
    259                         break;
    260 
    261                     case MESSAGE_GET_FOLDER_LIST:
    262                         // Whenever we transition we set the information for folder we need to
    263                         // return result.
    264                         mGetFolderList.setBounds(msg.arg1, msg.arg2);
    265                         mGetFolderList.setFolder((String) msg.obj);
    266                         mGetFolderList.setScope(AvrcpControllerService.BROWSE_SCOPE_VFS);
    267                         transitionTo(mGetFolderList);
    268                         break;
    269 
    270                     case MESSAGE_GET_PLAYER_LIST:
    271                         AvrcpControllerService.getPlayerListNative(
    272                             mRemoteDevice.getBluetoothAddress(), (byte) msg.arg1,
    273                             (byte) msg.arg2);
    274                         transitionTo(mGetPlayerListing);
    275                         sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
    276                         break;
    277 
    278                     case MESSAGE_CHANGE_FOLDER_PATH: {
    279                         int direction = msg.arg1;
    280                         Bundle b = (Bundle) msg.obj;
    281                         String uid = b.getString(AvrcpControllerService.EXTRA_FOLDER_BT_ID);
    282                         String fid = b.getString(AvrcpControllerService.EXTRA_FOLDER_ID);
    283 
    284                         // String is encoded as a Hex String (mostly for display purposes)
    285                         // hence convert this back to real byte string.
    286                         AvrcpControllerService.changeFolderPathNative(
    287                             mRemoteDevice.getBluetoothAddress(), (byte) msg.arg1,
    288                             AvrcpControllerService.hexStringToByteUID(uid));
    289                         mChangeFolderPath.setFolder(fid);
    290                         transitionTo(mChangeFolderPath);
    291                         sendMessage(MESSAGE_INTERNAL_BROWSE_DEPTH_INCREMENT, (byte) msg.arg1);
    292                         sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
    293                         break;
    294                     }
    295 
    296                     case MESSAGE_FETCH_ATTR_AND_PLAY_ITEM: {
    297                         int scope = msg.arg1;
    298                         String playItemUid = (String) msg.obj;
    299                         BrowseTree.BrowseNode currBrPlayer =
    300                             mBrowseTree.getCurrentBrowsedPlayer();
    301                         BrowseTree.BrowseNode currAddrPlayer =
    302                             mBrowseTree.getCurrentAddressedPlayer();
    303                         if (DBG) {
    304                             Log.d(TAG, "currBrPlayer " + currBrPlayer +
    305                                 " currAddrPlayer " + currAddrPlayer);
    306                         }
    307 
    308                         if (currBrPlayer == null || currBrPlayer.equals(currAddrPlayer)) {
    309                             // String is encoded as a Hex String (mostly for display purposes)
    310                             // hence convert this back to real byte string.
    311                             // NOTE: It may be possible that sending play while the same item is
    312                             // playing leads to reset of track.
    313                             AvrcpControllerService.playItemNative(
    314                                 mRemoteDevice.getBluetoothAddress(), (byte) scope,
    315                                 AvrcpControllerService.hexStringToByteUID(playItemUid), (int) 0);
    316                         } else {
    317                             // Send out the request for setting addressed player.
    318                             AvrcpControllerService.setAddressedPlayerNative(
    319                                 mRemoteDevice.getBluetoothAddress(),
    320                                 currBrPlayer.getPlayerID());
    321                             mSetAddrPlayer.setItemAndScope(
    322                                 currBrPlayer.getID(), playItemUid, scope);
    323                             transitionTo(mSetAddrPlayer);
    324                         }
    325                         break;
    326                     }
    327 
    328                     case MESSAGE_SET_BROWSED_PLAYER: {
    329                         AvrcpControllerService.setBrowsedPlayerNative(
    330                             mRemoteDevice.getBluetoothAddress(), (int) msg.arg1);
    331                         mSetBrowsedPlayer.setFolder((String) msg.obj);
    332                         transitionTo(mSetBrowsedPlayer);
    333                         break;
    334                     }
    335 
    336                     case MESSAGE_PROCESS_CONNECTION_CHANGE:
    337                         if (msg.arg1 == BluetoothProfile.STATE_DISCONNECTED) {
    338                             synchronized (mLock) {
    339                                 mIsConnected = false;
    340                                 mRemoteDevice = null;
    341                             }
    342                             mBrowseTree.clear();
    343                             transitionTo(mDisconnected);
    344                             BluetoothDevice rtDevice = (BluetoothDevice) msg.obj;
    345                             Intent intent = new Intent(
    346                                 BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
    347                             intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
    348                                 BluetoothProfile.STATE_CONNECTED);
    349                             intent.putExtra(BluetoothProfile.EXTRA_STATE,
    350                                 BluetoothProfile.STATE_DISCONNECTED);
    351                             intent.putExtra(BluetoothDevice.EXTRA_DEVICE, rtDevice);
    352                             mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
    353                         }
    354                         break;
    355 
    356                     case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
    357                         // Service tells us if the browse is connected or disconnected.
    358                         // This is useful only for deciding whether to send browse commands rest of
    359                         // the connection state handling should be done via the message
    360                         // MESSAGE_PROCESS_CONNECTION_CHANGE.
    361                         Intent intent = new Intent(
    362                             AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED);
    363                         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, (BluetoothDevice) msg.obj);
    364                         if (DBG) {
    365                             Log.d(TAG, "Browse connection state " + msg.arg1);
    366                         }
    367                         if (msg.arg1 == 1) {
    368                             intent.putExtra(
    369                                 BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED);
    370                         } else if (msg.arg1 == 0) {
    371                             intent.putExtra(
    372                                 BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED);
    373                             // If browse is disconnected, the next time we connect we should
    374                             // be at the ROOT.
    375                             mBrowseDepth = 0;
    376                         } else {
    377                             Log.w(TAG, "Incorrect browse state " + msg.arg1);
    378                         }
    379 
    380                         mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
    381                         break;
    382 
    383                     case MESSAGE_PROCESS_RC_FEATURES:
    384                         mRemoteDevice.setRemoteFeatures(msg.arg1);
    385                         break;
    386 
    387                     case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
    388                         mAbsoluteVolumeChangeInProgress = true;
    389                         setAbsVolume(msg.arg1, msg.arg2);
    390                         break;
    391 
    392                     case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION: {
    393                         mRemoteDevice.setNotificationLabel(msg.arg1);
    394                         mRemoteDevice.setAbsVolNotificationRequested(true);
    395                         int percentageVol = getVolumePercentage();
    396                         Log.d(TAG,
    397                             " Sending Interim Response = " + percentageVol + " label " + msg.arg1);
    398                         AvrcpControllerService
    399                             .sendRegisterAbsVolRspNative(mRemoteDevice.getBluetoothAddress(),
    400                                 NOTIFICATION_RSP_TYPE_INTERIM,
    401                                 percentageVol,
    402                                 mRemoteDevice.getNotificationLabel());
    403                     }
    404                     break;
    405 
    406                     case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION: {
    407                         if (mAbsoluteVolumeChangeInProgress) {
    408                             mAbsoluteVolumeChangeInProgress = false;
    409                         } else {
    410                             if (mRemoteDevice.getAbsVolNotificationRequested()) {
    411                                 int percentageVol = getVolumePercentage();
    412                                 if (percentageVol != previousPercentageVol) {
    413                                     AvrcpControllerService.sendRegisterAbsVolRspNative(
    414                                         mRemoteDevice.getBluetoothAddress(),
    415                                         NOTIFICATION_RSP_TYPE_CHANGED,
    416                                         percentageVol, mRemoteDevice.getNotificationLabel());
    417                                     previousPercentageVol = percentageVol;
    418                                     mRemoteDevice.setAbsVolNotificationRequested(false);
    419                                 }
    420                             }
    421                         }
    422                     }
    423                     break;
    424 
    425                     case MESSAGE_PROCESS_TRACK_CHANGED:
    426                         mAddressedPlayer.updateCurrentTrack((TrackInfo) msg.obj);
    427                         if (mBroadcastMetadata) {
    428                             broadcastMetaDataChanged(mAddressedPlayer.getCurrentTrack().
    429                                 getMediaMetaData());
    430                         }
    431                         break;
    432 
    433                     case MESSAGE_PROCESS_PLAY_POS_CHANGED:
    434                         mAddressedPlayer.setPlayTime(msg.arg2);
    435                         if (mBroadcastMetadata) {
    436                             broadcastPlayBackStateChanged(getCurrentPlayBackState());
    437                         }
    438                         break;
    439 
    440                     case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
    441                         int status = msg.arg1;
    442                         mAddressedPlayer.setPlayStatus(status);
    443                         if (status == PlaybackState.STATE_PLAYING) {
    444                             a2dpSinkService.informTGStatePlaying(mRemoteDevice.mBTDevice, true);
    445                         } else if (status == PlaybackState.STATE_PAUSED ||
    446                             status == PlaybackState.STATE_STOPPED) {
    447                             a2dpSinkService.informTGStatePlaying(mRemoteDevice.mBTDevice, false);
    448                         }
    449                         break;
    450 
    451                     default:
    452                         return false;
    453                 }
    454             }
    455             return true;
    456         }
    457     }
    458 
    459     // Handle the change folder path meta-action.
    460     // a) Send Change folder command
    461     // b) Once successful transition to folder fetch state.
    462     class ChangeFolderPath extends CmdState {
    463         private String STATE_TAG = "AVRCPSM.ChangeFolderPath";
    464         private int mTmpIncrDirection;
    465         private String mID = "";
    466 
    467         public void setFolder(String id) {
    468             mID = id;
    469         }
    470 
    471         @Override
    472         public void enter() {
    473             super.enter();
    474             mTmpIncrDirection = -1;
    475         }
    476 
    477         @Override
    478         public boolean processMessage(Message msg) {
    479             Log.d(STATE_TAG, "processMessage " + msg);
    480             switch (msg.what) {
    481                 case MESSAGE_INTERNAL_BROWSE_DEPTH_INCREMENT:
    482                     mTmpIncrDirection = msg.arg1;
    483                     break;
    484 
    485                 case MESSAGE_PROCESS_FOLDER_PATH: {
    486                     // Fetch the listing of objects in this folder.
    487                     Log.d(STATE_TAG, "MESSAGE_PROCESS_FOLDER_PATH returned " + msg.arg1 +
    488                         " elements");
    489 
    490                     // Update the folder depth.
    491                     if (mTmpIncrDirection ==
    492                         AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP) {
    493                         mBrowseDepth -= 1;;
    494                     } else if (mTmpIncrDirection ==
    495                         AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_DOWN) {
    496                         mBrowseDepth += 1;
    497                     } else {
    498                         throw new IllegalStateException("incorrect nav " + mTmpIncrDirection);
    499                     }
    500                     Log.d(STATE_TAG, "New browse depth " + mBrowseDepth);
    501 
    502                     if (msg.arg1 > 0) {
    503                         sendMessage(MESSAGE_GET_FOLDER_LIST, 0, msg.arg1 -1, mID);
    504                     } else {
    505                         // Return an empty response to the upper layer.
    506                         broadcastFolderList(mID, mEmptyMediaItemList);
    507                     }
    508                     mBrowseTree.setCurrentBrowsedFolder(mID);
    509                     transitionTo(mConnected);
    510                     break;
    511                 }
    512 
    513                 case MESSAGE_INTERNAL_CMD_TIMEOUT:
    514                     // We timed out changing folders. It is imperative we tell
    515                     // the upper layers that we failed by giving them an empty list.
    516                     Log.e(STATE_TAG, "change folder failed, sending empty list.");
    517                     broadcastFolderList(mID, mEmptyMediaItemList);
    518                     transitionTo(mConnected);
    519                     break;
    520 
    521                 default:
    522                     Log.d(STATE_TAG, "deferring message " + msg + " to Connected state.");
    523                     deferMessage(msg);
    524             }
    525             return true;
    526         }
    527     }
    528 
    529     // Handle the get folder listing action
    530     // a) Fetch the listing of folders
    531     // b) Once completed return the object listing
    532     class GetFolderList extends CmdState {
    533         private String STATE_TAG = "AVRCPSM.GetFolderList";
    534 
    535         String mID = "";
    536         int mStartInd;
    537         int mEndInd;
    538         int mCurrInd;
    539         int mScope;
    540         private ArrayList<MediaItem> mFolderList = new ArrayList<>();
    541 
    542         @Override
    543         public void enter() {
    544             mCurrInd = 0;
    545             mFolderList.clear();
    546 
    547             callNativeFunctionForScope(
    548                 mStartInd, Math.min(mEndInd, mStartInd + GET_FOLDER_ITEMS_PAGINATION_SIZE - 1));
    549         }
    550 
    551         public void setScope(int scope) {
    552             mScope = scope;
    553         }
    554 
    555         public void setFolder(String id) {
    556             Log.d(STATE_TAG, "Setting folder to " + id);
    557             mID = id;
    558         }
    559 
    560         public void setBounds(int startInd, int endInd) {
    561             if (DBG) {
    562                 Log.d(STATE_TAG, "startInd " + startInd + " endInd " + endInd);
    563             }
    564             mStartInd = startInd;
    565             mEndInd = endInd;
    566         }
    567 
    568         @Override
    569         public boolean processMessage(Message msg) {
    570             Log.d(STATE_TAG, "processMessage " + msg);
    571             switch (msg.what) {
    572                 case MESSAGE_PROCESS_GET_FOLDER_ITEMS:
    573                     ArrayList<MediaItem> folderList = (ArrayList<MediaItem>) msg.obj;
    574                     mFolderList.addAll(folderList);
    575                     if (DBG) {
    576                         Log.d(STATE_TAG, "Start " + mStartInd + " End " + mEndInd + " Curr " +
    577                             mCurrInd + " received " + folderList.size());
    578                     }
    579                     mCurrInd += folderList.size();
    580 
    581                     // Always update the node so that the user does not wait forever
    582                     // for the list to populate.
    583                     sendFolderBroadcastAndUpdateNode();
    584 
    585                     if (mCurrInd > mEndInd || folderList.size() == 0) {
    586                         // If we have fetched all the elements or if the remotes sends us 0 elements
    587                         // (which can lead us into a loop since mCurrInd does not proceed) we simply
    588                         // abort.
    589                         transitionTo(mConnected);
    590                     } else {
    591                         // Fetch the next set of items.
    592                         callNativeFunctionForScope(
    593                             (byte) mCurrInd,
    594                             (byte) Math.min(
    595                                 mEndInd, mCurrInd + GET_FOLDER_ITEMS_PAGINATION_SIZE - 1));
    596                         // Reset the timeout message since we are doing a new fetch now.
    597                         removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT);
    598                         sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
    599                     }
    600                     break;
    601 
    602                 case MESSAGE_INTERNAL_CMD_TIMEOUT:
    603                     // We have timed out to execute the request, we should simply send
    604                     // whatever listing we have gotten until now.
    605                     sendFolderBroadcastAndUpdateNode();
    606                     transitionTo(mConnected);
    607                     break;
    608 
    609                 case MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE:
    610                     // If we have gotten an error for OUT OF RANGE we have
    611                     // already sent all the items to the client hence simply
    612                     // transition to Connected state here.
    613                     transitionTo(mConnected);
    614                     break;
    615 
    616                 default:
    617                     Log.d(STATE_TAG, "deferring message " + msg + " to connected!");
    618                     deferMessage(msg);
    619             }
    620             return true;
    621         }
    622 
    623         private void sendFolderBroadcastAndUpdateNode() {
    624             BrowseTree.BrowseNode bn = mBrowseTree.findBrowseNodeByID(mID);
    625             if (bn.isPlayer()) {
    626                 // Add the now playing folder.
    627                 MediaDescription.Builder mdb = new MediaDescription.Builder();
    628                 mdb.setMediaId(BrowseTree.NOW_PLAYING_PREFIX + ":" +
    629                     bn.getPlayerID());
    630                 mdb.setTitle(BrowseTree.NOW_PLAYING_PREFIX);
    631                 Bundle mdBundle = new Bundle();
    632                 mdBundle.putString(
    633                     AvrcpControllerService.MEDIA_ITEM_UID_KEY,
    634                     BrowseTree.NOW_PLAYING_PREFIX + ":" + bn.getID());
    635                 mdb.setExtras(mdBundle);
    636                 mFolderList.add(new MediaItem(mdb.build(), MediaItem.FLAG_BROWSABLE));
    637             }
    638             mBrowseTree.refreshChildren(bn, mFolderList);
    639             broadcastFolderList(mID, mFolderList);
    640 
    641             // For now playing we need to set the current browsed folder here.
    642             // For normal folders it is set after ChangeFolderPath.
    643             if (mScope == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
    644                 mBrowseTree.setCurrentBrowsedFolder(mID);
    645             }
    646         }
    647 
    648         private void callNativeFunctionForScope(int start, int end) {
    649             switch (mScope) {
    650                 case AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING:
    651                     AvrcpControllerService.getNowPlayingListNative(
    652                         mRemoteDevice.getBluetoothAddress(), (byte) start, (byte) end);
    653                     break;
    654                 case AvrcpControllerService.BROWSE_SCOPE_VFS:
    655                     AvrcpControllerService.getFolderListNative(
    656                         mRemoteDevice.getBluetoothAddress(), (byte) start, (byte) end);
    657                     break;
    658                 default:
    659                     Log.e(STATE_TAG, "Scope " + mScope + " cannot be handled here.");
    660             }
    661         }
    662     }
    663 
    664     // Handle the get player listing action
    665     // a) Fetch the listing of players
    666     // b) Once completed return the object listing
    667     class GetPlayerListing extends CmdState {
    668         private String STATE_TAG = "AVRCPSM.GetPlayerList";
    669 
    670         @Override
    671         public boolean processMessage(Message msg) {
    672             Log.d(STATE_TAG, "processMessage " + msg);
    673             switch (msg.what) {
    674                 case MESSAGE_PROCESS_GET_PLAYER_ITEMS:
    675                     List<AvrcpPlayer> playerList =
    676                         (List<AvrcpPlayer>) msg.obj;
    677                     mBrowseTree.refreshChildren(BrowseTree.ROOT, playerList);
    678                     ArrayList<MediaItem> mediaItemList = new ArrayList<>();
    679                     for (BrowseTree.BrowseNode c :
    680                             mBrowseTree.findBrowseNodeByID(BrowseTree.ROOT).getChildren()) {
    681                         mediaItemList.add(c.getMediaItem());
    682                     }
    683                     broadcastFolderList(BrowseTree.ROOT, mediaItemList);
    684                     mBrowseTree.setCurrentBrowsedFolder(BrowseTree.ROOT);
    685                     transitionTo(mConnected);
    686                     break;
    687 
    688                 case MESSAGE_INTERNAL_CMD_TIMEOUT:
    689                     // We have timed out to execute the request.
    690                     // Send an empty list here.
    691                     broadcastFolderList(BrowseTree.ROOT, mEmptyMediaItemList);
    692                     transitionTo(mConnected);
    693                     break;
    694 
    695                 default:
    696                     Log.d(STATE_TAG, "deferring message " + msg + " to connected!");
    697                     deferMessage(msg);
    698             }
    699             return true;
    700         }
    701     }
    702 
    703     class MoveToRoot extends CmdState {
    704         private String STATE_TAG = "AVRCPSM.MoveToRoot";
    705         private String mID = "";
    706 
    707         public void setFolder(String id) {
    708             Log.d(STATE_TAG, "setFolder " + id);
    709             mID = id;
    710         }
    711 
    712         @Override
    713         public void enter() {
    714             // Setup the timeouts.
    715             super.enter();
    716 
    717             // We need to move mBrowseDepth levels up. The following message is
    718             // completely internal to this state.
    719             sendMessage(MESSAGE_INTERNAL_MOVE_N_LEVELS_UP);
    720         }
    721 
    722         @Override
    723         public boolean processMessage(Message msg) {
    724             Log.d(STATE_TAG, "processMessage " + msg + " browse depth " + mBrowseDepth);
    725             switch (msg.what) {
    726                 case MESSAGE_INTERNAL_MOVE_N_LEVELS_UP:
    727                     if (mBrowseDepth == 0) {
    728                         Log.w(STATE_TAG, "Already in root!");
    729                         transitionTo(mConnected);
    730                         sendMessage(MESSAGE_GET_FOLDER_LIST, 0, 0xff, mID);
    731                     } else {
    732                         AvrcpControllerService.changeFolderPathNative(
    733                             mRemoteDevice.getBluetoothAddress(),
    734                             (byte) AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP,
    735                             AvrcpControllerService.hexStringToByteUID(null));
    736                     }
    737                     break;
    738 
    739                 case MESSAGE_PROCESS_FOLDER_PATH:
    740                     mBrowseDepth -= 1;
    741                     Log.d(STATE_TAG, "New browse depth " + mBrowseDepth);
    742                     if (mBrowseDepth < 0) {
    743                         throw new IllegalArgumentException("Browse depth negative!");
    744                     }
    745 
    746                     sendMessage(MESSAGE_INTERNAL_MOVE_N_LEVELS_UP);
    747                     break;
    748 
    749                 default:
    750                     Log.d(STATE_TAG, "deferring message " + msg + " to connected!");
    751                     deferMessage(msg);
    752             }
    753             return true;
    754         }
    755     }
    756 
    757     class SetBrowsedPlayer extends CmdState {
    758         private String STATE_TAG = "AVRCPSM.SetBrowsedPlayer";
    759         String mID = "";
    760 
    761         public void setFolder(String id) {
    762             mID = id;
    763         }
    764 
    765         @Override
    766         public boolean processMessage(Message msg) {
    767             Log.d(STATE_TAG, "processMessage " + msg);
    768             switch (msg.what) {
    769                 case MESSAGE_PROCESS_SET_BROWSED_PLAYER:
    770                     // Set the new depth.
    771                     Log.d(STATE_TAG, "player depth " + msg.arg2);
    772                     mBrowseDepth = msg.arg2;
    773 
    774                     // If we already on top of player and there is no content.
    775                     // This should very rarely happen.
    776                     if (mBrowseDepth == 0 && msg.arg1 == 0) {
    777                         broadcastFolderList(mID, mEmptyMediaItemList);
    778                         transitionTo(mConnected);
    779                     } else {
    780                         // Otherwise move to root and fetch the listing.
    781                         // the MoveToRoot#enter() function takes care of fetch.
    782                         mMoveToRoot.setFolder(mID);
    783                         transitionTo(mMoveToRoot);
    784                     }
    785                     mBrowseTree.setCurrentBrowsedFolder(mID);
    786                     // Also set the browsed player here.
    787                     mBrowseTree.setCurrentBrowsedPlayer(mID);
    788                     break;
    789 
    790                 case MESSAGE_INTERNAL_CMD_TIMEOUT:
    791                     broadcastFolderList(mID, mEmptyMediaItemList);
    792                     transitionTo(mConnected);
    793                     break;
    794 
    795                 default:
    796                     Log.d(STATE_TAG, "deferring message " + msg + " to connected!");
    797                     deferMessage(msg);
    798             }
    799             return true;
    800         }
    801     }
    802 
    803     class SetAddresedPlayerAndPlayItem extends CmdState {
    804         private String STATE_TAG = "AVRCPSM.SetAddresedPlayerAndPlayItem";
    805         int mScope;
    806         String mPlayItemId;
    807         String mAddrPlayerId;
    808 
    809         public void setItemAndScope(String addrPlayerId, String playItemId, int scope) {
    810             mAddrPlayerId = addrPlayerId;
    811             mPlayItemId = playItemId;
    812             mScope = scope;
    813         }
    814 
    815         @Override
    816         public boolean processMessage(Message msg) {
    817             Log.d(STATE_TAG, "processMessage " + msg);
    818             switch (msg.what) {
    819                 case MESSAGE_PROCESS_SET_ADDRESSED_PLAYER:
    820                     // Set the new addressed player.
    821                     mBrowseTree.setCurrentAddressedPlayer(mAddrPlayerId);
    822 
    823                     // And now play the item.
    824                     AvrcpControllerService.playItemNative(
    825                         mRemoteDevice.getBluetoothAddress(), (byte) mScope,
    826                         AvrcpControllerService.hexStringToByteUID(mPlayItemId), (int) 0);
    827 
    828                     // Transition to connected state here.
    829                     transitionTo(mConnected);
    830                     break;
    831 
    832                 case MESSAGE_INTERNAL_CMD_TIMEOUT:
    833                     transitionTo(mConnected);
    834                     break;
    835 
    836                 default:
    837                     Log.d(STATE_TAG, "deferring message " + msg + " to connected!");
    838                     deferMessage(msg);
    839             }
    840             return true;
    841         }
    842     }
    843 
    844     // Class template for commands. Each state should do the following:
    845     // (a) In enter() send a timeout message which could be tracked in the
    846     // processMessage() stage.
    847     // (b) In exit() remove all the timeouts.
    848     //
    849     // Essentially the lifecycle of a timeout should be bounded to a CmdState always.
    850     abstract class CmdState extends State {
    851         @Override
    852         public void enter() {
    853             sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
    854         }
    855 
    856         @Override
    857         public void exit() {
    858             removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT);
    859         }
    860     }
    861 
    862     // Interface APIs
    863     boolean isConnected() {
    864         synchronized (mLock) {
    865             return mIsConnected;
    866         }
    867     }
    868 
    869     void doQuit() {
    870         try {
    871             mContext.unregisterReceiver(mBroadcastReceiver);
    872         } catch (IllegalArgumentException expected) {
    873             // If the receiver was never registered unregister will throw an
    874             // IllegalArgumentException.
    875         }
    876         quit();
    877     }
    878 
    879     void dump(StringBuilder sb) {
    880         ProfileService.println(sb, "StateMachine: " + this.toString());
    881     }
    882 
    883     MediaMetadata getCurrentMetaData() {
    884         synchronized (mLock) {
    885             if (mAddressedPlayer != null && mAddressedPlayer.getCurrentTrack() != null) {
    886                 MediaMetadata mmd = mAddressedPlayer.getCurrentTrack().getMediaMetaData();
    887                 if (DBG) {
    888                     Log.d(TAG, "getCurrentMetaData mmd " + mmd);
    889                 }
    890             }
    891             return mEmptyMMD;
    892         }
    893     }
    894 
    895     PlaybackState getCurrentPlayBackState() {
    896         return getCurrentPlayBackState(true);
    897     }
    898 
    899     PlaybackState getCurrentPlayBackState(boolean cached) {
    900         if (cached) {
    901             synchronized (mLock) {
    902                 if (mAddressedPlayer == null) {
    903                     return new PlaybackState.Builder().setState(PlaybackState.STATE_ERROR,
    904                         PlaybackState.PLAYBACK_POSITION_UNKNOWN,0).build();
    905                 }
    906                 return mAddressedPlayer.getPlaybackState();
    907             }
    908         } else {
    909             // Issue a native request, we return NULL since this is only for PTS.
    910             AvrcpControllerService.getPlaybackStateNative(mRemoteDevice.getBluetoothAddress());
    911             return null;
    912         }
    913     }
    914 
    915     // Entry point to the state machine where the services should call to fetch children
    916     // for a specific node. It checks if the currently browsed node is the same as the one being
    917     // asked for, in that case it returns the currently cached children. This saves bandwidth and
    918     // also if we are already fetching elements for a current folder (since we need to batch
    919     // fetches) then we should not submit another request but simply return what we have fetched
    920     // until now.
    921     //
    922     // It handles fetches to all VFS, Now Playing and Media Player lists.
    923     void getChildren(String parentMediaId, int start, int items) {
    924         BrowseTree.BrowseNode bn = mBrowseTree.findBrowseNodeByID(parentMediaId);
    925         if (bn == null) {
    926             Log.e(TAG, "Invalid folder to browse " + mBrowseTree);
    927             broadcastFolderList(parentMediaId, mEmptyMediaItemList);
    928             return;
    929         }
    930 
    931         if (DBG) {
    932             Log.d(TAG, "To Browse folder " + bn + " is cached " + bn.isCached() +
    933                 " current folder " + mBrowseTree.getCurrentBrowsedFolder());
    934         }
    935         if (bn.equals(mBrowseTree.getCurrentBrowsedFolder()) && bn.isCached()) {
    936             if (DBG) {
    937                 Log.d(TAG, "Same cached folder -- returning existing children.");
    938             }
    939             BrowseTree.BrowseNode n = mBrowseTree.findBrowseNodeByID(parentMediaId);
    940             ArrayList<MediaItem> childrenList = new ArrayList<MediaItem>();
    941             for (BrowseTree.BrowseNode cn : n.getChildren()) {
    942                 childrenList.add(cn.getMediaItem());
    943             }
    944             broadcastFolderList(parentMediaId, childrenList);
    945             return;
    946         }
    947 
    948         Message msg = null;
    949         int btDirection = mBrowseTree.getDirection(parentMediaId);
    950         BrowseTree.BrowseNode currFol = mBrowseTree.getCurrentBrowsedFolder();
    951         if (DBG) {
    952             Log.d(TAG, "Browse direction parent " + mBrowseTree.getCurrentBrowsedFolder() +
    953                 " req " + parentMediaId + " direction " + btDirection);
    954         }
    955         if (BrowseTree.ROOT.equals(parentMediaId)) {
    956             // Root contains the list of players.
    957             msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_GET_PLAYER_LIST, start, items);
    958         } else if (bn.isPlayer() && btDirection != BrowseTree.DIRECTION_SAME) {
    959             // Set browsed (and addressed player) as the new player.
    960             // This should fetch the list of folders.
    961             msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_SET_BROWSED_PLAYER,
    962                 bn.getPlayerID(), 0, bn.getID());
    963         } else if (bn.isNowPlaying()) {
    964             // Issue a request to fetch the items.
    965             msg = obtainMessage(
    966                 AvrcpControllerStateMachine.MESSAGE_GET_NOW_PLAYING_LIST,
    967                 start, items, parentMediaId);
    968         } else {
    969             // Only change folder if desired. If an app refreshes a folder
    970             // (because it resumed etc) and current folder does not change
    971             // then we can simply fetch list.
    972 
    973             // We exempt two conditions from change folder:
    974             // a) If the new folder is the same as current folder (refresh of UI)
    975             // b) If the new folder is ROOT and current folder is NOW_PLAYING (or vice-versa)
    976             // In this condition we 'fake' child-parent hierarchy but it does not exist in
    977             // bluetooth world.
    978             boolean isNowPlayingToRoot =
    979                 currFol.isNowPlaying() && bn.getID().equals(BrowseTree.ROOT);
    980             if (!isNowPlayingToRoot) {
    981                 // Find the direction of traversal.
    982                 int direction = -1;
    983                 Log.d(TAG, "Browse direction " + currFol + " " + bn + " = " + btDirection);
    984                 if (btDirection == BrowseTree.DIRECTION_UNKNOWN) {
    985                     Log.w(TAG, "parent " + bn + " is not a direct " +
    986                         "successor or predeccessor of current folder " + currFol);
    987                     broadcastFolderList(parentMediaId, mEmptyMediaItemList);
    988                     return;
    989                 }
    990 
    991                 if (btDirection == BrowseTree.DIRECTION_DOWN) {
    992                     direction = AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_DOWN;
    993                 } else if (btDirection == BrowseTree.DIRECTION_UP) {
    994                     direction = AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP;
    995                 }
    996 
    997                 Bundle b = new Bundle();
    998                 b.putString(AvrcpControllerService.EXTRA_FOLDER_ID, bn.getID());
    999                 b.putString(AvrcpControllerService.EXTRA_FOLDER_BT_ID, bn.getFolderUID());
   1000                 msg = obtainMessage(
   1001                     AvrcpControllerStateMachine.MESSAGE_CHANGE_FOLDER_PATH, direction, 0, b);
   1002             } else {
   1003                 // Fetch the listing without changing paths.
   1004                 msg = obtainMessage(
   1005                     AvrcpControllerStateMachine.MESSAGE_GET_FOLDER_LIST,
   1006                     start, items, bn.getFolderUID());
   1007             }
   1008         }
   1009 
   1010         if (msg != null) {
   1011             sendMessage(msg);
   1012         }
   1013     }
   1014 
   1015     public void fetchAttrAndPlayItem(String uid) {
   1016         BrowseTree.BrowseNode currItem = mBrowseTree.findFolderByIDLocked(uid);
   1017         BrowseTree.BrowseNode currFolder = mBrowseTree.getCurrentBrowsedFolder();
   1018         Log.d(TAG, "fetchAttrAndPlayItem mediaId=" + uid + " node=" + currItem);
   1019         if (currItem != null) {
   1020             int scope = currFolder.isNowPlaying() ?
   1021                 AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING :
   1022                 AvrcpControllerService.BROWSE_SCOPE_VFS;
   1023             Message msg = obtainMessage(
   1024                 AvrcpControllerStateMachine.MESSAGE_FETCH_ATTR_AND_PLAY_ITEM,
   1025                 scope, 0, currItem.getFolderUID());
   1026             sendMessage(msg);
   1027         }
   1028     }
   1029 
   1030     private void broadcastMetaDataChanged(MediaMetadata metadata) {
   1031         Intent intent = new Intent(AvrcpControllerService.ACTION_TRACK_EVENT);
   1032         intent.putExtra(AvrcpControllerService.EXTRA_METADATA, metadata);
   1033         if (DBG) {
   1034             Log.d(TAG, " broadcastMetaDataChanged = " + metadata.getDescription());
   1035         }
   1036         mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
   1037     }
   1038 
   1039     private void broadcastFolderList(String id, ArrayList<MediaItem> items) {
   1040         Intent intent = new Intent(AvrcpControllerService.ACTION_FOLDER_LIST);
   1041         Log.d(TAG, "broadcastFolderList id " + id + " items " + items);
   1042         intent.putExtra(AvrcpControllerService.EXTRA_FOLDER_ID, id);
   1043         intent.putParcelableArrayListExtra(
   1044             AvrcpControllerService.EXTRA_FOLDER_LIST, items);
   1045         mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
   1046     }
   1047 
   1048     private void broadcastPlayBackStateChanged(PlaybackState state) {
   1049         Intent intent = new Intent(AvrcpControllerService.ACTION_TRACK_EVENT);
   1050         intent.putExtra(AvrcpControllerService.EXTRA_PLAYBACK, state);
   1051         if (DBG) {
   1052             Log.d(TAG, " broadcastPlayBackStateChanged = " + state.toString());
   1053         }
   1054         mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
   1055     }
   1056 
   1057     private void setAbsVolume(int absVol, int label) {
   1058         int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
   1059         int currIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
   1060         // Ignore first volume command since phone may not know difference between stream volume
   1061         // and amplifier volume.
   1062         if (mRemoteDevice.getFirstAbsVolCmdRecvd()) {
   1063             int newIndex = (maxVolume * absVol) / ABS_VOL_BASE;
   1064             Log.d(TAG,
   1065                 " setAbsVolume =" + absVol + " maxVol = " + maxVolume + " cur = " + currIndex +
   1066                     " new = " + newIndex);
   1067             /*
   1068              * In some cases change in percentage is not sufficient enough to warrant
   1069              * change in index values which are in range of 0-15. For such cases
   1070              * no action is required
   1071              */
   1072             if (newIndex != currIndex) {
   1073                 mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newIndex,
   1074                     AudioManager.FLAG_SHOW_UI);
   1075             }
   1076         } else {
   1077             mRemoteDevice.setFirstAbsVolCmdRecvd();
   1078             absVol = (currIndex * ABS_VOL_BASE) / maxVolume;
   1079             Log.d(TAG, " SetAbsVol recvd for first time, respond with " + absVol);
   1080         }
   1081         AvrcpControllerService.sendAbsVolRspNative(
   1082             mRemoteDevice.getBluetoothAddress(), absVol, label);
   1083     }
   1084 
   1085     private int getVolumePercentage() {
   1086         int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
   1087         int currIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
   1088         int percentageVol = ((currIndex * ABS_VOL_BASE) / maxVolume);
   1089         return percentageVol;
   1090     }
   1091 
   1092     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
   1093         @Override
   1094         public void onReceive(Context context, Intent intent) {
   1095             String action = intent.getAction();
   1096             if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) {
   1097                 int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
   1098                 if (streamType == AudioManager.STREAM_MUSIC) {
   1099                     sendMessage(MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION);
   1100                 }
   1101             }
   1102         }
   1103     };
   1104 
   1105     public static String dumpMessageString(int message) {
   1106         String str = "UNKNOWN";
   1107         switch (message) {
   1108             case MESSAGE_SEND_PASS_THROUGH_CMD:
   1109                 str = "REQ_PASS_THROUGH_CMD";
   1110                 break;
   1111             case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
   1112                 str = "REQ_GRP_NAV_CMD";
   1113                 break;
   1114             case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
   1115                 str = "CB_SET_ABS_VOL_CMD";
   1116                 break;
   1117             case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
   1118                 str = "CB_REGISTER_ABS_VOL";
   1119                 break;
   1120             case MESSAGE_PROCESS_TRACK_CHANGED:
   1121                 str = "CB_TRACK_CHANGED";
   1122                 break;
   1123             case MESSAGE_PROCESS_PLAY_POS_CHANGED:
   1124                 str = "CB_PLAY_POS_CHANGED";
   1125                 break;
   1126             case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
   1127                 str = "CB_PLAY_STATUS_CHANGED";
   1128                 break;
   1129             case MESSAGE_PROCESS_RC_FEATURES:
   1130                 str = "CB_RC_FEATURES";
   1131                 break;
   1132             case MESSAGE_PROCESS_CONNECTION_CHANGE:
   1133                 str = "CB_CONN_CHANGED";
   1134                 break;
   1135             default:
   1136                 str = Integer.toString(message);
   1137                 break;
   1138         }
   1139         return str;
   1140     }
   1141 
   1142     public static String displayBluetoothAvrcpSettings(BluetoothAvrcpPlayerSettings mSett) {
   1143         StringBuffer sb =  new StringBuffer();
   1144         int supportedSetting = mSett.getSettings();
   1145         if(VDBG) Log.d(TAG," setting: " + supportedSetting);
   1146         if((supportedSetting & BluetoothAvrcpPlayerSettings.SETTING_EQUALIZER) != 0) {
   1147             sb.append(" EQ : ");
   1148             sb.append(Integer.toString(mSett.getSettingValue(BluetoothAvrcpPlayerSettings.
   1149                                                              SETTING_EQUALIZER)));
   1150         }
   1151         if((supportedSetting & BluetoothAvrcpPlayerSettings.SETTING_REPEAT) != 0) {
   1152             sb.append(" REPEAT : ");
   1153             sb.append(Integer.toString(mSett.getSettingValue(BluetoothAvrcpPlayerSettings.
   1154                                                              SETTING_REPEAT)));
   1155         }
   1156         if((supportedSetting & BluetoothAvrcpPlayerSettings.SETTING_SHUFFLE) != 0) {
   1157             sb.append(" SHUFFLE : ");
   1158             sb.append(Integer.toString(mSett.getSettingValue(BluetoothAvrcpPlayerSettings.
   1159                                                              SETTING_SHUFFLE)));
   1160         }
   1161         if((supportedSetting & BluetoothAvrcpPlayerSettings.SETTING_SCAN) != 0) {
   1162             sb.append(" SCAN : ");
   1163             sb.append(Integer.toString(mSett.getSettingValue(BluetoothAvrcpPlayerSettings.
   1164                                                              SETTING_SCAN)));
   1165         }
   1166         return sb.toString();
   1167     }
   1168 }
   1169