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