Home | History | Annotate | Download | only in bluetooth
      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      */
    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
    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
    184         public void onPlaybackStateChanged(PlaybackState state) {
    185             Log.d(TAG + " onPlaybackStateChanged: " + state.getState());
    186         }
    187 
    188         @Override
    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      */
    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      */
    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")
    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")
    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")
    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")
    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")
    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.")
    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.")
    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.")
    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
    493     public void shutdown() {
    494         setCurrentMediaController(null);
    495     }
    496 }
    497