• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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