blob: 404a791172bfd4dc0387f0d6b60dbdb742dc3841 [file] [log] [blame]
/*
* 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);
}
}