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 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      */
    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
    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
    183         public void onPlaybackStateChanged(PlaybackState state) {
    184             Log.d(TAG + " onPlaybackStateChanged: " + state.getState());
    185         }
    186 
    187         @Override
    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      */
    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      */
    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")
    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")
    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")
    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")
    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.")
    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.")
    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.")
    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
    470     public void shutdown() {
    471         setCurrentMediaController(null);
    472     }
    473 }
    474