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.MediaMetadata; 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.ResultReceiver; 37 import android.service.media.MediaBrowserService; 38 import android.util.Pair; 39 import android.util.Log; 40 41 import com.android.bluetooth.R; 42 43 import java.lang.ref.WeakReference; 44 import java.util.ArrayList; 45 import java.util.List; 46 47 public class A2dpMediaBrowserService extends MediaBrowserService { 48 private static final String TAG = "A2dpMediaBrowserService"; 49 private static final String MEDIA_ID_ROOT = "__ROOT__"; 50 private static final String UNKNOWN_BT_AUDIO = "__UNKNOWN_BT_AUDIO__"; 51 private static final float PLAYBACK_SPEED = 1.0f; 52 53 // Message sent when A2DP device is disconnected. 54 private static final int MSG_DEVICE_DISCONNECT = 0; 55 // Message snet when the AVRCP profile is disconnected = 1; 56 private static final int MSG_PROFILE_DISCONNECT = 1; 57 // Message sent when A2DP device is connected. 58 private static final int MSG_DEVICE_CONNECT = 2; 59 // Message sent when AVRCP profile is connected (note AVRCP profile may be connected before or 60 // after A2DP device is connected). 61 private static final int MSG_PROFILE_CONNECT = 3; 62 // Message sent when we recieve a TRACK update from AVRCP profile over a connected A2DP device. 63 private static final int MSG_TRACK = 4; 64 // Internal message sent to trigger a AVRCP action. 65 private static final int MSG_AVRCP_PASSTHRU = 5; 66 67 private MediaSession mSession; 68 private MediaMetadata mA2dpMetadata; 69 70 private BluetoothAdapter mAdapter; 71 private BluetoothAvrcpController mAvrcpProfile; 72 private BluetoothDevice mA2dpDevice = null; 73 private Handler mAvrcpCommandQueue; 74 75 private long mTransportControlFlags = PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY 76 | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS; 77 78 private static final class AvrcpCommandQueueHandler extends Handler { 79 WeakReference<A2dpMediaBrowserService> mInst; 80 AvrcpCommandQueueHandler(Looper looper, A2dpMediaBrowserService sink)81 AvrcpCommandQueueHandler(Looper looper, A2dpMediaBrowserService sink) { 82 super(looper); 83 mInst = new WeakReference<A2dpMediaBrowserService>(sink); 84 } 85 86 @Override handleMessage(Message msg)87 public void handleMessage(Message msg) { 88 A2dpMediaBrowserService inst = mInst.get(); 89 if (inst == null) { 90 Log.e(TAG, "Parent class has died; aborting."); 91 return; 92 } 93 94 switch (msg.what) { 95 case MSG_DEVICE_CONNECT: 96 inst.msgDeviceConnect((BluetoothDevice) msg.obj); 97 break; 98 case MSG_PROFILE_CONNECT: 99 inst.msgProfileConnect((BluetoothProfile) msg.obj); 100 break; 101 case MSG_DEVICE_DISCONNECT: 102 inst.msgDeviceDisconnect((BluetoothDevice) msg.obj); 103 break; 104 case MSG_PROFILE_DISCONNECT: 105 inst.msgProfileDisconnect(); 106 break; 107 case MSG_TRACK: 108 Pair<PlaybackState, MediaMetadata> pair = 109 (Pair<PlaybackState, MediaMetadata>) (msg.obj); 110 inst.msgTrack(pair.first, pair.second); 111 break; 112 case MSG_AVRCP_PASSTHRU: 113 inst.msgPassThru((int) msg.obj); 114 break; 115 } 116 } 117 } 118 119 @Override onCreate()120 public void onCreate() { 121 Log.d(TAG, "onCreate"); 122 super.onCreate(); 123 mSession = new MediaSession(this, TAG); 124 setSessionToken(mSession.getSessionToken()); 125 mSession.setCallback(mSessionCallbacks); 126 mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | 127 MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); 128 mAvrcpCommandQueue = new AvrcpCommandQueueHandler(Looper.getMainLooper(), this); 129 130 mAdapter = BluetoothAdapter.getDefaultAdapter(); 131 mAdapter.getProfileProxy(this, mServiceListener, BluetoothProfile.AVRCP_CONTROLLER); 132 133 IntentFilter filter = new IntentFilter(); 134 filter.addAction(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED); 135 filter.addAction(BluetoothAvrcpController.ACTION_TRACK_EVENT); 136 registerReceiver(mBtReceiver, filter); 137 } 138 139 @Override onDestroy()140 public void onDestroy() { 141 Log.d(TAG, "onDestroy"); 142 mSession.release(); 143 unregisterReceiver(mBtReceiver); 144 super.onDestroy(); 145 } 146 147 @Override onGetRoot(String clientPackageName, int clientUid, Bundle rootHints)148 public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { 149 return new BrowserRoot(MEDIA_ID_ROOT, null); 150 } 151 152 @Override onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result)153 public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) { 154 Log.d(TAG, "onLoadChildren parentMediaId=" + parentMediaId); 155 List<MediaItem> items = new ArrayList<MediaItem>(); 156 result.sendResult(items); 157 } 158 159 BluetoothProfile.ServiceListener mServiceListener = new BluetoothProfile.ServiceListener() { 160 public void onServiceConnected(int profile, BluetoothProfile proxy) { 161 Log.d(TAG, "onServiceConnected"); 162 if (profile == BluetoothProfile.AVRCP_CONTROLLER) { 163 mAvrcpCommandQueue.obtainMessage(MSG_PROFILE_CONNECT, proxy).sendToTarget(); 164 List<BluetoothDevice> devices = proxy.getConnectedDevices(); 165 if (devices != null && devices.size() > 0) { 166 BluetoothDevice device = devices.get(0); 167 Log.d(TAG, "got AVRCP device " + device); 168 } 169 } 170 } 171 172 public void onServiceDisconnected(int profile) { 173 Log.d(TAG, "onServiceDisconnected " + profile); 174 if (profile == BluetoothProfile.AVRCP_CONTROLLER) { 175 mAvrcpProfile = null; 176 mAvrcpCommandQueue.obtainMessage(MSG_PROFILE_DISCONNECT).sendToTarget(); 177 } 178 } 179 }; 180 181 // Media Session Stuff. 182 private MediaSession.Callback mSessionCallbacks = new MediaSession.Callback() { 183 @Override 184 public void onPlay() { 185 Log.d(TAG, "onPlay"); 186 mAvrcpCommandQueue.obtainMessage( 187 MSG_AVRCP_PASSTHRU, BluetoothAvrcpController.PASS_THRU_CMD_ID_PLAY).sendToTarget(); 188 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 189 } 190 191 @Override 192 public void onPause() { 193 Log.d(TAG, "onPause"); 194 mAvrcpCommandQueue.obtainMessage( 195 MSG_AVRCP_PASSTHRU, BluetoothAvrcpController.PASS_THRU_CMD_ID_PAUSE).sendToTarget(); 196 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 197 } 198 199 @Override 200 public void onSkipToNext() { 201 Log.d(TAG, "onSkipToNext"); 202 mAvrcpCommandQueue.obtainMessage( 203 MSG_AVRCP_PASSTHRU, BluetoothAvrcpController.PASS_THRU_CMD_ID_FORWARD) 204 .sendToTarget(); 205 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 206 } 207 208 @Override 209 public void onSkipToPrevious() { 210 Log.d(TAG, "onSkipToPrevious"); 211 212 mAvrcpCommandQueue.obtainMessage( 213 MSG_AVRCP_PASSTHRU, BluetoothAvrcpController.PASS_THRU_CMD_ID_BACKWARD) 214 .sendToTarget(); 215 // TRACK_EVENT should be fired eventually and the UI should be hence updated. 216 } 217 218 // These are not yet supported. 219 @Override 220 public void onStop() { 221 Log.d(TAG, "onStop"); 222 } 223 224 @Override 225 public void onCustomAction(String action, Bundle extras) { 226 Log.d(TAG, "onCustomAction action=" + action + " extras=" + extras); 227 } 228 229 @Override 230 public void onPlayFromSearch(String query, Bundle extras) { 231 Log.d(TAG, "playFromSearch not supported in AVRCP"); 232 } 233 234 @Override 235 public void onCommand(String command, Bundle args, ResultReceiver cb) { 236 Log.d(TAG, "onCommand command=" + command + " args=" + args); 237 } 238 239 @Override 240 public void onSkipToQueueItem(long queueId) { 241 Log.d(TAG, "onSkipToQueueItem"); 242 } 243 244 @Override 245 public void onPlayFromMediaId(String mediaId, Bundle extras) { 246 Log.d(TAG, "onPlayFromMediaId mediaId=" + mediaId + " extras=" + extras); 247 } 248 249 }; 250 251 private BroadcastReceiver mBtReceiver = new BroadcastReceiver() { 252 @Override 253 public void onReceive(Context context, Intent intent) { 254 Log.d(TAG, "onReceive intent=" + intent); 255 String action = intent.getAction(); 256 BluetoothDevice btDev = 257 (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 258 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 259 260 if (BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { 261 Log.d(TAG, "handleConnectionStateChange: newState=" 262 + state + " btDev=" + btDev); 263 264 // Connected state will be handled when AVRCP BluetoothProfile gets connected. 265 if (state == BluetoothProfile.STATE_CONNECTED) { 266 mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_CONNECT, btDev).sendToTarget(); 267 } else if (state == BluetoothProfile.STATE_DISCONNECTED) { 268 // Set the playback state to unconnected. 269 mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_DISCONNECT, btDev).sendToTarget(); 270 } 271 } else if (BluetoothAvrcpController.ACTION_TRACK_EVENT.equals(action)) { 272 PlaybackState pbb = 273 intent.getParcelableExtra(BluetoothAvrcpController.EXTRA_PLAYBACK); 274 MediaMetadata mmd = 275 intent.getParcelableExtra(BluetoothAvrcpController.EXTRA_METADATA); 276 mAvrcpCommandQueue.obtainMessage( 277 MSG_TRACK, new Pair<PlaybackState, MediaMetadata>(pbb, mmd)).sendToTarget(); 278 } 279 } 280 }; 281 msgDeviceConnect(BluetoothDevice device)282 private void msgDeviceConnect(BluetoothDevice device) { 283 Log.d(TAG, "msgDeviceConnect"); 284 // We are connected to a new device via A2DP now. 285 mA2dpDevice = device; 286 refreshInitialPlayingState(); 287 } 288 msgProfileConnect(BluetoothProfile profile)289 private void msgProfileConnect(BluetoothProfile profile) { 290 Log.d(TAG, "msgProfileConnect"); 291 if (profile != null) { 292 mAvrcpProfile = (BluetoothAvrcpController) profile; 293 } 294 refreshInitialPlayingState(); 295 } 296 297 // Refresh the UI if we have a connected device and AVRCP is initialized. refreshInitialPlayingState()298 private void refreshInitialPlayingState() { 299 if (mAvrcpProfile == null || mA2dpDevice == null) { 300 Log.d(TAG, "AVRCP Profile " + mAvrcpProfile + " device " + mA2dpDevice); 301 return; 302 } 303 304 List<BluetoothDevice> devices = mAvrcpProfile.getConnectedDevices(); 305 if (devices.size() == 0) { 306 Log.w(TAG, "No devices connected yet"); 307 return; 308 } 309 310 if (mA2dpDevice != null && !mA2dpDevice.equals(devices.get(0))) { 311 Log.e(TAG, "A2dp device : " + mA2dpDevice + " avrcp device " + devices.get(0)); 312 } 313 mA2dpDevice = devices.get(0); 314 315 PlaybackState playbackState = mAvrcpProfile.getPlaybackState(mA2dpDevice); 316 // Add actions required for playback and rebuild the object. 317 PlaybackState.Builder pbb = new PlaybackState.Builder(playbackState); 318 playbackState = pbb.setActions(mTransportControlFlags).build(); 319 320 MediaMetadata mediaMetadata = mAvrcpProfile.getMetadata(mA2dpDevice); 321 Log.d(TAG, "Media metadata " + mediaMetadata + " playback state " + playbackState); 322 mSession.setMetadata(mAvrcpProfile.getMetadata(mA2dpDevice)); 323 mSession.setPlaybackState(playbackState); 324 } 325 msgDeviceDisconnect(BluetoothDevice device)326 private void msgDeviceDisconnect(BluetoothDevice device) { 327 Log.d(TAG, "msgDeviceDisconnect"); 328 if (mA2dpDevice == null) { 329 Log.w(TAG, "Already disconnected - nothing to do here."); 330 return; 331 } else if (!mA2dpDevice.equals(device)) { 332 Log.e(TAG, "Not the right device to disconnect current " + 333 mA2dpDevice + " dc " + device); 334 return; 335 } 336 337 // Unset the session. 338 PlaybackState.Builder pbb = new PlaybackState.Builder(); 339 pbb = pbb.setState(PlaybackState.STATE_ERROR, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 340 PLAYBACK_SPEED) 341 .setActions(mTransportControlFlags) 342 .setErrorMessage(getString(R.string.bluetooth_disconnected)); 343 mSession.setPlaybackState(pbb.build()); 344 } 345 msgProfileDisconnect()346 private void msgProfileDisconnect() { 347 Log.d(TAG, "msgProfileDisconnect"); 348 // The profile is disconnected - even if the device is still connected we cannot really have 349 // a functioning UI so reset the session. 350 mAvrcpProfile = null; 351 352 // Unset the session. 353 PlaybackState.Builder pbb = new PlaybackState.Builder(); 354 pbb = pbb.setState(PlaybackState.STATE_ERROR, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 355 PLAYBACK_SPEED) 356 .setActions(mTransportControlFlags) 357 .setErrorMessage(getString(R.string.bluetooth_disconnected)); 358 mSession.setPlaybackState(pbb.build()); 359 } 360 msgTrack(PlaybackState pb, MediaMetadata mmd)361 private void msgTrack(PlaybackState pb, MediaMetadata mmd) { 362 Log.d(TAG, "msgTrack: playback: " + pb + " mmd: " + mmd); 363 // Log the current track position/content. 364 MediaController controller = mSession.getController(); 365 PlaybackState prevPS = controller.getPlaybackState(); 366 MediaMetadata prevMM = controller.getMetadata(); 367 368 if (prevPS != null) { 369 Log.d(TAG, "prevPS " + prevPS); 370 } 371 372 if (prevMM != null) { 373 String title = prevMM.getString(MediaMetadata.METADATA_KEY_TITLE); 374 long trackLen = prevMM.getLong(MediaMetadata.METADATA_KEY_DURATION); 375 Log.d(TAG, "prev MM title " + title + " track len " + trackLen); 376 } 377 378 if (mmd != null) { 379 Log.d(TAG, "msgTrack() mmd " + mmd.getDescription()); 380 mSession.setMetadata(mmd); 381 } 382 383 if (pb != null) { 384 Log.d(TAG, "msgTrack() playbackstate " + pb); 385 PlaybackState.Builder pbb = new PlaybackState.Builder(pb); 386 pb = pbb.setActions(mTransportControlFlags).build(); 387 mSession.setPlaybackState(pb); 388 } 389 } 390 msgPassThru(int cmd)391 private void msgPassThru(int cmd) { 392 Log.d(TAG, "msgPassThru " + cmd); 393 if (mA2dpDevice == null) { 394 // We should have already disconnected - ignore this message. 395 Log.e(TAG, "Already disconnected ignoring."); 396 return; 397 } 398 399 if (mAvrcpProfile == null) { 400 // We may be disconnected with the profile but there is not much we can do for now but 401 // to wait for the profile to come back up. 402 Log.e(TAG, "Profile disconnected; ignoring."); 403 return; 404 } 405 406 // Send the pass through. 407 mAvrcpProfile.sendPassThroughCmd( 408 mA2dpDevice, cmd, BluetoothAvrcpController.KEY_STATE_PRESSED); 409 mAvrcpProfile.sendPassThroughCmd( 410 mA2dpDevice, cmd, BluetoothAvrcpController.KEY_STATE_RELEASED); 411 } 412 } 413