• 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.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