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