A2dpMediaBrowserService.java revision 4b491c2c874395c436949183bcbd84ebb2493131
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 // Message sent when AVRCP browse is connected. 82 private static final int MSG_DEVICE_BROWSE_CONNECT = 6; 83 // Message sent when AVRCP browse is disconnected. 84 private static final int MSG_DEVICE_BROWSE_DISCONNECT = 7; 85 // Message sent when folder list is fetched. 86 private static final int MSG_FOLDER_LIST = 9; 87 88 private MediaSession mSession; 89 private MediaMetadata mA2dpMetadata; 90 91 private AvrcpControllerService mAvrcpCtrlSrvc; 92 private boolean mBrowseConnected = false; 93 private BluetoothDevice mA2dpDevice = null; 94 private Handler mAvrcpCommandQueue; 95 private final Map<String, Result<List<MediaItem>>> mParentIdToRequestMap = new HashMap<>(); 96 private static final List<MediaItem> mEmptyList = new ArrayList<MediaItem>(); 97 98 // Browsing related structures. 99 private List<MediaItem> mNowPlayingList = null; 100 101 private long mTransportControlFlags = PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY 102 | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS; 103 104 private static final class AvrcpCommandQueueHandler extends Handler { 105 WeakReference<A2dpMediaBrowserService> mInst; 106 107 AvrcpCommandQueueHandler(Looper looper, A2dpMediaBrowserService sink) { 108 super(looper); 109 mInst = new WeakReference<A2dpMediaBrowserService>(sink); 110 } 111 112 @Override 113 public void handleMessage(Message msg) { 114 A2dpMediaBrowserService inst = mInst.get(); 115 if (inst == null) { 116 Log.e(TAG, "Parent class has died; aborting."); 117 return; 118 } 119 120 switch (msg.what) { 121 case MSG_DEVICE_CONNECT: 122 inst.msgDeviceConnect((BluetoothDevice) msg.obj); 123 break; 124 case MSG_DEVICE_DISCONNECT: 125 inst.msgDeviceDisconnect((BluetoothDevice) msg.obj); 126 break; 127 case MSG_TRACK: 128 Pair<PlaybackState, MediaMetadata> pair = 129 (Pair<PlaybackState, MediaMetadata>) (msg.obj); 130 inst.msgTrack(pair.first, pair.second); 131 break; 132 case MSG_AVRCP_PASSTHRU: 133 inst.msgPassThru((int) msg.obj); 134 break; 135 case MSG_DEVICE_BROWSE_CONNECT: 136 inst.msgDeviceBrowseConnect((BluetoothDevice) msg.obj); 137 break; 138 case MSG_DEVICE_BROWSE_DISCONNECT: 139 inst.msgDeviceBrowseDisconnect((BluetoothDevice) msg.obj); 140 break; 141 case MSG_FOLDER_LIST: 142 inst.msgFolderList((Intent) msg.obj); 143 break; 144 default: 145 Log.e(TAG, "Message not handled " + msg); 146 } 147 } 148 } 149 150 @Override 151 public void onCreate() { 152 Log.d(TAG, "onCreate"); 153 super.onCreate(); 154 155 mSession = new MediaSession(this, TAG); 156 setSessionToken(mSession.getSessionToken()); 157 mSession.setCallback(mSessionCallbacks); 158 mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | 159 MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); 160 mAvrcpCommandQueue = new AvrcpCommandQueueHandler(Looper.getMainLooper(), this); 161 162 refreshInitialPlayingState(); 163 164 IntentFilter filter = new IntentFilter(); 165 filter.addAction(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED); 166 filter.addAction(AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED); 167 filter.addAction(AvrcpControllerService.ACTION_TRACK_EVENT); 168 filter.addAction(AvrcpControllerService.ACTION_FOLDER_LIST); 169 registerReceiver(mBtReceiver, filter); 170 171 synchronized (this) { 172 mParentIdToRequestMap.clear(); 173 } 174 } 175 176 @Override 177 public void onDestroy() { 178 Log.d(TAG, "onDestroy"); 179 mSession.release(); 180 unregisterReceiver(mBtReceiver); 181 super.onDestroy(); 182 } 183 184 @Override 185 public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { 186 return new BrowserRoot(BrowseTree.ROOT, null); 187 } 188 189 @Override 190 public synchronized void onLoadChildren( 191 final String parentMediaId, final Result<List<MediaItem>> result) { 192 if (mAvrcpCtrlSrvc == null) { 193 Log.e(TAG, "AVRCP not yet connected."); 194 result.sendResult(mEmptyList); 195 return; 196 } 197 198 Log.d(TAG, "onLoadChildren parentMediaId=" + parentMediaId); 199 mAvrcpCtrlSrvc.getChildren(mA2dpDevice, parentMediaId, 0, 0xff); 200 201 // Since we are using this thread from a binder thread we should make sure that 202 // we synchronize against other such asynchronous calls. 203 synchronized (this) { 204 mParentIdToRequestMap.put(parentMediaId, result); 205 } 206 result.detach(); 207 } 208 209 @Override 210 public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) { 211 } 212 213 // Media Session Stuff. 214 private MediaSession.Callback mSessionCallbacks = new MediaSession.Callback() { 215 @Override 216 public void onPlay() { 217 Log.d(TAG, "onPlay"); 218 mAvrcpCommandQueue.obtainMessage( 219 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PLAY).sendToTarget(); 220 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 221 } 222 223 @Override 224 public void onPause() { 225 Log.d(TAG, "onPause"); 226 mAvrcpCommandQueue.obtainMessage( 227 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE).sendToTarget(); 228 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 229 } 230 231 @Override 232 public void onSkipToNext() { 233 Log.d(TAG, "onSkipToNext"); 234 mAvrcpCommandQueue.obtainMessage( 235 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_FORWARD) 236 .sendToTarget(); 237 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 238 } 239 240 @Override 241 public void onSkipToPrevious() { 242 Log.d(TAG, "onSkipToPrevious"); 243 244 mAvrcpCommandQueue.obtainMessage( 245 MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_BACKWARD) 246 .sendToTarget(); 247 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 248 } 249 250 // These are not yet supported. 251 @Override 252 public void onStop() { 253 Log.d(TAG, "onStop"); 254 } 255 256 @Override 257 public void onCustomAction(String action, Bundle extras) { 258 Log.d(TAG, "onCustomAction action=" + action + " extras=" + extras); 259 } 260 261 @Override 262 public void onPlayFromSearch(String query, Bundle extras) { 263 Log.d(TAG, "playFromSearch not supported in AVRCP"); 264 } 265 266 @Override 267 public void onCommand(String command, Bundle args, ResultReceiver cb) { 268 Log.d(TAG, "onCommand command=" + command + " args=" + args); 269 } 270 271 @Override 272 public void onSkipToQueueItem(long queueId) { 273 Log.d(TAG, "onSkipToQueueItem"); 274 } 275 276 @Override 277 public void onPlayFromMediaId(String mediaId, Bundle extras) { 278 synchronized (A2dpMediaBrowserService.this) { 279 // Play the item if possible. 280 mAvrcpCtrlSrvc.fetchAttrAndPlayItem(mA2dpDevice, mediaId); 281 282 // Since we request explicit playback here we should start the updates to UI. 283 mAvrcpCtrlSrvc.startAvrcpUpdates(); 284 } 285 286 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 287 } 288 }; 289 290 private BroadcastReceiver mBtReceiver = new BroadcastReceiver() { 291 @Override 292 public void onReceive(Context context, Intent intent) { 293 Log.d(TAG, "onReceive intent=" + intent); 294 String action = intent.getAction(); 295 BluetoothDevice btDev = 296 (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 297 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 298 299 if (BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { 300 Log.d(TAG, "handleConnectionStateChange: newState=" 301 + state + " btDev=" + btDev); 302 303 // Connected state will be handled when AVRCP BluetoothProfile gets connected. 304 if (state == BluetoothProfile.STATE_CONNECTED) { 305 mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_CONNECT, btDev).sendToTarget(); 306 } else if (state == BluetoothProfile.STATE_DISCONNECTED) { 307 // Set the playback state to unconnected. 308 mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_DISCONNECT, btDev).sendToTarget(); 309 // If we have been pushing updates via the session then stop sending them since 310 // we are not connected anymore. 311 if (mSession.isActive()) { 312 mSession.setActive(false); 313 } 314 } 315 } else if (AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED.equals( 316 action)) { 317 if (state == BluetoothProfile.STATE_CONNECTED) { 318 mAvrcpCommandQueue.obtainMessage( 319 MSG_DEVICE_BROWSE_CONNECT, btDev).sendToTarget(); 320 } else if (state == BluetoothProfile.STATE_DISCONNECTED) { 321 mAvrcpCommandQueue.obtainMessage( 322 MSG_DEVICE_BROWSE_DISCONNECT, btDev).sendToTarget(); 323 } 324 } else if (AvrcpControllerService.ACTION_TRACK_EVENT.equals(action)) { 325 PlaybackState pbb = 326 intent.getParcelableExtra(AvrcpControllerService.EXTRA_PLAYBACK); 327 MediaMetadata mmd = 328 intent.getParcelableExtra(AvrcpControllerService.EXTRA_METADATA); 329 mAvrcpCommandQueue.obtainMessage( 330 MSG_TRACK, new Pair<PlaybackState, MediaMetadata>(pbb, mmd)).sendToTarget(); 331 } else if (AvrcpControllerService.ACTION_FOLDER_LIST.equals(action)) { 332 mAvrcpCommandQueue.obtainMessage(MSG_FOLDER_LIST, intent).sendToTarget(); 333 } 334 } 335 }; 336 337 private synchronized void msgDeviceConnect(BluetoothDevice device) { 338 Log.d(TAG, "msgDeviceConnect"); 339 // We are connected to a new device via A2DP now. 340 mA2dpDevice = device; 341 mAvrcpCtrlSrvc = AvrcpControllerService.getAvrcpControllerService(); 342 if (mAvrcpCtrlSrvc == null) { 343 Log.e(TAG, "!!!AVRCP Controller cannot be null"); 344 return; 345 } 346 refreshInitialPlayingState(); 347 } 348 349 350 // Refresh the UI if we have a connected device and AVRCP is initialized. 351 private synchronized void refreshInitialPlayingState() { 352 if (mA2dpDevice == null) { 353 Log.d(TAG, "device " + mA2dpDevice); 354 return; 355 } 356 357 List<BluetoothDevice> devices = mAvrcpCtrlSrvc.getConnectedDevices(); 358 if (devices.size() == 0) { 359 Log.w(TAG, "No devices connected yet"); 360 return; 361 } 362 363 if (mA2dpDevice != null && !mA2dpDevice.equals(devices.get(0))) { 364 Log.e(TAG, "A2dp device : " + mA2dpDevice + " avrcp device " + devices.get(0)); 365 return; 366 } 367 mA2dpDevice = devices.get(0); 368 369 PlaybackState playbackState = mAvrcpCtrlSrvc.getPlaybackState(mA2dpDevice); 370 // Add actions required for playback and rebuild the object. 371 PlaybackState.Builder pbb = new PlaybackState.Builder(playbackState); 372 playbackState = pbb.setActions(mTransportControlFlags).build(); 373 374 MediaMetadata mediaMetadata = mAvrcpCtrlSrvc.getMetaData(mA2dpDevice); 375 Log.d(TAG, "Media metadata " + mediaMetadata + " playback state " + playbackState); 376 mSession.setMetadata(mAvrcpCtrlSrvc.getMetaData(mA2dpDevice)); 377 mSession.setPlaybackState(playbackState); 378 } 379 380 private void msgDeviceDisconnect(BluetoothDevice device) { 381 Log.d(TAG, "msgDeviceDisconnect"); 382 if (mA2dpDevice == null) { 383 Log.w(TAG, "Already disconnected - nothing to do here."); 384 return; 385 } else if (!mA2dpDevice.equals(device)) { 386 Log.e(TAG, "Not the right device to disconnect current " + 387 mA2dpDevice + " dc " + device); 388 return; 389 } 390 391 // Unset the session. 392 PlaybackState.Builder pbb = new PlaybackState.Builder(); 393 pbb = pbb.setState(PlaybackState.STATE_ERROR, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 394 PLAYBACK_SPEED) 395 .setActions(mTransportControlFlags) 396 .setErrorMessage(getString(R.string.bluetooth_disconnected)); 397 mSession.setPlaybackState(pbb.build()); 398 399 // Set device to null. 400 mA2dpDevice = null; 401 mBrowseConnected = false; 402 } 403 404 private void msgTrack(PlaybackState pb, MediaMetadata mmd) { 405 Log.d(TAG, "msgTrack: playback: " + pb + " mmd: " + mmd); 406 // Log the current track position/content. 407 MediaController controller = mSession.getController(); 408 PlaybackState prevPS = controller.getPlaybackState(); 409 MediaMetadata prevMM = controller.getMetadata(); 410 411 if (prevPS != null) { 412 Log.d(TAG, "prevPS " + prevPS); 413 } 414 415 if (prevMM != null) { 416 String title = prevMM.getString(MediaMetadata.METADATA_KEY_TITLE); 417 long trackLen = prevMM.getLong(MediaMetadata.METADATA_KEY_DURATION); 418 Log.d(TAG, "prev MM title " + title + " track len " + trackLen); 419 } 420 421 if (mmd != null) { 422 Log.d(TAG, "msgTrack() mmd " + mmd.getDescription()); 423 mSession.setMetadata(mmd); 424 } 425 426 if (pb != null) { 427 Log.d(TAG, "msgTrack() playbackstate " + pb); 428 PlaybackState.Builder pbb = new PlaybackState.Builder(pb); 429 pb = pbb.setActions(mTransportControlFlags).build(); 430 mSession.setPlaybackState(pb); 431 432 // If we are now playing then we should start pushing updates via MediaSession so that 433 // external UI (such as SystemUI) can show the currently playing music. 434 if (pb.getState() == PlaybackState.STATE_PLAYING && !mSession.isActive()) { 435 mSession.setActive(true); 436 } 437 } 438 } 439 440 private synchronized void msgPassThru(int cmd) { 441 Log.d(TAG, "msgPassThru " + cmd); 442 if (mA2dpDevice == null) { 443 // We should have already disconnected - ignore this message. 444 Log.e(TAG, "Already disconnected ignoring."); 445 return; 446 } 447 448 // Send the pass through. 449 mAvrcpCtrlSrvc.sendPassThroughCmd( 450 mA2dpDevice, cmd, AvrcpControllerService.KEY_STATE_PRESSED); 451 mAvrcpCtrlSrvc.sendPassThroughCmd( 452 mA2dpDevice, cmd, AvrcpControllerService.KEY_STATE_RELEASED); 453 } 454 455 private void msgDeviceBrowseConnect(BluetoothDevice device) { 456 Log.d(TAG, "msgDeviceBrowseConnect device " + device); 457 // We should already be connected to this device over A2DP. 458 if (!device.equals(mA2dpDevice)) { 459 Log.e(TAG, "Browse connected over different device a2dp " + mA2dpDevice + 460 " browse " + device); 461 return; 462 } 463 mBrowseConnected = true; 464 } 465 466 private void msgFolderList(Intent intent) { 467 // Parse the folder list for children list and id. 468 List<Parcelable> extraParcelableList = 469 (ArrayList<Parcelable>) intent.getParcelableArrayListExtra( 470 AvrcpControllerService.EXTRA_FOLDER_LIST); 471 List<MediaItem> folderList = new ArrayList<MediaItem>(); 472 for (Parcelable p : extraParcelableList) { 473 folderList.add((MediaItem) p); 474 } 475 476 String id = intent.getStringExtra(AvrcpControllerService.EXTRA_FOLDER_ID); 477 Log.d(TAG, "Parent: " + id + " Folder list: " + folderList); 478 synchronized (this) { 479 Result<List<MediaItem>> results = mParentIdToRequestMap.remove(id); 480 if (results == null) { 481 Log.w(TAG, "Request no longer exists, hence ignoring reply!"); 482 return; 483 } 484 results.sendResult(folderList); 485 } 486 } 487 488 private void msgDeviceBrowseDisconnect(BluetoothDevice device) { 489 Log.d(TAG, "msgDeviceBrowseDisconnect device " + device); 490 // Disconnect only if mA2dpDevice is non null 491 if (!device.equals(mA2dpDevice)) { 492 Log.w(TAG, "Browse disconnecting from different device a2dp " + mA2dpDevice + 493 " browse " + device); 494 return; 495 } 496 mBrowseConnected = false; 497 } 498} 499