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