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