1 /* 2 * Copyright (C) 2015 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.a2dpsink.mbs; 18 19 import android.bluetooth.BluetoothAdapter; 20 import android.bluetooth.BluetoothAvrcpController; 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.browse.MediaBrowser; 28 import android.media.browse.MediaBrowser.MediaItem; 29 import android.media.MediaDescription; 30 import android.media.MediaMetadata; 31 import android.media.session.MediaController; 32 import android.media.session.MediaSession; 33 import android.media.session.PlaybackState; 34 import android.os.Bundle; 35 import android.os.Handler; 36 import android.os.Looper; 37 import android.os.Message; 38 import android.os.Parcelable; 39 import android.os.ResultReceiver; 40 import android.service.media.MediaBrowserService; 41 import android.util.Pair; 42 import android.util.Log; 43 44 import com.android.bluetooth.R; 45 import com.android.bluetooth.avrcpcontroller.AvrcpControllerService; 46 import com.android.bluetooth.avrcpcontroller.BrowseTree; 47 48 import java.lang.ref.WeakReference; 49 import java.util.ArrayList; 50 import java.util.HashMap; 51 import java.util.List; 52 import java.util.Map; 53 54 /** 55 * Implements the MediaBrowserService interface to AVRCP and A2DP 56 * 57 * This service provides a means for external applications to access A2DP and AVRCP. 58 * The applications are expected to use MediaBrowser (see API) and all the music 59 * browsing/playback/metadata can be controlled via MediaBrowser and MediaController. 60 * 61 * The current behavior of MediaSession exposed by this service is as follows: 62 * 1. MediaSession is active (i.e. SystemUI and other overview UIs can see updates) when device is 63 * connected and first starts playing. Before it starts playing we do not active the session. 64 * 1.1 The session is active throughout the duration of connection. 65 * 2. The session is de-activated when the device disconnects. It will be connected again when (1) 66 * happens. 67 */ 68 public class A2dpMediaBrowserService extends MediaBrowserService { 69 private static final String TAG = "A2dpMediaBrowserService"; 70 private static final String UNKNOWN_BT_AUDIO = "__UNKNOWN_BT_AUDIO__"; 71 private static final float PLAYBACK_SPEED = 1.0f; 72 73 // Message sent when A2DP device is disconnected. 74 private static final int MSG_DEVICE_DISCONNECT = 0; 75 // Message sent when A2DP device is connected. 76 private static final int MSG_DEVICE_CONNECT = 2; 77 // Message sent when we recieve a TRACK update from AVRCP profile over a connected A2DP device. 78 private static final int MSG_TRACK = 4; 79 // Internal message sent to trigger a AVRCP action. 80 private static final int MSG_AVRCP_PASSTHRU = 5; 81 // Internal message to trigger a getplaystatus command to remote. 82 private static final int MSG_AVRCP_GET_PLAY_STATUS_NATIVE = 6; 83 // Message sent when AVRCP browse is connected. 84 private static final int MSG_DEVICE_BROWSE_CONNECT = 7; 85 // Message sent when AVRCP browse is disconnected. 86 private static final int MSG_DEVICE_BROWSE_DISCONNECT = 8; 87 // Message sent when folder list is fetched. 88 private static final int MSG_FOLDER_LIST = 9; 89 90 // Custom actions for PTS testing. 91 private String CUSTOM_ACTION_VOL_UP = "com.android.bluetooth.a2dpsink.mbs.CUSTOM_ACTION_VOL_UP"; 92 private String CUSTOM_ACTION_VOL_DN = "com.android.bluetooth.a2dpsink.mbs.CUSTOM_ACTION_VOL_DN"; 93 private String CUSTOM_ACTION_GET_PLAY_STATUS_NATIVE = 94 "com.android.bluetooth.a2dpsink.mbs.CUSTOM_ACTION_GET_PLAY_STATUS_NATIVE"; 95 96 private MediaSession mSession; 97 private MediaMetadata mA2dpMetadata; 98 99 private AvrcpControllerService mAvrcpCtrlSrvc; 100 private boolean mBrowseConnected = false; 101 private BluetoothDevice mA2dpDevice = null; 102 private Handler mAvrcpCommandQueue; 103 private final Map<String, Result<List<MediaItem>>> mParentIdToRequestMap = new HashMap<>(); 104 private static final List<MediaItem> mEmptyList = new ArrayList<MediaItem>(); 105 106 // Browsing related structures. 107 private List<MediaItem> mNowPlayingList = null; 108 109 private long mTransportControlFlags = PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY 110 | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS; 111 112 private static final class AvrcpCommandQueueHandler extends Handler { 113 WeakReference<A2dpMediaBrowserService> mInst; 114 AvrcpCommandQueueHandler(Looper looper, A2dpMediaBrowserService sink)115 AvrcpCommandQueueHandler(Looper looper, A2dpMediaBrowserService sink) { 116 super(looper); 117 mInst = new WeakReference<A2dpMediaBrowserService>(sink); 118 } 119 120 @Override handleMessage(Message msg)121 public void handleMessage(Message msg) { 122 A2dpMediaBrowserService inst = mInst.get(); 123 if (inst == null) { 124 Log.e(TAG, "Parent class has died; aborting."); 125 return; 126 } 127 128 switch (msg.what) { 129 case MSG_DEVICE_CONNECT: 130 inst.msgDeviceConnect((BluetoothDevice) msg.obj); 131 break; 132 case MSG_DEVICE_DISCONNECT: 133 inst.msgDeviceDisconnect((BluetoothDevice) msg.obj); 134 break; 135 case MSG_TRACK: 136 Pair<PlaybackState, MediaMetadata> pair = 137 (Pair<PlaybackState, MediaMetadata>) (msg.obj); 138 inst.msgTrack(pair.first, pair.second); 139 break; 140 case MSG_AVRCP_PASSTHRU: 141 inst.msgPassThru((int) msg.obj); 142 break; 143 case MSG_AVRCP_GET_PLAY_STATUS_NATIVE: 144 inst.msgGetPlayStatusNative(); 145 break; 146 case MSG_DEVICE_BROWSE_CONNECT: 147 inst.msgDeviceBrowseConnect((BluetoothDevice) msg.obj); 148 break; 149 case MSG_DEVICE_BROWSE_DISCONNECT: 150 inst.msgDeviceBrowseDisconnect((BluetoothDevice) msg.obj); 151 break; 152 case MSG_FOLDER_LIST: 153 inst.msgFolderList((Intent) msg.obj); 154 break; 155 default: 156 Log.e(TAG, "Message not handled " + msg); 157 } 158 } 159 } 160 161 @Override onCreate()162 public void onCreate() { 163 Log.d(TAG, "onCreate"); 164 super.onCreate(); 165 166 mSession = new MediaSession(this, TAG); 167 setSessionToken(mSession.getSessionToken()); 168 mSession.setCallback(mSessionCallbacks); 169 mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | 170 MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); 171 mSession.setActive(true); 172 mAvrcpCommandQueue = new AvrcpCommandQueueHandler(Looper.getMainLooper(), this); 173 174 refreshInitialPlayingState(); 175 176 IntentFilter filter = new IntentFilter(); 177 filter.addAction(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED); 178 filter.addAction(AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED); 179 filter.addAction(AvrcpControllerService.ACTION_TRACK_EVENT); 180 filter.addAction(AvrcpControllerService.ACTION_FOLDER_LIST); 181 registerReceiver(mBtReceiver, filter); 182 183 synchronized (this) { 184 mParentIdToRequestMap.clear(); 185 } 186 } 187 188 @Override onDestroy()189 public void onDestroy() { 190 Log.d(TAG, "onDestroy"); 191 mSession.release(); 192 unregisterReceiver(mBtReceiver); 193 super.onDestroy(); 194 } 195 196 @Override onGetRoot(String clientPackageName, int clientUid, Bundle rootHints)197 public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { 198 return new BrowserRoot(BrowseTree.ROOT, null); 199 } 200 201 @Override onLoadChildren( final String parentMediaId, final Result<List<MediaItem>> result)202 public synchronized void onLoadChildren( 203 final String parentMediaId, final Result<List<MediaItem>> result) { 204 if (mAvrcpCtrlSrvc == null) { 205 Log.e(TAG, "AVRCP not yet connected."); 206 result.sendResult(mEmptyList); 207 return; 208 } 209 210 Log.d(TAG, "onLoadChildren parentMediaId=" + parentMediaId); 211 if (!mAvrcpCtrlSrvc.getChildren(mA2dpDevice, parentMediaId, 0, 0xff)) { 212 result.sendResult(mEmptyList); 213 return; 214 } 215 216 // Since we are using this thread from a binder thread we should make sure that 217 // we synchronize against other such asynchronous calls. 218 synchronized (this) { 219 mParentIdToRequestMap.put(parentMediaId, result); 220 } 221 result.detach(); 222 } 223 224 @Override onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result)225 public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) { 226 } 227 228 // Media Session Stuff. 229 private MediaSession.Callback mSessionCallbacks = new MediaSession.Callback() { 230 @Override 231 public void onPlay() { 232 Log.d(TAG, "onPlay"); 233 mAvrcpCommandQueue.obtainMessage( 234 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PLAY).sendToTarget(); 235 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 236 } 237 238 @Override 239 public void onPause() { 240 Log.d(TAG, "onPause"); 241 mAvrcpCommandQueue.obtainMessage( 242 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE).sendToTarget(); 243 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 244 } 245 246 @Override 247 public void onSkipToNext() { 248 Log.d(TAG, "onSkipToNext"); 249 mAvrcpCommandQueue.obtainMessage( 250 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_FORWARD) 251 .sendToTarget(); 252 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 253 } 254 255 @Override 256 public void onSkipToPrevious() { 257 Log.d(TAG, "onSkipToPrevious"); 258 259 mAvrcpCommandQueue.obtainMessage( 260 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_BACKWARD) 261 .sendToTarget(); 262 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 263 } 264 265 @Override 266 public void onStop() { 267 Log.d(TAG, "onStop"); 268 mAvrcpCommandQueue.obtainMessage( 269 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_STOP) 270 .sendToTarget(); 271 } 272 273 @Override 274 public void onRewind() { 275 Log.d(TAG, "onRewind"); 276 mAvrcpCommandQueue.obtainMessage( 277 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_REWIND).sendToTarget(); 278 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 279 } 280 281 @Override 282 public void onFastForward() { 283 Log.d(TAG, "onFastForward"); 284 mAvrcpCommandQueue.obtainMessage( 285 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_FF).sendToTarget(); 286 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 287 } 288 289 @Override 290 public void onPlayFromMediaId(String mediaId, Bundle extras) { 291 synchronized (A2dpMediaBrowserService.this) { 292 // Play the item if possible. 293 mAvrcpCtrlSrvc.fetchAttrAndPlayItem(mA2dpDevice, mediaId); 294 295 // Since we request explicit playback here we should start the updates to UI. 296 mAvrcpCtrlSrvc.startAvrcpUpdates(); 297 } 298 299 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 300 } 301 302 // Support VOL UP and VOL DOWN events for PTS testing. 303 @Override 304 public void onCustomAction(String action, Bundle extras) { 305 Log.d(TAG, "onCustomAction " + action); 306 if (CUSTOM_ACTION_VOL_UP.equals(action)) { 307 mAvrcpCommandQueue.obtainMessage( 308 MSG_AVRCP_PASSTHRU, 309 AvrcpControllerService.PASS_THRU_CMD_ID_VOL_UP).sendToTarget(); 310 } else if (CUSTOM_ACTION_VOL_DN.equals(action)) { 311 mAvrcpCommandQueue.obtainMessage( 312 MSG_AVRCP_PASSTHRU, 313 AvrcpControllerService.PASS_THRU_CMD_ID_VOL_DOWN).sendToTarget(); 314 } else if (CUSTOM_ACTION_GET_PLAY_STATUS_NATIVE.equals(action)) { 315 mAvrcpCommandQueue.obtainMessage( 316 MSG_AVRCP_GET_PLAY_STATUS_NATIVE).sendToTarget(); 317 }else { 318 Log.w(TAG, "Custom action " + action + " not supported."); 319 } 320 } 321 }; 322 323 private BroadcastReceiver mBtReceiver = new BroadcastReceiver() { 324 @Override 325 public void onReceive(Context context, Intent intent) { 326 Log.d(TAG, "onReceive intent=" + intent); 327 String action = intent.getAction(); 328 BluetoothDevice btDev = 329 (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 330 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 331 332 if (BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { 333 Log.d(TAG, "handleConnectionStateChange: newState=" 334 + state + " btDev=" + btDev); 335 336 // Connected state will be handled when AVRCP BluetoothProfile gets connected. 337 if (state == BluetoothProfile.STATE_CONNECTED) { 338 mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_CONNECT, btDev).sendToTarget(); 339 } else if (state == BluetoothProfile.STATE_DISCONNECTED) { 340 // Set the playback state to unconnected. 341 mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_DISCONNECT, btDev).sendToTarget(); 342 // If we have been pushing updates via the session then stop sending them since 343 // we are not connected anymore. 344 if (mSession.isActive()) { 345 mSession.setActive(false); 346 } 347 } 348 } else if (AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED.equals( 349 action)) { 350 if (state == BluetoothProfile.STATE_CONNECTED) { 351 mAvrcpCommandQueue.obtainMessage( 352 MSG_DEVICE_BROWSE_CONNECT, btDev).sendToTarget(); 353 } else if (state == BluetoothProfile.STATE_DISCONNECTED) { 354 mAvrcpCommandQueue.obtainMessage( 355 MSG_DEVICE_BROWSE_DISCONNECT, btDev).sendToTarget(); 356 } 357 } else if (AvrcpControllerService.ACTION_TRACK_EVENT.equals(action)) { 358 PlaybackState pbb = 359 intent.getParcelableExtra(AvrcpControllerService.EXTRA_PLAYBACK); 360 MediaMetadata mmd = 361 intent.getParcelableExtra(AvrcpControllerService.EXTRA_METADATA); 362 mAvrcpCommandQueue.obtainMessage( 363 MSG_TRACK, new Pair<PlaybackState, MediaMetadata>(pbb, mmd)).sendToTarget(); 364 } else if (AvrcpControllerService.ACTION_FOLDER_LIST.equals(action)) { 365 mAvrcpCommandQueue.obtainMessage(MSG_FOLDER_LIST, intent).sendToTarget(); 366 } 367 } 368 }; 369 msgDeviceConnect(BluetoothDevice device)370 private synchronized void msgDeviceConnect(BluetoothDevice device) { 371 Log.d(TAG, "msgDeviceConnect"); 372 // We are connected to a new device via A2DP now. 373 mA2dpDevice = device; 374 mAvrcpCtrlSrvc = AvrcpControllerService.getAvrcpControllerService(); 375 if (mAvrcpCtrlSrvc == null) { 376 Log.e(TAG, "!!!AVRCP Controller cannot be null"); 377 return; 378 } 379 refreshInitialPlayingState(); 380 } 381 382 383 // Refresh the UI if we have a connected device and AVRCP is initialized. refreshInitialPlayingState()384 private synchronized void refreshInitialPlayingState() { 385 if (mA2dpDevice == null) { 386 Log.d(TAG, "device " + mA2dpDevice); 387 return; 388 } 389 390 List<BluetoothDevice> devices = mAvrcpCtrlSrvc.getConnectedDevices(); 391 if (devices.size() == 0) { 392 Log.w(TAG, "No devices connected yet"); 393 return; 394 } 395 396 if (mA2dpDevice != null && !mA2dpDevice.equals(devices.get(0))) { 397 Log.e(TAG, "A2dp device : " + mA2dpDevice + " avrcp device " + devices.get(0)); 398 return; 399 } 400 mA2dpDevice = devices.get(0); 401 402 PlaybackState playbackState = mAvrcpCtrlSrvc.getPlaybackState(mA2dpDevice); 403 // Add actions required for playback and rebuild the object. 404 PlaybackState.Builder pbb = new PlaybackState.Builder(playbackState); 405 playbackState = pbb.setActions(mTransportControlFlags).build(); 406 407 MediaMetadata mediaMetadata = mAvrcpCtrlSrvc.getMetaData(mA2dpDevice); 408 Log.d(TAG, "Media metadata " + mediaMetadata + " playback state " + playbackState); 409 mSession.setMetadata(mAvrcpCtrlSrvc.getMetaData(mA2dpDevice)); 410 mSession.setPlaybackState(playbackState); 411 } 412 msgDeviceDisconnect(BluetoothDevice device)413 private void msgDeviceDisconnect(BluetoothDevice device) { 414 Log.d(TAG, "msgDeviceDisconnect"); 415 if (mA2dpDevice == null) { 416 Log.w(TAG, "Already disconnected - nothing to do here."); 417 return; 418 } else if (!mA2dpDevice.equals(device)) { 419 Log.e(TAG, "Not the right device to disconnect current " + 420 mA2dpDevice + " dc " + device); 421 return; 422 } 423 424 // Unset the session. 425 PlaybackState.Builder pbb = new PlaybackState.Builder(); 426 pbb = pbb.setState(PlaybackState.STATE_ERROR, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 427 PLAYBACK_SPEED) 428 .setActions(mTransportControlFlags) 429 .setErrorMessage(getString(R.string.bluetooth_disconnected)); 430 mSession.setPlaybackState(pbb.build()); 431 432 // Set device to null. 433 mA2dpDevice = null; 434 mBrowseConnected = false; 435 } 436 msgTrack(PlaybackState pb, MediaMetadata mmd)437 private void msgTrack(PlaybackState pb, MediaMetadata mmd) { 438 Log.d(TAG, "msgTrack: playback: " + pb + " mmd: " + mmd); 439 // Log the current track position/content. 440 MediaController controller = mSession.getController(); 441 PlaybackState prevPS = controller.getPlaybackState(); 442 MediaMetadata prevMM = controller.getMetadata(); 443 444 if (prevPS != null) { 445 Log.d(TAG, "prevPS " + prevPS); 446 } 447 448 if (prevMM != null) { 449 String title = prevMM.getString(MediaMetadata.METADATA_KEY_TITLE); 450 long trackLen = prevMM.getLong(MediaMetadata.METADATA_KEY_DURATION); 451 Log.d(TAG, "prev MM title " + title + " track len " + trackLen); 452 } 453 454 if (mmd != null) { 455 Log.d(TAG, "msgTrack() mmd " + mmd.getDescription()); 456 mSession.setMetadata(mmd); 457 } 458 459 if (pb != null) { 460 Log.d(TAG, "msgTrack() playbackstate " + pb); 461 PlaybackState.Builder pbb = new PlaybackState.Builder(pb); 462 pb = pbb.setActions(mTransportControlFlags).build(); 463 mSession.setPlaybackState(pb); 464 465 // If we are now playing then we should start pushing updates via MediaSession so that 466 // external UI (such as SystemUI) can show the currently playing music. 467 if (pb.getState() == PlaybackState.STATE_PLAYING && !mSession.isActive()) { 468 mSession.setActive(true); 469 } 470 } 471 } 472 msgPassThru(int cmd)473 private synchronized void msgPassThru(int cmd) { 474 Log.d(TAG, "msgPassThru " + cmd); 475 if (mA2dpDevice == null) { 476 // We should have already disconnected - ignore this message. 477 Log.e(TAG, "Already disconnected ignoring."); 478 return; 479 } 480 481 // Send the pass through. 482 mAvrcpCtrlSrvc.sendPassThroughCmd( 483 mA2dpDevice, cmd, AvrcpControllerService.KEY_STATE_PRESSED); 484 mAvrcpCtrlSrvc.sendPassThroughCmd( 485 mA2dpDevice, cmd, AvrcpControllerService.KEY_STATE_RELEASED); 486 } 487 msgGetPlayStatusNative()488 private synchronized void msgGetPlayStatusNative() { 489 Log.d(TAG, "msgGetPlayStatusNative"); 490 if (mA2dpDevice == null) { 491 // We should have already disconnected - ignore this message. 492 Log.e(TAG, "Already disconnected ignoring."); 493 return; 494 } 495 496 // Ask for a non cached version. 497 mAvrcpCtrlSrvc.getPlaybackState(mA2dpDevice, false); 498 } 499 msgDeviceBrowseConnect(BluetoothDevice device)500 private void msgDeviceBrowseConnect(BluetoothDevice device) { 501 Log.d(TAG, "msgDeviceBrowseConnect device " + device); 502 // We should already be connected to this device over A2DP. 503 if (!device.equals(mA2dpDevice)) { 504 Log.e(TAG, "Browse connected over different device a2dp " + mA2dpDevice + 505 " browse " + device); 506 return; 507 } 508 mBrowseConnected = true; 509 } 510 msgFolderList(Intent intent)511 private void msgFolderList(Intent intent) { 512 // Parse the folder list for children list and id. 513 List<Parcelable> extraParcelableList = 514 (ArrayList<Parcelable>) intent.getParcelableArrayListExtra( 515 AvrcpControllerService.EXTRA_FOLDER_LIST); 516 List<MediaItem> folderList = new ArrayList<MediaItem>(); 517 for (Parcelable p : extraParcelableList) { 518 folderList.add((MediaItem) p); 519 } 520 521 String id = intent.getStringExtra(AvrcpControllerService.EXTRA_FOLDER_ID); 522 Log.d(TAG, "Parent: " + id + " Folder list: " + folderList); 523 synchronized (this) { 524 // If we have a result object then we should send the result back 525 // to client since it is blocking otherwise we may have gotten more items 526 // from remote device, hence let client know to fetch again. 527 Result<List<MediaItem>> results = mParentIdToRequestMap.remove(id); 528 if (results == null) { 529 Log.w(TAG, "Request no longer exists, notifying that children changed."); 530 notifyChildrenChanged(id); 531 } else { 532 results.sendResult(folderList); 533 } 534 } 535 } 536 msgDeviceBrowseDisconnect(BluetoothDevice device)537 private void msgDeviceBrowseDisconnect(BluetoothDevice device) { 538 Log.d(TAG, "msgDeviceBrowseDisconnect device " + device); 539 // Disconnect only if mA2dpDevice is non null 540 if (!device.equals(mA2dpDevice)) { 541 Log.w(TAG, "Browse disconnecting from different device a2dp " + mA2dpDevice + 542 " browse " + device); 543 return; 544 } 545 mBrowseConnected = false; 546 } 547 } 548