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