aboutsummaryrefslogtreecommitdiff
path: root/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothMediaFacade.java
blob: 404a791172bfd4dc0387f0d6b60dbdb742dc3841 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.android_scripting.facade.bluetooth;

import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.media.MediaMetadata;
import android.media.browse.MediaBrowser;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;

import com.googlecode.android_scripting.Log;
import com.googlecode.android_scripting.facade.EventFacade;
import com.googlecode.android_scripting.facade.FacadeManager;
import com.googlecode.android_scripting.facade.bluetooth.media.BluetoothSL4AAudioSrcMBS;
import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
import com.googlecode.android_scripting.rpc.Rpc;
import com.googlecode.android_scripting.rpc.RpcParameter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * SL4A Facade for running Bluetooth Media related test cases
 * The APIs provided here can be grouped into 3 categories:
 * 1. Those that can run on both an Audio Source and Sink
 * 2. Those that makes sense to run only on a Audio Source like a phone
 * 3. Those that makes sense to run only on a Audio Sink like a Car.
 *
 * This media test framework consists of 3 classes:
 * 1. BluetoothMediaFacade - this class that provides the APIs that a RPC client can interact with
 * 2. BluetoothSL4AMBS - This is a MediaBrowserService that is intended to run on the Audio Source
 * (phone).  This MediaBrowserService that runs as part of the SL4A app is used to intercept
 * Media key events coming in from a AVRCP Controller like Car.  Intercepting these events lets us
 * instrument the Bluetooth media related tests.
 * 3. BluetoothMediaPlayback - The class that the MediaBrowserService uses to play media files.
 * It is a UI-less MediaPlayer that serves the purpose of Bluetooth Media testing.
 *
 * The idea is for the BluetoothMediaFacade to create a BluetoothSL4AMBS MediaSession on the
 * Phone (Bluetooth Audio source/Avrcp Target) and use it intercept the Media commands coming
 * from the CarKitt (Bluetooth Audio Sink / Avrcp Controller).
 * On the Carkitt side, we just create and connect a MediaBrowser to the A2dpMediaBrowserService
 * that is part of the Carkitt's Bluetooth Audio App.  We use this browser to send media commands
 * to the Phone side and intercept the commands with the BluetoothSL4AMBS.
 * This set up helps to instrument tests that can test various Bluetooth Media usecases.
 */

public class BluetoothMediaFacade extends RpcReceiver {
    private static final String TAG = "BluetoothMediaFacade";
    private static final boolean VDBG = false;
    private final Service mService;
    private final Context mContext;
    private Handler mHandler;
    private MediaSessionManager mSessionManager;
    private MediaController mMediaController = null;
    private MediaController.Callback mMediaCtrlCallback = null;
    private MediaSessionManager.OnActiveSessionsChangedListener mSessionListener;
    private MediaBrowser mBrowser = null;

    private static EventFacade mEventFacade;
    // Events posted
    private static final String EVENT_PLAY_RECEIVED = "playReceived";
    private static final String EVENT_PAUSE_RECEIVED = "pauseReceived";
    private static final String EVENT_SKIP_PREV_RECEIVED = "skipPrevReceived";
    private static final String EVENT_SKIP_NEXT_RECEIVED = "skipNextReceived";

    // Commands received
    private static final String CMD_MEDIA_PLAY = "play";
    private static final String CMD_MEDIA_PAUSE = "pause";
    private static final String CMD_MEDIA_SKIP_NEXT = "skipNext";
    private static final String CMD_MEDIA_SKIP_PREV = "skipPrev";

    private static final String BLUETOOTH_PKG_NAME = "com.android.bluetooth";
    private static final String BROWSER_SERVICE_NAME =
            "com.android.bluetooth.a2dpsink.mbs.A2dpMediaBrowserService";
    private static final String A2DP_MBS_TAG = "A2dpMediaBrowserService";

    // MediaMetadata keys
    private static final String MEDIA_KEY_TITLE = "keyTitle";
    private static final String MEDIA_KEY_ALBUM = "keyAlbum";
    private static final String MEDIA_KEY_ARTIST = "keyArtist";
    private static final String MEDIA_KEY_DURATION = "keyDuration";
    private static final String MEDIA_KEY_NUM_TRACKS = "keyNumTracks";

    /**
     * Following things are initialized here:
     * 1. Setup Listeners to Active Media Session changes
     * 2. Create a new MediaController.callback instance
     */
    public BluetoothMediaFacade(FacadeManager manager) {
        super(manager);
        mService = manager.getService();
        mEventFacade = manager.getReceiver(EventFacade.class);
        mHandler = new Handler(Looper.getMainLooper());
        mContext = mService.getApplicationContext();
        mSessionManager =
                (MediaSessionManager) mContext.getSystemService(mContext.MEDIA_SESSION_SERVICE);
        mSessionListener = new SessionChangeListener();
        // Listen on Active MediaSession changes, so we can get the active session's MediaController
        if (mSessionManager != null) {
            ComponentName compName =
                    new ComponentName(mContext.getPackageName(), this.getClass().getName());
            mSessionManager.addOnActiveSessionsChangedListener(mSessionListener, null,
                    mHandler);
            if (VDBG) {
                List<MediaController> mcl = mSessionManager.getActiveSessions(null);
                Log.d(TAG + " Num Sessions " + mcl.size());
                for (int i = 0; i < mcl.size(); i++) {
                    Log.d(TAG + "Active session : " + i + ((MediaController) (mcl.get(
                            i))).getPackageName() + ((MediaController) (mcl.get(i))).getTag());
                }
            }
        }
        mMediaCtrlCallback = new MediaControllerCallback();
    }

    /**
     * The listener that was setup for listening to changes to Active Media Sessions.
     * This listener is useful in both Car and Phone sides.
     */
    private class SessionChangeListener
            implements MediaSessionManager.OnActiveSessionsChangedListener {
        /**
         * On the Phone side, it listens to the BluetoothSL4AAudioSrcMBS (that the SL4A app runs)
         * becoming active.
         * On the Car side, it listens to the A2dpMediaBrowserService (associated with the
         * Bluetooth Audio App) becoming active.
         * The idea is to get a handle to the MediaController appropriate for the device, so
         * that we can send and receive Media commands.
         */
        @Override
        public void onActiveSessionsChanged(List<MediaController> controllers) {
            if (VDBG) {
                Log.d(TAG + " onActiveSessionsChanged : " + controllers.size());
                for (int i = 0; i < controllers.size(); i++) {
                    Log.d(TAG + "Active session : " + i + ((MediaController) (controllers.get(
                            i))).getPackageName() + ((MediaController) (controllers.get(
                            i))).getTag());
                }
            }
            // As explained above, looking for the BluetoothSL4AAudioSrcMBS (when running on Phone)
            // or A2dpMediaBrowserService (when running on Carkitt).
            for (int i = 0; i < controllers.size(); i++) {
                MediaController controller = (MediaController) controllers.get(i);
                if ((controller.getTag().contains(BluetoothSL4AAudioSrcMBS.getTag()))
                        || (controller.getTag().contains(A2DP_MBS_TAG))) {
                    setCurrentMediaController(controller);
                    return;
                }
            }
        }
    }

    /**
     * When the MediaController for the required MediaSession is obtained, register for its
     * callbacks.
     * Not used yet, but this can be used to verify state changes in both ends.
     */
    private class MediaControllerCallback extends MediaController.Callback {
        @Override
        public void onPlaybackStateChanged(PlaybackState state) {
            Log.d(TAG + " onPlaybackStateChanged: " + state.getState());
        }

        @Override
        public void onMetadataChanged(MediaMetadata metadata) {
            Log.d(TAG + " onMetadataChanged ");
        }
    }

    /**
     * Callback on <code>MediaBrowser.connect()</code>
     * This is relevant only on the Carkitt side, since the intent is to connect a MediaBrowser
     * to the A2dpMediaBrowser Service that is run by the Car's Bluetooth Audio App.
     * On successful connection, we obtain the handle to the corresponding MediaController,
     * so we can imitate sending media commands via the Bluetooth Audio App.
     */
    MediaBrowser.ConnectionCallback mBrowserConnectionCallback =
            new MediaBrowser.ConnectionCallback() {
                private static final String classTag = TAG + " BrowserConnectionCallback";

                @Override
                public void onConnected() {
                    Log.d(classTag + " onConnected: session token " + mBrowser.getSessionToken());
                    MediaController mediaController = new MediaController(mContext,
                            mBrowser.getSessionToken());
                    // Update the MediaController
                    setCurrentMediaController(mediaController);
                }

                @Override
                public void onConnectionFailed() {
                    Log.d(classTag + " onConnectionFailed");
                }
            };

    /**
     * Update the Current MediaController.
     * As has been commented above, we need the MediaController handles to the
     * BluetoothSL4AAudioSrcMBS on Phone and A2dpMediaBrowserService on Car to send and receive
     * media commands.
     *
     * @param controller - Controller to update with
     */
    private void setCurrentMediaController(MediaController controller) {
        Handler mainHandler = new Handler(mContext.getMainLooper());
        if (mMediaController == null && controller != null) {
            Log.d(TAG + " Setting MediaController " + controller.getTag());
            mMediaController = controller;
            mMediaController.registerCallback(mMediaCtrlCallback);
        } else if (mMediaController != null && controller != null) {
            // We have a new MediaController that we have to update to.
            if (controller.getSessionToken().equals(mMediaController.getSessionToken())
                    == false) {
                Log.d(TAG + " Changing MediaController " + controller.getTag());
                mMediaController.unregisterCallback(mMediaCtrlCallback);
                mMediaController = controller;
                mMediaController.registerCallback(mMediaCtrlCallback, mainHandler);
            }
        } else if (mMediaController != null && controller == null) {
            // Clearing the current MediaController
            Log.d(TAG + " Clearing MediaController " + mMediaController.getTag());
            mMediaController.unregisterCallback(mMediaCtrlCallback);
            mMediaController = controller;
        }
    }

    /**
     * Class method called from {@link BluetoothSL4AAudioSrcMBS} to post an Event through
     * EventFacade back to the RPC client.
     * This is dispatched from the Phone to the host (RPC Client) to acknowledge that it
     * received a playback command.
     *
     * @param playbackState PlaybackState change that is posted as an Event to the client.
     */
    public static void dispatchPlaybackStateChanged(int playbackState) {
        Bundle news = new Bundle();
        switch (playbackState) {
            case PlaybackState.STATE_PLAYING:
                mEventFacade.postEvent(EVENT_PLAY_RECEIVED, news);
                break;
            case PlaybackState.STATE_PAUSED:
                mEventFacade.postEvent(EVENT_PAUSE_RECEIVED, news);
                break;
            case PlaybackState.STATE_SKIPPING_TO_NEXT:
                mEventFacade.postEvent(EVENT_SKIP_NEXT_RECEIVED, news);
                break;
            case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
                mEventFacade.postEvent(EVENT_SKIP_PREV_RECEIVED, news);
                break;
            default:
                break;
        }
    }

    /******************************RPC APIS************************************************/

    /**
     * Relevance - Phone and Car.
     * Sends the passthrough command through the currently active MediaController.
     * If there isn't one, look for the currently active sessions and just pick the first one,
     * just a fallback.
     * This function is generic enough to be used in either a Phone or the Car side, since
     * all this does is to pick the currently active Media Controller and sends a passthrough
     * command.  In the test setup, this is used to mimic sending a passthrough command from
     * Car.
     */
    @Rpc(description = "Simulate a passthrough command")
    public void bluetoothMediaPassthrough(
            @RpcParameter(name = "passthruCmd", description = "play/pause/skipFwd/skipBack")
                    String passthruCmd) {
        Log.d(TAG + "Passthrough Cmd " + passthruCmd);
        if (mMediaController == null) {
            Log.i(TAG + " Media Controller not ready - Grabbing existing one");
            ComponentName name =
                    new ComponentName(mContext.getPackageName(),
                            mSessionListener.getClass().getName());
            List<MediaController> listMC = mSessionManager.getActiveSessions(null);
            if (listMC.size() > 0) {
                if (VDBG) {
                    Log.d(TAG + " Num Sessions " + listMC.size());
                    for (int i = 0; i < listMC.size(); i++) {
                        Log.d(TAG + "Active session : " + i + ((MediaController) (listMC.get(
                                i))).getPackageName() + ((MediaController) (listMC.get(
                                i))).getTag());
                    }
                }
                mMediaController = (MediaController) listMC.get(0);
            } else {
                Log.d(TAG + " No Active Media Session to grab");
                return;
            }
        }

        switch (passthruCmd) {
            case CMD_MEDIA_PLAY:
                mMediaController.getTransportControls().play();
                break;
            case CMD_MEDIA_PAUSE:
                mMediaController.getTransportControls().pause();
                break;
            case CMD_MEDIA_SKIP_NEXT:
                mMediaController.getTransportControls().skipToNext();
                break;
            case CMD_MEDIA_SKIP_PREV:
                mMediaController.getTransportControls().skipToPrevious();
                break;
            default:
                Log.d(TAG + " Unsupported Passthrough Cmd");
                break;
        }
    }

    /**
     * Relevance - Phone and Car.
     * Returns the currently playing media's metadata.
     * Can be queried on the car and the phone in the middle of a streaming session to
     * verify they are in sync.
     *
     * @return Currently playing Media's metadata
     */
    @Rpc(description = "Gets the Metadata of currently playing Media")
    public Map<String, String> bluetoothMediaGetCurrentMediaMetaData() {
        Map<String, String> track = null;
        if (mMediaController == null) {
            Log.d(TAG + "MediaController Not set");
            return track;
        }
        MediaMetadata metadata = mMediaController.getMetadata();
        if (metadata == null) {
            Log.e("No Metadata available.");
            return track;
        }
        track = new HashMap<>();
        track.put(MEDIA_KEY_TITLE, metadata.getString(MediaMetadata.METADATA_KEY_TITLE));
        track.put(MEDIA_KEY_ALBUM, metadata.getString(MediaMetadata.METADATA_KEY_ALBUM));
        track.put(MEDIA_KEY_ARTIST, metadata.getString(MediaMetadata.METADATA_KEY_ARTIST));
        track.put(MEDIA_KEY_DURATION,
                String.valueOf(metadata.getLong(MediaMetadata.METADATA_KEY_DURATION)));
        track.put(MEDIA_KEY_NUM_TRACKS,
                String.valueOf(metadata.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS)));
        return track;
    }

    /**
     * Relevance - Phone and Car
     * Returns the current active media sessions for the device. This is useful to see if a
     * Media Session we are interested in is currently active.
     * In the Bluetooth Media tests, this is indirectly used to determine if audio is being
     * played via BT.  For ex., when the Car and Phone are connected via BT and audio is being
     * streamed, A2dpMediaBrowserService will be active on the Car side.  If the connection is
     * terminated in the middle, A2dpMediaBrowserService will no longer be active on the Carkitt,
     * whereas BluetoothSL4AAudioSrcMBS will still be active.
     *
     * @return A list of names of the active media sessions
     */
    @Rpc(description = "Get the current active Media Sessions")
    public List<String> bluetoothMediaGetActiveMediaSessions() {
        List<MediaController> controllers = mSessionManager.getActiveSessions(null);
        List<String> sessions = new ArrayList<String>();
        for (MediaController mc : controllers) {
            sessions.add(mc.getTag());
        }
        return sessions;
    }

    /**
     * Relevance - Car Only
     * Called from the Carkitt to connect a MediaBrowser to the Bluetooth Audio App's
     * A2dpMediaBrowserService.  The callback on successful connection gives the handle to
     * the MediaController through which we can send media commands.
     */
    @Rpc(description = "Connect a MediaBrowser to the A2dpMediaBrowserservice in the Carkitt")
    public void bluetoothMediaConnectToCarMBS() {
        ComponentName compName;
        // Create a MediaBrowser to connect to the A2dpMBS
        if (mBrowser == null) {
            compName = new ComponentName(BLUETOOTH_PKG_NAME, BROWSER_SERVICE_NAME);
            // Note - MediaBrowser connect needs to be done on the Main Thread's handler,
            // otherwise we never get the ServiceConnected callback.
            Runnable createAndConnectMediaBrowser = new Runnable() {
                @Override
                public void run() {
                    mBrowser = new MediaBrowser(mContext, compName, mBrowserConnectionCallback,
                            null);
                    if (mBrowser != null) {
                        Log.d(TAG + " Connecting to MBS");
                        mBrowser.connect();
                    } else {
                        Log.d(TAG + " Failed to create a MediaBrowser");
                    }
                }
            };

            Handler mainHandler = new Handler(mContext.getMainLooper());
            mainHandler.post(createAndConnectMediaBrowser);
        } //mBrowser
    }

    /**
     * Relevance - Phone Only
     * Start the BluetoothSL4AAudioSrcMBS on the Phone so the media commands coming in
     * via Bluetooth AVRCP can be intercepted by the SL4A test
     */
    @Rpc(description = "Start the BluetoothSL4AAudioSrcMBS on Phone.")
    public void bluetoothMediaPhoneSL4AMBSStart() {
        Log.d(TAG + "Starting BluetoothSL4AAudioSrcMBS");
        // Start the Avrcp Media Browser service.  Starting it sets it to active.
        Intent startIntent = new Intent(mContext, BluetoothSL4AAudioSrcMBS.class);
        mContext.startService(startIntent);
    }

    /**
     * Relevance - Phone Only
     * Stop the BluetoothSL4AAudioSrcMBS
     */
    @Rpc(description = "Stop the BluetoothSL4AAudioSrcMBS running on Phone.")
    public void bluetoothMediaPhoneSL4AMBSStop() {
        Log.d(TAG + "Stopping BluetoothSL4AAudioSrcMBS");
        // Stop the Avrcp Media Browser service.
        Intent stopIntent = new Intent(mContext, BluetoothSL4AAudioSrcMBS.class);
        mContext.stopService(stopIntent);
    }

    /**
     * Relevance - Phone only
     * This is used to simulate play/pause/skip media commands on the Phone directly, as against
     * receiving these commands via AVRCP from the Carkitt.
     * This function talks to the BluetoothSL4AAudioSrcMBS to simulate the media command.
     * An example test where this would be useful - Play music on Phone that is not connected
     * on bluetooth and connect in the middle to verify if music is steamed to the other end.
     *
     * @param command - Media command to simulate on the Phone
     */
    @Rpc(description = "Media Commands on the Phone's BluetoothAvrcpMBS.")
    public void bluetoothMediaHandleMediaCommandOnPhone(String command) {
        BluetoothSL4AAudioSrcMBS mbs =
                BluetoothSL4AAudioSrcMBS.getAvrcpMediaBrowserService();
        if (mbs != null) {
            mbs.handleMediaCommand(command);
        } else {
            Log.e(TAG + " No BluetoothSL4AAudioSrcMBS running on the device");
        }
    }


    @Override
    public void shutdown() {
        setCurrentMediaController(null);
    }
}