• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
AvrcpControllerStateMachine(Context context)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
processMessage(Message msg)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
processMessage(Message msg)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 
setFolder(String id)467         public void setFolder(String id) {
468             mID = id;
469         }
470 
471         @Override
enter()472         public void enter() {
473             super.enter();
474             mTmpIncrDirection = -1;
475         }
476 
477         @Override
processMessage(Message msg)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
enter()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 
setScope(int scope)551         public void setScope(int scope) {
552             mScope = scope;
553         }
554 
setFolder(String id)555         public void setFolder(String id) {
556             Log.d(STATE_TAG, "Setting folder to " + id);
557             mID = id;
558         }
559 
setBounds(int startInd, int endInd)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
processMessage(Message msg)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 
sendFolderBroadcastAndUpdateNode()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 
callNativeFunctionForScope(int start, int end)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
processMessage(Message msg)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 
setFolder(String id)707         public void setFolder(String id) {
708             Log.d(STATE_TAG, "setFolder " + id);
709             mID = id;
710         }
711 
712         @Override
enter()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
processMessage(Message msg)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 
setFolder(String id)761         public void setFolder(String id) {
762             mID = id;
763         }
764 
765         @Override
processMessage(Message msg)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 
setItemAndScope(String addrPlayerId, String playItemId, int scope)809         public void setItemAndScope(String addrPlayerId, String playItemId, int scope) {
810             mAddrPlayerId = addrPlayerId;
811             mPlayItemId = playItemId;
812             mScope = scope;
813         }
814 
815         @Override
processMessage(Message msg)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
enter()852         public void enter() {
853             sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
854         }
855 
856         @Override
exit()857         public void exit() {
858             removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT);
859         }
860     }
861 
862     // Interface APIs
isConnected()863     boolean isConnected() {
864         synchronized (mLock) {
865             return mIsConnected;
866         }
867     }
868 
doQuit()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 
dump(StringBuilder sb)879     void dump(StringBuilder sb) {
880         ProfileService.println(sb, "StateMachine: " + this.toString());
881     }
882 
getCurrentMetaData()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 
getCurrentPlayBackState()895     PlaybackState getCurrentPlayBackState() {
896         return getCurrentPlayBackState(true);
897     }
898 
getCurrentPlayBackState(boolean cached)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.
getChildren(String parentMediaId, int start, int items)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 
fetchAttrAndPlayItem(String uid)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 
broadcastMetaDataChanged(MediaMetadata metadata)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 
broadcastFolderList(String id, ArrayList<MediaItem> items)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 
broadcastPlayBackStateChanged(PlaybackState state)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 
setAbsVolume(int absVol, int label)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 
getVolumePercentage()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 
dumpMessageString(int message)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 
displayBluetoothAvrcpSettings(BluetoothAvrcpPlayerSettings mSett)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