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