1 /* 2 * Copyright (C) 2017 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.googlecode.android_scripting.facade.bluetooth; 18 19 import android.app.Service; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.media.MediaMetadata; 24 import android.media.browse.MediaBrowser; 25 import android.media.session.MediaController; 26 import android.media.session.MediaSessionManager; 27 import android.media.session.PlaybackState; 28 import android.os.Bundle; 29 import android.os.Handler; 30 import android.os.Looper; 31 32 import com.googlecode.android_scripting.Log; 33 import com.googlecode.android_scripting.facade.EventFacade; 34 import com.googlecode.android_scripting.facade.FacadeManager; 35 import com.googlecode.android_scripting.facade.bluetooth.media.BluetoothSL4AAudioSrcMBS; 36 import com.googlecode.android_scripting.jsonrpc.RpcReceiver; 37 import com.googlecode.android_scripting.rpc.Rpc; 38 import com.googlecode.android_scripting.rpc.RpcParameter; 39 40 import java.util.ArrayList; 41 import java.util.HashMap; 42 import java.util.List; 43 import java.util.Map; 44 45 /** 46 * SL4A Facade for running Bluetooth Media related test cases 47 * The APIs provided here can be grouped into 3 categories: 48 * 1. Those that can run on both an Audio Source and Sink 49 * 2. Those that makes sense to run only on a Audio Source like a phone 50 * 3. Those that makes sense to run only on a Audio Sink like a Car. 51 * 52 * This media test framework consists of 3 classes: 53 * 1. BluetoothMediaFacade - this class that provides the APIs that a RPC client can interact with 54 * 2. BluetoothSL4AMBS - This is a MediaBrowserService that is intended to run on the Audio Source 55 * (phone). This MediaBrowserService that runs as part of the SL4A app is used to intercept 56 * Media key events coming in from a AVRCP Controller like Car. Intercepting these events lets us 57 * instrument the Bluetooth media related tests. 58 * 3. BluetoothMediaPlayback - The class that the MediaBrowserService uses to play media files. 59 * It is a UI-less MediaPlayer that serves the purpose of Bluetooth Media testing. 60 * 61 * The idea is for the BluetoothMediaFacade to create a BluetoothSL4AMBS MediaSession on the 62 * Phone (Bluetooth Audio source/Avrcp Target) and use it intercept the Media commands coming 63 * from the CarKitt (Bluetooth Audio Sink / Avrcp Controller). 64 * On the Carkitt side, we just create and connect a MediaBrowser to the 65 * BluetoothMediaBrowserService that is part of the Carkitt's Bluetooth Audio App. We use this 66 * browser to send media commands to the Phone side and intercept the commands with the 67 * BluetoothSL4AMBS. 68 * This set up helps to instrument tests that can test various Bluetooth Media usecases. 69 */ 70 71 public class BluetoothMediaFacade extends RpcReceiver { 72 private static final String TAG = "BluetoothMediaFacade"; 73 private static final boolean VDBG = false; 74 private final Service mService; 75 private final Context mContext; 76 private Handler mHandler; 77 private MediaSessionManager mSessionManager; 78 private MediaController mMediaController = null; 79 private MediaController.Callback mMediaCtrlCallback = null; 80 private MediaSessionManager.OnActiveSessionsChangedListener mSessionListener; 81 private MediaBrowser mBrowser = null; 82 83 private static EventFacade mEventFacade; 84 // Events posted 85 private static final String EVENT_PLAY_RECEIVED = "playReceived"; 86 private static final String EVENT_PAUSE_RECEIVED = "pauseReceived"; 87 private static final String EVENT_SKIP_PREV_RECEIVED = "skipPrevReceived"; 88 private static final String EVENT_SKIP_NEXT_RECEIVED = "skipNextReceived"; 89 90 // Commands received 91 private static final String CMD_MEDIA_PLAY = "play"; 92 private static final String CMD_MEDIA_PAUSE = "pause"; 93 private static final String CMD_MEDIA_SKIP_NEXT = "skipNext"; 94 private static final String CMD_MEDIA_SKIP_PREV = "skipPrev"; 95 96 private static final String BLUETOOTH_PKG_NAME = "com.android.bluetooth"; 97 private static final String BROWSER_SERVICE_NAME = 98 "com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService"; 99 private static final String BLUETOOTH_MBS_TAG = "BluetoothMediaBrowserService"; 100 101 // MediaMetadata keys 102 private static final String MEDIA_KEY_TITLE = "keyTitle"; 103 private static final String MEDIA_KEY_ALBUM = "keyAlbum"; 104 private static final String MEDIA_KEY_ARTIST = "keyArtist"; 105 private static final String MEDIA_KEY_DURATION = "keyDuration"; 106 private static final String MEDIA_KEY_NUM_TRACKS = "keyNumTracks"; 107 108 /** 109 * Following things are initialized here: 110 * 1. Setup Listeners to Active Media Session changes 111 * 2. Create a new MediaController.callback instance 112 */ BluetoothMediaFacade(FacadeManager manager)113 public BluetoothMediaFacade(FacadeManager manager) { 114 super(manager); 115 mService = manager.getService(); 116 mEventFacade = manager.getReceiver(EventFacade.class); 117 mHandler = new Handler(Looper.getMainLooper()); 118 mContext = mService.getApplicationContext(); 119 mSessionManager = 120 (MediaSessionManager) mContext.getSystemService(mContext.MEDIA_SESSION_SERVICE); 121 mSessionListener = new SessionChangeListener(); 122 // Listen on Active MediaSession changes, so we can get the active session's MediaController 123 if (mSessionManager != null) { 124 ComponentName compName = 125 new ComponentName(mContext.getPackageName(), this.getClass().getName()); 126 mSessionManager.addOnActiveSessionsChangedListener(mSessionListener, null, 127 mHandler); 128 if (VDBG) { 129 List<MediaController> mcl = mSessionManager.getActiveSessions(null); 130 Log.d(TAG + " Num Sessions " + mcl.size()); 131 for (int i = 0; i < mcl.size(); i++) { 132 Log.d(TAG + "Active session : " + i + ((MediaController) (mcl.get( 133 i))).getPackageName() + ((MediaController) (mcl.get(i))).getTag()); 134 } 135 } 136 } 137 mMediaCtrlCallback = new MediaControllerCallback(); 138 } 139 140 /** 141 * The listener that was setup for listening to changes to Active Media Sessions. 142 * This listener is useful in both Car and Phone sides. 143 */ 144 private class SessionChangeListener 145 implements MediaSessionManager.OnActiveSessionsChangedListener { 146 /** 147 * On the Phone side, it listens to the BluetoothSL4AAudioSrcMBS (that the SL4A app runs) 148 * becoming active. 149 * On the Car side, it listens to the BluetoothMediaBrowserService (associated with the 150 * Bluetooth Audio App) becoming active. 151 * The idea is to get a handle to the MediaController appropriate for the device, so 152 * that we can send and receive Media commands. 153 */ 154 @Override onActiveSessionsChanged(List<MediaController> controllers)155 public void onActiveSessionsChanged(List<MediaController> controllers) { 156 if (VDBG) { 157 Log.d(TAG + " onActiveSessionsChanged : " + controllers.size()); 158 for (int i = 0; i < controllers.size(); i++) { 159 Log.d(TAG + "Active session : " + i + ((MediaController) (controllers.get( 160 i))).getPackageName() + ((MediaController) (controllers.get( 161 i))).getTag()); 162 } 163 } 164 // As explained above, looking for the BluetoothSL4AAudioSrcMBS (when running on Phone) 165 // or BluetoothMediaBrowserService (when running on Carkitt). 166 for (int i = 0; i < controllers.size(); i++) { 167 MediaController controller = (MediaController) controllers.get(i); 168 if ((controller.getTag().contains(BluetoothSL4AAudioSrcMBS.getTag())) 169 || (controller.getTag().contains(BLUETOOTH_MBS_TAG))) { 170 setCurrentMediaController(controller); 171 return; 172 } 173 } 174 } 175 } 176 177 /** 178 * When the MediaController for the required MediaSession is obtained, register for its 179 * callbacks. 180 * Not used yet, but this can be used to verify state changes in both ends. 181 */ 182 private class MediaControllerCallback extends MediaController.Callback { 183 @Override onPlaybackStateChanged(PlaybackState state)184 public void onPlaybackStateChanged(PlaybackState state) { 185 Log.d(TAG + " onPlaybackStateChanged: " + state.getState()); 186 } 187 188 @Override onMetadataChanged(MediaMetadata metadata)189 public void onMetadataChanged(MediaMetadata metadata) { 190 Log.d(TAG + " onMetadataChanged "); 191 } 192 } 193 194 /** 195 * Callback on <code>MediaBrowser.connect()</code> 196 * This is relevant only on the Carkitt side, since the intent is to connect a MediaBrowser 197 * to the BluetoothMediaBrowserService that is run by the Car's Bluetooth Audio App. 198 * On successful connection, we obtain the handle to the corresponding MediaController, 199 * so we can imitate sending media commands via the Bluetooth Audio App. 200 */ 201 MediaBrowser.ConnectionCallback mBrowserConnectionCallback = 202 new MediaBrowser.ConnectionCallback() { 203 private static final String classTag = TAG + " BrowserConnectionCallback"; 204 205 @Override 206 public void onConnected() { 207 Log.d(classTag + " onConnected: session token " + mBrowser.getSessionToken()); 208 MediaController mediaController = new MediaController(mContext, 209 mBrowser.getSessionToken()); 210 // Update the MediaController 211 setCurrentMediaController(mediaController); 212 } 213 214 @Override 215 public void onConnectionFailed() { 216 Log.d(classTag + " onConnectionFailed"); 217 } 218 }; 219 220 /** 221 * Update the Current MediaController. 222 * As has been commented above, we need the MediaController handles to the 223 * BluetoothSL4AAudioSrcMBS on Phone and BluetoothMediaBrowserService on Car to send and receive 224 * media commands. 225 * 226 * @param controller - Controller to update with 227 */ setCurrentMediaController(MediaController controller)228 private void setCurrentMediaController(MediaController controller) { 229 Handler mainHandler = new Handler(mContext.getMainLooper()); 230 if (mMediaController == null && controller != null) { 231 Log.d(TAG + " Setting MediaController " + controller.getTag()); 232 mMediaController = controller; 233 mMediaController.registerCallback(mMediaCtrlCallback); 234 } else if (mMediaController != null && controller != null) { 235 // We have a new MediaController that we have to update to. 236 if (controller.getSessionToken().equals(mMediaController.getSessionToken()) 237 == false) { 238 Log.d(TAG + " Changing MediaController " + controller.getTag()); 239 mMediaController.unregisterCallback(mMediaCtrlCallback); 240 mMediaController = controller; 241 mMediaController.registerCallback(mMediaCtrlCallback, mainHandler); 242 } 243 } else if (mMediaController != null && controller == null) { 244 // Clearing the current MediaController 245 Log.d(TAG + " Clearing MediaController " + mMediaController.getTag()); 246 mMediaController.unregisterCallback(mMediaCtrlCallback); 247 mMediaController = controller; 248 } 249 } 250 251 /** 252 * Class method called from {@link BluetoothSL4AAudioSrcMBS} to post an Event through 253 * EventFacade back to the RPC client. 254 * This is dispatched from the Phone to the host (RPC Client) to acknowledge that it 255 * received a playback command. 256 * 257 * @param playbackState PlaybackState change that is posted as an Event to the client. 258 */ dispatchPlaybackStateChanged(int playbackState)259 public static void dispatchPlaybackStateChanged(int playbackState) { 260 Bundle news = new Bundle(); 261 switch (playbackState) { 262 case PlaybackState.STATE_PLAYING: 263 mEventFacade.postEvent(EVENT_PLAY_RECEIVED, news); 264 break; 265 case PlaybackState.STATE_PAUSED: 266 mEventFacade.postEvent(EVENT_PAUSE_RECEIVED, news); 267 break; 268 case PlaybackState.STATE_SKIPPING_TO_NEXT: 269 mEventFacade.postEvent(EVENT_SKIP_NEXT_RECEIVED, news); 270 break; 271 case PlaybackState.STATE_SKIPPING_TO_PREVIOUS: 272 mEventFacade.postEvent(EVENT_SKIP_PREV_RECEIVED, news); 273 break; 274 default: 275 break; 276 } 277 } 278 279 /******************************RPC APIS************************************************/ 280 281 /** 282 * Relevance - Phone and Car. 283 * Sends the passthrough command through the currently active MediaController. 284 * If there isn't one, look for the currently active sessions and just pick the first one, 285 * just a fallback. 286 * This function is generic enough to be used in either a Phone or the Car side, since 287 * all this does is to pick the currently active Media Controller and sends a passthrough 288 * command. In the test setup, this is used to mimic sending a passthrough command from 289 * Car. 290 */ 291 @Rpc(description = "Simulate a passthrough command") bluetoothMediaPassthrough( @pcParametername = "passthruCmd", description = "play/pause/skipFwd/skipBack") String passthruCmd)292 public void bluetoothMediaPassthrough( 293 @RpcParameter(name = "passthruCmd", description = "play/pause/skipFwd/skipBack") 294 String passthruCmd) { 295 Log.d(TAG + "Passthrough Cmd " + passthruCmd); 296 if (mMediaController == null) { 297 Log.i(TAG + " Media Controller not ready - Grabbing existing one"); 298 ComponentName name = 299 new ComponentName(mContext.getPackageName(), 300 mSessionListener.getClass().getName()); 301 List<MediaController> listMC = mSessionManager.getActiveSessions(null); 302 if (listMC.size() > 0) { 303 if (VDBG) { 304 Log.d(TAG + " Num Sessions " + listMC.size()); 305 for (int i = 0; i < listMC.size(); i++) { 306 Log.d(TAG + "Active session : " + i + ((MediaController) (listMC.get( 307 i))).getPackageName() + ((MediaController) (listMC.get( 308 i))).getTag()); 309 } 310 } 311 mMediaController = (MediaController) listMC.get(0); 312 } else { 313 Log.d(TAG + " No Active Media Session to grab"); 314 return; 315 } 316 } 317 318 switch (passthruCmd) { 319 case CMD_MEDIA_PLAY: 320 mMediaController.getTransportControls().play(); 321 break; 322 case CMD_MEDIA_PAUSE: 323 mMediaController.getTransportControls().pause(); 324 break; 325 case CMD_MEDIA_SKIP_NEXT: 326 mMediaController.getTransportControls().skipToNext(); 327 break; 328 case CMD_MEDIA_SKIP_PREV: 329 mMediaController.getTransportControls().skipToPrevious(); 330 break; 331 default: 332 Log.d(TAG + " Unsupported Passthrough Cmd"); 333 break; 334 } 335 } 336 337 /** 338 * Relevance - Phone and Car. 339 * Returns the currently playing media's metadata. 340 * Can be queried on the car and the phone in the middle of a streaming session to 341 * verify they are in sync. 342 * 343 * @return Currently playing Media's metadata 344 */ 345 @Rpc(description = "Gets the Metadata of currently playing Media") bluetoothMediaGetCurrentMediaMetaData()346 public Map<String, String> bluetoothMediaGetCurrentMediaMetaData() { 347 Map<String, String> track = null; 348 if (mMediaController == null) { 349 Log.d(TAG + "MediaController Not set"); 350 return track; 351 } 352 MediaMetadata metadata = mMediaController.getMetadata(); 353 if (metadata == null) { 354 Log.e("No Metadata available."); 355 return track; 356 } 357 track = new HashMap<>(); 358 track.put(MEDIA_KEY_TITLE, metadata.getString(MediaMetadata.METADATA_KEY_TITLE)); 359 track.put(MEDIA_KEY_ALBUM, metadata.getString(MediaMetadata.METADATA_KEY_ALBUM)); 360 track.put(MEDIA_KEY_ARTIST, metadata.getString(MediaMetadata.METADATA_KEY_ARTIST)); 361 track.put(MEDIA_KEY_DURATION, 362 String.valueOf(metadata.getLong(MediaMetadata.METADATA_KEY_DURATION))); 363 track.put(MEDIA_KEY_NUM_TRACKS, 364 String.valueOf(metadata.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS))); 365 return track; 366 } 367 368 /** 369 * Relevance - Phone and Car. 370 * Returns the currently playing media's playback state. 371 * Can be queried on the car and the phone in the middle of a streaming session to 372 * verify they are in sync. 373 * 374 * @return Currently playing Media's playback state 375 */ 376 @Rpc(description = "Gets the state of current playback") bluetoothMediaGetCurrentPlaybackState()377 public PlaybackState bluetoothMediaGetCurrentPlaybackState() throws Exception { 378 if (mMediaController == null) { 379 Log.e(TAG + "MediaController not set"); 380 throw new Exception("MediaController not set"); 381 } 382 PlaybackState playbackState = mMediaController.getPlaybackState(); 383 if (playbackState == null) { 384 Log.d("No playback state available."); 385 return null; 386 } 387 return playbackState; 388 } 389 390 /** 391 * Relevance - Phone and Car 392 * Returns the current active media sessions for the device. This is useful to see if a 393 * Media Session we are interested in is currently active. 394 * In the Bluetooth Media tests, this is indirectly used to determine if audio is being 395 * played via BT. For ex., when the Car and Phone are connected via BT and audio is being 396 * streamed, BluetoothMediaBrowserService will be active on the Car side. If the connection is 397 * terminated in the middle, BluetoothMediaBrowserService will no longer be active on the 398 * Carkitt, whereas BluetoothSL4AAudioSrcMBS will still be active. 399 * 400 * @return A list of names of the active media sessions 401 */ 402 @Rpc(description = "Get the current active Media Sessions") bluetoothMediaGetActiveMediaSessions()403 public List<String> bluetoothMediaGetActiveMediaSessions() { 404 List<MediaController> controllers = mSessionManager.getActiveSessions(null); 405 List<String> sessions = new ArrayList<String>(); 406 for (MediaController mc : controllers) { 407 sessions.add(mc.getTag()); 408 } 409 return sessions; 410 } 411 412 /** 413 * Relevance - Car Only 414 * Called from the Carkitt to connect a MediaBrowser to the Bluetooth Audio App's 415 * BluetoothMediaBrowserService. The callback on successful connection gives the handle to 416 * the MediaController through which we can send media commands. 417 */ 418 @Rpc(description = "Connect a MediaBrowser to the BluetoothMediaBrowserService in the Carkitt") bluetoothMediaConnectToCarMBS()419 public void bluetoothMediaConnectToCarMBS() { 420 ComponentName compName; 421 // Create a MediaBrowser to connect to the BluetoothMediaBrowserService 422 if (mBrowser == null) { 423 compName = new ComponentName(BLUETOOTH_PKG_NAME, BROWSER_SERVICE_NAME); 424 // Note - MediaBrowser connect needs to be done on the Main Thread's handler, 425 // otherwise we never get the ServiceConnected callback. 426 Runnable createAndConnectMediaBrowser = new Runnable() { 427 @Override 428 public void run() { 429 mBrowser = new MediaBrowser(mContext, compName, mBrowserConnectionCallback, 430 null); 431 if (mBrowser != null) { 432 Log.d(TAG + " Connecting to MBS"); 433 mBrowser.connect(); 434 } else { 435 Log.d(TAG + " Failed to create a MediaBrowser"); 436 } 437 } 438 }; 439 440 Handler mainHandler = new Handler(mContext.getMainLooper()); 441 mainHandler.post(createAndConnectMediaBrowser); 442 } //mBrowser 443 } 444 445 /** 446 * Relevance - Phone Only 447 * Start the BluetoothSL4AAudioSrcMBS on the Phone so the media commands coming in 448 * via Bluetooth AVRCP can be intercepted by the SL4A test 449 */ 450 @Rpc(description = "Start the BluetoothSL4AAudioSrcMBS on Phone.") bluetoothMediaPhoneSL4AMBSStart()451 public void bluetoothMediaPhoneSL4AMBSStart() { 452 Log.d(TAG + "Starting BluetoothSL4AAudioSrcMBS"); 453 // Start the Avrcp Media Browser service. Starting it sets it to active. 454 Intent startIntent = new Intent(mContext, BluetoothSL4AAudioSrcMBS.class); 455 mContext.startService(startIntent); 456 } 457 458 /** 459 * Relevance - Phone Only 460 * Stop the BluetoothSL4AAudioSrcMBS 461 */ 462 @Rpc(description = "Stop the BluetoothSL4AAudioSrcMBS running on Phone.") bluetoothMediaPhoneSL4AMBSStop()463 public void bluetoothMediaPhoneSL4AMBSStop() { 464 Log.d(TAG + "Stopping BluetoothSL4AAudioSrcMBS"); 465 // Stop the Avrcp Media Browser service. 466 Intent stopIntent = new Intent(mContext, BluetoothSL4AAudioSrcMBS.class); 467 mContext.stopService(stopIntent); 468 } 469 470 /** 471 * Relevance - Phone only 472 * This is used to simulate play/pause/skip media commands on the Phone directly, as against 473 * receiving these commands via AVRCP from the Carkitt. 474 * This function talks to the BluetoothSL4AAudioSrcMBS to simulate the media command. 475 * An example test where this would be useful - Play music on Phone that is not connected 476 * on bluetooth and connect in the middle to verify if music is steamed to the other end. 477 * 478 * @param command - Media command to simulate on the Phone 479 */ 480 @Rpc(description = "Media Commands on the Phone's BluetoothAvrcpMBS.") bluetoothMediaHandleMediaCommandOnPhone(String command)481 public void bluetoothMediaHandleMediaCommandOnPhone(String command) { 482 BluetoothSL4AAudioSrcMBS mbs = 483 BluetoothSL4AAudioSrcMBS.getAvrcpMediaBrowserService(); 484 if (mbs != null) { 485 mbs.handleMediaCommand(command); 486 } else { 487 Log.e(TAG + " No BluetoothSL4AAudioSrcMBS running on the device"); 488 } 489 } 490 491 492 @Override shutdown()493 public void shutdown() { 494 setCurrentMediaController(null); 495 } 496 } 497