A2dpMediaBrowserService.java revision ea14e69d8b1c7293869c0e4ae9180f36149a95b7
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 17package com.android.bluetooth.a2dpsink.mbs; 18 19import android.bluetooth.BluetoothAdapter; 20import android.bluetooth.BluetoothAvrcpController; 21import android.bluetooth.BluetoothDevice; 22import android.bluetooth.BluetoothProfile; 23import android.content.BroadcastReceiver; 24import android.content.Context; 25import android.content.Intent; 26import android.content.IntentFilter; 27import android.media.browse.MediaBrowser; 28import android.media.browse.MediaBrowser.MediaItem; 29import android.media.MediaDescription; 30import android.media.MediaMetadata; 31import android.media.session.MediaController; 32import android.media.session.MediaSession; 33import android.media.session.PlaybackState; 34import android.os.Bundle; 35import android.os.Handler; 36import android.os.Looper; 37import android.os.Message; 38import android.os.Parcelable; 39import android.os.ResultReceiver; 40import android.service.media.MediaBrowserService; 41import android.util.Pair; 42import android.util.Log; 43 44import com.android.bluetooth.R; 45import com.android.bluetooth.avrcpcontroller.AvrcpControllerService; 46import com.android.bluetooth.avrcpcontroller.BrowseTree; 47 48import java.lang.ref.WeakReference; 49import java.util.ArrayList; 50import java.util.HashMap; 51import java.util.List; 52import 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 */ 68public 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 115 AvrcpCommandQueueHandler(Looper looper, A2dpMediaBrowserService sink) { 116 super(looper); 117 mInst = new WeakReference<A2dpMediaBrowserService>(sink); 118 } 119 120 @Override 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 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 189 public void onDestroy() { 190 Log.d(TAG, "onDestroy"); 191 mSession.release(); 192 unregisterReceiver(mBtReceiver); 193 super.onDestroy(); 194 } 195 196 @Override 197 public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { 198 return new BrowserRoot(BrowseTree.ROOT, null); 199 } 200 201 @Override 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 mAvrcpCtrlSrvc.getChildren(mA2dpDevice, parentMediaId, 0, 0xff); 212 213 // Since we are using this thread from a binder thread we should make sure that 214 // we synchronize against other such asynchronous calls. 215 synchronized (this) { 216 mParentIdToRequestMap.put(parentMediaId, result); 217 } 218 result.detach(); 219 } 220 221 @Override 222 public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) { 223 } 224 225 // Media Session Stuff. 226 private MediaSession.Callback mSessionCallbacks = new MediaSession.Callback() { 227 @Override 228 public void onPlay() { 229 Log.d(TAG, "onPlay"); 230 mAvrcpCommandQueue.obtainMessage( 231 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PLAY).sendToTarget(); 232 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 233 } 234 235 @Override 236 public void onPause() { 237 Log.d(TAG, "onPause"); 238 mAvrcpCommandQueue.obtainMessage( 239 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE).sendToTarget(); 240 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 241 } 242 243 @Override 244 public void onSkipToNext() { 245 Log.d(TAG, "onSkipToNext"); 246 mAvrcpCommandQueue.obtainMessage( 247 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_FORWARD) 248 .sendToTarget(); 249 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 250 } 251 252 @Override 253 public void onSkipToPrevious() { 254 Log.d(TAG, "onSkipToPrevious"); 255 256 mAvrcpCommandQueue.obtainMessage( 257 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_BACKWARD) 258 .sendToTarget(); 259 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 260 } 261 262 @Override 263 public void onStop() { 264 Log.d(TAG, "onStop"); 265 mAvrcpCommandQueue.obtainMessage( 266 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_STOP) 267 .sendToTarget(); 268 } 269 270 @Override 271 public void onRewind() { 272 Log.d(TAG, "onRewind"); 273 mAvrcpCommandQueue.obtainMessage( 274 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_REWIND).sendToTarget(); 275 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 276 } 277 278 @Override 279 public void onFastForward() { 280 Log.d(TAG, "onFastForward"); 281 mAvrcpCommandQueue.obtainMessage( 282 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_FF).sendToTarget(); 283 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 284 } 285 286 @Override 287 public void onPlayFromMediaId(String mediaId, Bundle extras) { 288 synchronized (A2dpMediaBrowserService.this) { 289 // Play the item if possible. 290 mAvrcpCtrlSrvc.fetchAttrAndPlayItem(mA2dpDevice, mediaId); 291 292 // Since we request explicit playback here we should start the updates to UI. 293 mAvrcpCtrlSrvc.startAvrcpUpdates(); 294 } 295 296 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 297 } 298 299 // Support VOL UP and VOL DOWN events for PTS testing. 300 @Override 301 public void onCustomAction(String action, Bundle extras) { 302 Log.d(TAG, "onCustomAction " + action); 303 if (CUSTOM_ACTION_VOL_UP.equals(action)) { 304 mAvrcpCommandQueue.obtainMessage( 305 MSG_AVRCP_PASSTHRU, 306 AvrcpControllerService.PASS_THRU_CMD_ID_VOL_UP).sendToTarget(); 307 } else if (CUSTOM_ACTION_VOL_DN.equals(action)) { 308 mAvrcpCommandQueue.obtainMessage( 309 MSG_AVRCP_PASSTHRU, 310 AvrcpControllerService.PASS_THRU_CMD_ID_VOL_DOWN).sendToTarget(); 311 } else if (CUSTOM_ACTION_GET_PLAY_STATUS_NATIVE.equals(action)) { 312 mAvrcpCommandQueue.obtainMessage( 313 MSG_AVRCP_GET_PLAY_STATUS_NATIVE).sendToTarget(); 314 }else { 315 Log.w(TAG, "Custom action " + action + " not supported."); 316 } 317 } 318 }; 319 320 private BroadcastReceiver mBtReceiver = new BroadcastReceiver() { 321 @Override 322 public void onReceive(Context context, Intent intent) { 323 Log.d(TAG, "onReceive intent=" + intent); 324 String action = intent.getAction(); 325 BluetoothDevice btDev = 326 (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 327 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 328 329 if (BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { 330 Log.d(TAG, "handleConnectionStateChange: newState=" 331 + state + " btDev=" + btDev); 332 333 // Connected state will be handled when AVRCP BluetoothProfile gets connected. 334 if (state == BluetoothProfile.STATE_CONNECTED) { 335 mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_CONNECT, btDev).sendToTarget(); 336 } else if (state == BluetoothProfile.STATE_DISCONNECTED) { 337 // Set the playback state to unconnected. 338 mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_DISCONNECT, btDev).sendToTarget(); 339 // If we have been pushing updates via the session then stop sending them since 340 // we are not connected anymore. 341 if (mSession.isActive()) { 342 mSession.setActive(false); 343 } 344 } 345 } else if (AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED.equals( 346 action)) { 347 if (state == BluetoothProfile.STATE_CONNECTED) { 348 mAvrcpCommandQueue.obtainMessage( 349 MSG_DEVICE_BROWSE_CONNECT, btDev).sendToTarget(); 350 } else if (state == BluetoothProfile.STATE_DISCONNECTED) { 351 mAvrcpCommandQueue.obtainMessage( 352 MSG_DEVICE_BROWSE_DISCONNECT, btDev).sendToTarget(); 353 } 354 } else if (AvrcpControllerService.ACTION_TRACK_EVENT.equals(action)) { 355 PlaybackState pbb = 356 intent.getParcelableExtra(AvrcpControllerService.EXTRA_PLAYBACK); 357 MediaMetadata mmd = 358 intent.getParcelableExtra(AvrcpControllerService.EXTRA_METADATA); 359 mAvrcpCommandQueue.obtainMessage( 360 MSG_TRACK, new Pair<PlaybackState, MediaMetadata>(pbb, mmd)).sendToTarget(); 361 } else if (AvrcpControllerService.ACTION_FOLDER_LIST.equals(action)) { 362 mAvrcpCommandQueue.obtainMessage(MSG_FOLDER_LIST, intent).sendToTarget(); 363 } 364 } 365 }; 366 367 private synchronized void msgDeviceConnect(BluetoothDevice device) { 368 Log.d(TAG, "msgDeviceConnect"); 369 // We are connected to a new device via A2DP now. 370 mA2dpDevice = device; 371 mAvrcpCtrlSrvc = AvrcpControllerService.getAvrcpControllerService(); 372 if (mAvrcpCtrlSrvc == null) { 373 Log.e(TAG, "!!!AVRCP Controller cannot be null"); 374 return; 375 } 376 refreshInitialPlayingState(); 377 } 378 379 380 // Refresh the UI if we have a connected device and AVRCP is initialized. 381 private synchronized void refreshInitialPlayingState() { 382 if (mA2dpDevice == null) { 383 Log.d(TAG, "device " + mA2dpDevice); 384 return; 385 } 386 387 List<BluetoothDevice> devices = mAvrcpCtrlSrvc.getConnectedDevices(); 388 if (devices.size() == 0) { 389 Log.w(TAG, "No devices connected yet"); 390 return; 391 } 392 393 if (mA2dpDevice != null && !mA2dpDevice.equals(devices.get(0))) { 394 Log.e(TAG, "A2dp device : " + mA2dpDevice + " avrcp device " + devices.get(0)); 395 return; 396 } 397 mA2dpDevice = devices.get(0); 398 399 PlaybackState playbackState = mAvrcpCtrlSrvc.getPlaybackState(mA2dpDevice); 400 // Add actions required for playback and rebuild the object. 401 PlaybackState.Builder pbb = new PlaybackState.Builder(playbackState); 402 playbackState = pbb.setActions(mTransportControlFlags).build(); 403 404 MediaMetadata mediaMetadata = mAvrcpCtrlSrvc.getMetaData(mA2dpDevice); 405 Log.d(TAG, "Media metadata " + mediaMetadata + " playback state " + playbackState); 406 mSession.setMetadata(mAvrcpCtrlSrvc.getMetaData(mA2dpDevice)); 407 mSession.setPlaybackState(playbackState); 408 } 409 410 private void msgDeviceDisconnect(BluetoothDevice device) { 411 Log.d(TAG, "msgDeviceDisconnect"); 412 if (mA2dpDevice == null) { 413 Log.w(TAG, "Already disconnected - nothing to do here."); 414 return; 415 } else if (!mA2dpDevice.equals(device)) { 416 Log.e(TAG, "Not the right device to disconnect current " + 417 mA2dpDevice + " dc " + device); 418 return; 419 } 420 421 // Unset the session. 422 PlaybackState.Builder pbb = new PlaybackState.Builder(); 423 pbb = pbb.setState(PlaybackState.STATE_ERROR, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 424 PLAYBACK_SPEED) 425 .setActions(mTransportControlFlags) 426 .setErrorMessage(getString(R.string.bluetooth_disconnected)); 427 mSession.setPlaybackState(pbb.build()); 428 429 // Set device to null. 430 mA2dpDevice = null; 431 mBrowseConnected = false; 432 } 433 434 private void msgTrack(PlaybackState pb, MediaMetadata mmd) { 435 Log.d(TAG, "msgTrack: playback: " + pb + " mmd: " + mmd); 436 // Log the current track position/content. 437 MediaController controller = mSession.getController(); 438 PlaybackState prevPS = controller.getPlaybackState(); 439 MediaMetadata prevMM = controller.getMetadata(); 440 441 if (prevPS != null) { 442 Log.d(TAG, "prevPS " + prevPS); 443 } 444 445 if (prevMM != null) { 446 String title = prevMM.getString(MediaMetadata.METADATA_KEY_TITLE); 447 long trackLen = prevMM.getLong(MediaMetadata.METADATA_KEY_DURATION); 448 Log.d(TAG, "prev MM title " + title + " track len " + trackLen); 449 } 450 451 if (mmd != null) { 452 Log.d(TAG, "msgTrack() mmd " + mmd.getDescription()); 453 mSession.setMetadata(mmd); 454 } 455 456 if (pb != null) { 457 Log.d(TAG, "msgTrack() playbackstate " + pb); 458 PlaybackState.Builder pbb = new PlaybackState.Builder(pb); 459 pb = pbb.setActions(mTransportControlFlags).build(); 460 mSession.setPlaybackState(pb); 461 462 // If we are now playing then we should start pushing updates via MediaSession so that 463 // external UI (such as SystemUI) can show the currently playing music. 464 if (pb.getState() == PlaybackState.STATE_PLAYING && !mSession.isActive()) { 465 mSession.setActive(true); 466 } 467 } 468 } 469 470 private synchronized void msgPassThru(int cmd) { 471 Log.d(TAG, "msgPassThru " + cmd); 472 if (mA2dpDevice == null) { 473 // We should have already disconnected - ignore this message. 474 Log.e(TAG, "Already disconnected ignoring."); 475 return; 476 } 477 478 // Send the pass through. 479 mAvrcpCtrlSrvc.sendPassThroughCmd( 480 mA2dpDevice, cmd, AvrcpControllerService.KEY_STATE_PRESSED); 481 mAvrcpCtrlSrvc.sendPassThroughCmd( 482 mA2dpDevice, cmd, AvrcpControllerService.KEY_STATE_RELEASED); 483 } 484 485 private synchronized void msgGetPlayStatusNative() { 486 Log.d(TAG, "msgGetPlayStatusNative"); 487 if (mA2dpDevice == null) { 488 // We should have already disconnected - ignore this message. 489 Log.e(TAG, "Already disconnected ignoring."); 490 return; 491 } 492 493 // Ask for a non cached version. 494 mAvrcpCtrlSrvc.getPlaybackState(mA2dpDevice, false); 495 } 496 497 private void msgDeviceBrowseConnect(BluetoothDevice device) { 498 Log.d(TAG, "msgDeviceBrowseConnect device " + device); 499 // We should already be connected to this device over A2DP. 500 if (!device.equals(mA2dpDevice)) { 501 Log.e(TAG, "Browse connected over different device a2dp " + mA2dpDevice + 502 " browse " + device); 503 return; 504 } 505 mBrowseConnected = true; 506 } 507 508 private void msgFolderList(Intent intent) { 509 // Parse the folder list for children list and id. 510 List<Parcelable> extraParcelableList = 511 (ArrayList<Parcelable>) intent.getParcelableArrayListExtra( 512 AvrcpControllerService.EXTRA_FOLDER_LIST); 513 List<MediaItem> folderList = new ArrayList<MediaItem>(); 514 for (Parcelable p : extraParcelableList) { 515 folderList.add((MediaItem) p); 516 } 517 518 String id = intent.getStringExtra(AvrcpControllerService.EXTRA_FOLDER_ID); 519 Log.d(TAG, "Parent: " + id + " Folder list: " + folderList); 520 synchronized (this) { 521 // If we have a result object then we should send the result back 522 // to client since it is blocking otherwise we may have gotten more items 523 // from remote device, hence let client know to fetch again. 524 Result<List<MediaItem>> results = mParentIdToRequestMap.remove(id); 525 if (results == null) { 526 Log.w(TAG, "Request no longer exists, notifying that children changed."); 527 notifyChildrenChanged(id); 528 } else { 529 results.sendResult(folderList); 530 } 531 } 532 } 533 534 private void msgDeviceBrowseDisconnect(BluetoothDevice device) { 535 Log.d(TAG, "msgDeviceBrowseDisconnect device " + device); 536 // Disconnect only if mA2dpDevice is non null 537 if (!device.equals(mA2dpDevice)) { 538 Log.w(TAG, "Browse disconnecting from different device a2dp " + mA2dpDevice + 539 " browse " + device); 540 return; 541 } 542 mBrowseConnected = false; 543 } 544} 545