Basic framework support to test Avrcp/A2dp profiles.
Background and a design doc for the framework can be found in b/29097732
Add a MediaBrowserService to the test app for intercepting Media Keys.
Add a BluetoothMediaFacade for bluetooth media related test cases
Added an API to ConnectionFacade to disconnect on a specific list of profiles. Also re-factored some code here to share code.
Bug: 29097732, 28913426, 29100401
Change-Id: I1d172a1de342f19545f0a46f025effa7daf5ad4a
(cherry picked from commit 901bd7709544583e9497dc970105093180a5debf)
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothAvrcpMediaBrowserService.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothAvrcpMediaBrowserService.java
new file mode 100644
index 0000000..dfdc4c7
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothAvrcpMediaBrowserService.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * 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.media.browse.MediaBrowser.MediaItem;
+import android.media.session.*;
+import android.os.Bundle;
+import android.service.media.MediaBrowserService;
+
+import com.googlecode.android_scripting.facade.bluetooth.BluetoothMediaFacade;
+import com.googlecode.android_scripting.Log;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A {@link MediaBrowserService} implemented in the SL4A App to intercept Media keys and
+ * commands.
+ * This would be running on the AVRCP TG device and whenever the device receives a media
+ * command from a AVRCP CT, this MediaBrowserService's MediaSession would intercept it.
+ * Helps to verify the commands received by the AVRCP TG are the same as what was sent from
+ * an AVRCP CT.
+ */
+public class BluetoothAvrcpMediaBrowserService extends MediaBrowserService {
+ private static final String TAG = "BluetoothAvrcpMBS";
+ private static final String MEDIA_ROOT_ID = "__ROOT__";
+
+ private MediaSession mMediaSession = null;
+ private MediaSession.Token mSessionToken = null;
+ private MediaController mMediaController = null;
+
+ /**
+ * MediaSession callback dispatching the corresponding <code>PlaybackState</code> to
+ * {@link BluetoothMediaFacade}
+ */
+ private MediaSession.Callback mMediaSessionCallback =
+ new MediaSession.Callback() {
+ @Override
+ public void onPlay() {
+ Log.d(TAG + " onPlay");
+ BluetoothMediaFacade.dispatchPlaybackStateChanged(PlaybackState.STATE_PLAYING);
+ }
+
+ @Override
+ public void onPause() {
+ Log.d(TAG + " onPause");
+ BluetoothMediaFacade.dispatchPlaybackStateChanged(PlaybackState.STATE_PAUSED);
+ }
+
+ @Override
+ public void onRewind() {
+ Log.d(TAG + " onRewind");
+ BluetoothMediaFacade.dispatchPlaybackStateChanged(PlaybackState.STATE_REWINDING);
+ }
+
+ @Override
+ public void onFastForward() {
+ Log.d(TAG + " onFastForward");
+ BluetoothMediaFacade.dispatchPlaybackStateChanged(PlaybackState.STATE_FAST_FORWARDING);
+ }
+
+ @Override
+ public void onSkipToNext() {
+ Log.d(TAG + " onSkipToNext");
+ BluetoothMediaFacade.dispatchPlaybackStateChanged(PlaybackState.STATE_SKIPPING_TO_NEXT);
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ Log.d(TAG + " onSkipToPrevious");
+ BluetoothMediaFacade.dispatchPlaybackStateChanged(
+ PlaybackState.STATE_SKIPPING_TO_PREVIOUS);
+ }
+ };
+
+ /**
+ * We do the following on the AvrcpMediaBrowserService onCreate():
+ * 1. Create a new MediaSession
+ * 2. Register a callback with the created MediaSession
+ * 3. Set its playback state and set the session to active.
+ */
+ @Override
+ public void onCreate() {
+ Log.d(TAG + " onCreate");
+ super.onCreate();
+ mMediaSession = new MediaSession(this, TAG);
+ mSessionToken = mMediaSession.getSessionToken();
+ setSessionToken(mSessionToken);
+ mMediaSession.setCallback(mMediaSessionCallback);
+ mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS
+ | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
+ // Note - MediaButton Intent is not received until the session has a PlaybackState
+ // whose state is set to something other than STATE_STOPPED or STATE_NONE
+ PlaybackState state = new PlaybackState.Builder()
+ .setActions(PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PAUSE
+ | PlaybackState.ACTION_FAST_FORWARD | PlaybackState.ACTION_PLAY_PAUSE
+ | PlaybackState.ACTION_REWIND | PlaybackState.ACTION_SKIP_TO_NEXT
+ | PlaybackState.ACTION_SKIP_TO_PREVIOUS)
+ .setState(PlaybackState.STATE_PLAYING, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 1)
+ .build();
+ mMediaSession.setPlaybackState(state);
+ mMediaSession.setActive(true);
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.d(TAG + " onDestroy");
+ mMediaSession.release();
+ mMediaSession = null;
+ mSessionToken = null;
+ super.onDestroy();
+ }
+
+ @Override
+ public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
+ BrowserRoot mediaRoot = new BrowserRoot(MEDIA_ROOT_ID, null);
+ return mediaRoot;
+ }
+
+ @Override
+ public void onLoadChildren(String parentId, Result<List<MediaItem>> result) {
+ List<MediaItem> mediaList = new ArrayList<MediaItem>();
+ result.sendResult(mediaList);
+ }
+
+ /**
+ * Returns the TAG string
+ * @return <code>BluetoothAvrcpMediaBrowserService</code>'s tag
+ */
+ public static String getTag() {
+ return TAG;
+ }
+}
+
+
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothConnectionFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothConnectionFacade.java
index bcc5e38..e41d6c8 100644
--- a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothConnectionFacade.java
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothConnectionFacade.java
@@ -50,6 +50,9 @@
import com.googlecode.android_scripting.rpc.Rpc;
import com.googlecode.android_scripting.rpc.RpcParameter;
+import org.json.JSONArray;
+import org.json.JSONException;
+
public class BluetoothConnectionFacade extends RpcReceiver {
private final Service mService;
@@ -151,7 +154,7 @@
* Constructor
*
* @param deviceID Either the device alias name or mac address.
- * @param bond If true, bond the device only.
+ * @param bond If true, bond the device only.
*/
public DiscoverConnectReceiver(String deviceID) {
super();
@@ -169,7 +172,7 @@
mBluetoothAdapter.cancelDiscovery();
mDevice = device;
}
- // After discovery stops.
+ // After discovery stops.
} else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
if (mDevice == null) {
Log.d("Device " + mDeviceID + " not discovered.");
@@ -218,7 +221,7 @@
mBluetoothAdapter.cancelDiscovery();
mDevice = device;
}
- // After discovery stops.
+ // After discovery stops.
} else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
if (mDevice == null) {
Log.d("Device " + mDeviceID + " was not discovered.");
@@ -271,37 +274,50 @@
Log.e("Action devices does match act: " + device + " exp " + mDeviceID);
return;
}
+ // Find the state.
+ int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+ if (state == -1) {
+ Log.e("Action does not have a state.");
+ return;
+ }
+
+ String connState = "";
+ if (state == BluetoothProfile.STATE_CONNECTED) {
+ connState = "Connecting";
+ } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
+ connState = "Disconnecting";
+ }
int profile = -1;
String listener = "";
switch (action) {
case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED:
profile = BluetoothProfile.A2DP;
- listener = "A2dpConnecting" + mDeviceID;
+ listener = "A2dp" + connState + mDeviceID;
break;
case BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED:
profile = BluetoothProfile.INPUT_DEVICE;
- listener = "HidConnecting" + mDeviceID;
+ listener = "Hid" + connState + mDeviceID;
break;
case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED:
profile = BluetoothProfile.HEADSET;
- listener = "HspConnecting" + mDeviceID;
+ listener = "Hsp" + connState + mDeviceID;
break;
case BluetoothPan.ACTION_CONNECTION_STATE_CHANGED:
profile = BluetoothProfile.PAN;
- listener = "PanConnecting" + mDeviceID;
+ listener = "Pan" + connState + mDeviceID;
break;
case BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED:
profile = BluetoothProfile.HEADSET_CLIENT;
- listener = "HfpClientConnecting" + mDeviceID;
+ listener = "HfpClient" + connState + mDeviceID;
break;
case BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED:
profile = BluetoothProfile.A2DP_SINK;
- listener = "A2dpSinkConnecting" + mDeviceID;
+ listener = "A2dpSink" + connState + mDeviceID;
break;
case BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED:
profile = BluetoothProfile.PBAP_CLIENT;
- listener = "PbapClientConnecting" + mDeviceID;
+ listener = "PbapClient" + connState + mDeviceID;
break;
}
@@ -310,15 +326,10 @@
return;
}
- // Find the state.
- int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
- if (state == -1) {
- Log.e("Action does not have a state.");
- return;
- }
// Post an event to Facade.
- if (state == BluetoothProfile.STATE_CONNECTED) {
+ if ((state == BluetoothProfile.STATE_CONNECTED) || (state
+ == BluetoothProfile.STATE_DISCONNECTED)) {
Bundle news = new Bundle();
news.putInt("profile", profile);
news.putInt("state", state);
@@ -329,6 +340,66 @@
}
}
+ /**
+ * Converts a given JSONArray to an ArrayList of Integers
+ *
+ * @param jsonArray the JSONArray to be converted
+ * @return <code>List<Integer></></code> the converted list of Integers
+ */
+ private List<Integer> jsonArrayToIntegerList(JSONArray jsonArray) throws JSONException {
+ if (jsonArray == null) {
+ return null;
+ }
+ List<Integer> intArray = new ArrayList<Integer>();
+ for (int i = 0; i < jsonArray.length(); i++) {
+ intArray.add(jsonArray.getInt(i));
+ }
+ return intArray;
+
+ }
+
+ /**
+ * Helper function used by{@link connectProfile} & {@link disconnectProfiles} to handle a
+ * successful return from a profile connect/disconnect call
+ *
+ * @param deviceID The device id string
+ * @param profile String representation of the profile - used to create keys in the
+ * <code>listeningDevices</code> hashMap.
+ * @param connState String that denotes if this is called after a connect or disconnect
+ * call. Helps to share code between connect & disconnect
+ * @param profileFilter The <code>IntentFilter</code> for this profile that we want to listen
+ * on.
+ */
+ private void handleConnectionStateChanged(String deviceID, String profile, String connState,
+ IntentFilter profileFilter) {
+ Log.d(connState + " " + profile);
+ ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
+ mService.registerReceiver(receiver, profileFilter);
+ listeningDevices.put(profile + connState + deviceID, receiver);
+ }
+
+ /**
+ * Helper function used by{@link connectProfile} & {@link disconnectProfiles} to handle a
+ * unsuccessful return from a profile connect/disconnect call
+ *
+ * @param profile String representation of the profile - used to post an Event on the
+ * <code>mEventFacade</code>
+ * @param connState String that denotes if this is called after a connect or disconnect call.
+ * Helps to share code between connect & disconnect
+ */
+ private void handleConnectionStateChangeFailed(String profile, String connState) {
+ Log.d("Failed Starting " + profile + " " + connState);
+ Bundle badNews = (Bundle) mBadNews.clone();
+ badNews.putString("Type", profile);
+ mEventFacade.postEvent(connState, badNews);
+ }
+
+ /**
+ * Connect on all the profiles to the given Bluetooth device
+ *
+ * @param device The <code>BluetoothDevice</code> to connect to
+ * @param deviceID Name (String) of the device to connect to
+ */
private void connectProfile(BluetoothDevice device, String deviceID) {
mService.registerReceiver(mPairingHelper, mPairingFilter);
ParcelUuid[] deviceUuids = device.getUuids();
@@ -340,94 +411,72 @@
if (BluetoothUuid.containsAnyUuid(BluetoothA2dpFacade.SINK_UUIDS, deviceUuids)) {
boolean status = mA2dpProfile.a2dpConnect(device);
if (status) {
- Log.d("Connecting A2dp...");
- ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
- mService.registerReceiver(receiver, mA2dpStateChangeFilter);
- listeningDevices.put("A2dpConnecting" + deviceID, receiver);
+ handleConnectionStateChanged(deviceID, "A2dp", "Connecting", mA2dpStateChangeFilter);
} else {
- Log.d("Failed starting A2dp connection.");
- Bundle a2dpBadNews = (Bundle) mBadNews.clone();
- a2dpBadNews.putString("Type", "a2dp");
- mEventFacade.postEvent("Connect", a2dpBadNews);
+ handleConnectionStateChangeFailed("a2dp", "Connect");
}
}
if (BluetoothUuid.containsAnyUuid(BluetoothA2dpSinkFacade.SOURCE_UUIDS, deviceUuids)) {
boolean status = mA2dpSinkProfile.a2dpSinkConnect(device);
if (status) {
- Log.d("Connecting A2dp Sink...");
- ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
- mService.registerReceiver(receiver, mA2dpSinkStateChangeFilter);
- listeningDevices.put("A2dpSinkConnecting" + deviceID, receiver);
+ handleConnectionStateChanged(deviceID, "A2dpSink", "Connecting",
+ mA2dpSinkStateChangeFilter);
} else {
- Log.d("Failed starting A2dp Sink connection.");
- Bundle a2dpSinkBadNews = (Bundle) mBadNews.clone();
- a2dpSinkBadNews.putString("Type", "a2dpsink");
- mEventFacade.postEvent("Connect", a2dpSinkBadNews);
+ handleConnectionStateChangeFailed("a2dpsink", "Connect");
}
}
if (BluetoothUuid.containsAnyUuid(BluetoothHidFacade.UUIDS, deviceUuids)) {
boolean status = mHidProfile.hidConnect(device);
if (status) {
- Log.d("Connecting Hid...");
- ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
- mService.registerReceiver(receiver, mHidStateChangeFilter);
- listeningDevices.put("HidConnecting" + deviceID, receiver);
+ handleConnectionStateChanged(deviceID, "Hid", "Connecting", mHidStateChangeFilter);
} else {
- Log.d("Failed starting Hid connection.");
- mEventFacade.postEvent("HidConnect" + deviceID, mBadNews);
+ handleConnectionStateChangeFailed("Hid", "Connect");
}
}
if (BluetoothUuid.containsAnyUuid(BluetoothHspFacade.UUIDS, deviceUuids)) {
boolean status = mHspProfile.hspConnect(device);
if (status) {
- Log.d("Connecting Hsp...");
- ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
- mService.registerReceiver(receiver, mHspStateChangeFilter);
- listeningDevices.put("HspConnecting" + deviceID, receiver);
+ handleConnectionStateChanged(deviceID, "Hsp", "Connecting", mHspStateChangeFilter);
} else {
- Log.d("Failed starting Hsp connection.");
- mEventFacade.postEvent("HspConnect" + deviceID, mBadNews);
+ handleConnectionStateChangeFailed("Hsp", "Connect");
}
}
if (BluetoothUuid.containsAnyUuid(BluetoothHfpClientFacade.UUIDS, deviceUuids)) {
boolean status = mHfpClientProfile.hfpClientConnect(device);
if (status) {
- Log.d("Connecting HFP Client ...");
- ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
- mService.registerReceiver(receiver, mHfpClientStateChangeFilter);
- listeningDevices.put("HfpClientConnecting" + deviceID, receiver);
+ handleConnectionStateChanged(deviceID, "HfpClient", "Connecting",
+ mHfpClientStateChangeFilter);
} else {
- Log.d("Failed starting Hfp Client connection.");
- mEventFacade.postEvent("HfpClientConnect" + deviceID, mBadNews);
+ handleConnectionStateChangeFailed("HfpClient", "Connect");
}
}
if (BluetoothUuid.containsAnyUuid(BluetoothPanFacade.UUIDS, deviceUuids)) {
boolean status = mPanProfile.panConnect(device);
if (status) {
- Log.d("Connecting Pan...");
- ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
- mService.registerReceiver(receiver, mPanStateChangeFilter);
- listeningDevices.put("PanConnecting" + deviceID, receiver);
+ handleConnectionStateChanged(deviceID, "Pan", "Connecting",
+ mHfpClientStateChangeFilter);
} else {
- Log.d("Failed starting Pan connection.");
- mEventFacade.postEvent("PanConnect" + deviceID, mBadNews);
+ handleConnectionStateChangeFailed("Pan", "Connect");
}
}
if (BluetoothUuid.containsAnyUuid(BluetoothPbapClientFacade.UUIDS, deviceUuids)) {
boolean status = mPbapClientProfile.pbapClientConnect(device);
if (status) {
- Log.d("Connecting PBAP Client ...");
- ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
- mService.registerReceiver(receiver, mPbapClientStateChangeFilter);
- listeningDevices.put("PbapClientConnecting" + deviceID, receiver);
+ handleConnectionStateChanged(deviceID, "PbapClient", "Connecting",
+ mPbapClientStateChangeFilter);
} else {
- Log.d("Failed starting Pbap Client connection.");
- mEventFacade.postEvent("PbapClientConnect" + deviceID, mBadNews);
+ handleConnectionStateChangeFailed("PbapClient", "Connect");
}
}
mService.unregisterReceiver(mPairingHelper);
}
+ /**
+ * Disconnect on all available profiles from the given device
+ *
+ * @param device The <code>BluetoothDevice</code> to disconnect from
+ * @param deviceID Name (String) of the device to disconnect from
+ */
private void disconnectProfiles(BluetoothDevice device, String deviceID) {
Log.d("Disconnecting device " + device);
// Blindly disconnect all profiles. We may not have some of them connected so that will be a
@@ -441,6 +490,79 @@
mPanProfile.panDisconnect(device);
}
+ /**
+ * Disconnect from specific profiles provided in the given List of profiles.
+ *
+ * @param device The {@link BluetoothDevice} to disconnect from
+ * @param deviceID Name/BDADDR (String) of the device to disconnect from
+ * @param profileIds The list of profiles we want to disconnect on.
+ */
+ private void disconnectProfiles(BluetoothDevice device, String deviceID,
+ List<Integer> profileIds) {
+ boolean result;
+ for (int profileId : profileIds) {
+ switch (profileId) {
+ case BluetoothProfile.A2DP_SINK:
+ result = mA2dpSinkProfile.a2dpSinkDisconnect(device);
+ if (result) {
+ handleConnectionStateChanged(deviceID, "A2dpSink", "Disconnecting",
+ mA2dpSinkStateChangeFilter);
+ } else {
+ handleConnectionStateChangeFailed("a2dpsink", "Disconnect");
+ }
+ break;
+ case BluetoothProfile.A2DP:
+ result = mA2dpProfile.a2dpDisconnect(device);
+ if (result) {
+ handleConnectionStateChanged(deviceID, "A2dp", "Disconnecting",
+ mA2dpStateChangeFilter);
+ } else {
+ handleConnectionStateChangeFailed("a2dp", "Disconnect");
+ }
+ break;
+ case BluetoothProfile.INPUT_DEVICE:
+ result = mHidProfile.hidDisconnect(device);
+ if (result) {
+ handleConnectionStateChanged(deviceID, "Hid", "Disconnecting",
+ mHidStateChangeFilter);
+ } else {
+ handleConnectionStateChangeFailed("Hid", "Disconnect");
+ }
+ break;
+ case BluetoothProfile.HEADSET:
+ result = mHspProfile.hspDisconnect(device);
+ if (result) {
+ handleConnectionStateChanged(deviceID, "Hsp", "Disconnecting",
+ mHspStateChangeFilter);
+ } else {
+ handleConnectionStateChangeFailed("Hsp", "Disconnect");
+ }
+ break;
+ case BluetoothProfile.HEADSET_CLIENT:
+ result = mHfpClientProfile.hfpClientDisconnect(device);
+ if (result) {
+ handleConnectionStateChanged(deviceID, "HfpClient", "Disconnecting",
+ mHfpClientStateChangeFilter);
+ } else {
+ handleConnectionStateChangeFailed("HfpClient", "Disconnect");
+ }
+ break;
+ case BluetoothProfile.PBAP_CLIENT:
+ result = mPbapClientProfile.pbapClientDisconnect(device);
+ if (result) {
+ handleConnectionStateChanged(deviceID, "PbapClient", "Disconnecting",
+ mPbapClientStateChangeFilter);
+ } else {
+ handleConnectionStateChangeFailed("PbapClient", "Disconnect");
+ }
+ break;
+ default:
+ Log.d("Unknown Profile Id to disconnect from. Quitting");
+ return; // returns on the first unknown profile it encounters.
+ }
+ }
+ }
+
@Rpc(description = "Start intercepting all bluetooth connection pop-ups.")
public void bluetoothStartPairingHelper() {
mService.registerReceiver(mPairingHelper, mPairingFilter);
@@ -473,11 +595,11 @@
}
@Rpc(description = "Return list of connected bluetooth devices over a profile",
- returns = "List of devices connected over the profile")
+ returns = "List of devices connected over the profile")
public List<BluetoothDevice> bluetoothGetConnectedDevicesOnProfile(
@RpcParameter(name = "profileId",
- description = "profileId same as BluetoothProfile")
- Integer profileId) {
+ description = "profileId same as BluetoothProfile")
+ Integer profileId) {
BluetoothProfile profile = null;
switch (profileId) {
case BluetoothProfile.A2DP_SINK:
@@ -493,11 +615,11 @@
}
@Rpc(description = "Connect to a specified device once it's discovered.",
- returns = "Whether discovery started successfully.")
+ returns = "Whether discovery started successfully.")
public Boolean bluetoothDiscoverAndConnect(
@RpcParameter(name = "deviceID",
- description = "Name or MAC address of a bluetooth device.")
- String deviceID) {
+ description = "Name or MAC address of a bluetooth device.")
+ String deviceID) {
mBluetoothAdapter.cancelDiscovery();
if (listeningDevices.containsKey(deviceID)) {
Log.d("This device is already in the process of discovery and connecting.");
@@ -510,11 +632,11 @@
}
@Rpc(description = "Bond to a specified device once it's discovered.",
- returns = "Whether discovery started successfully. ")
+ returns = "Whether discovery started successfully. ")
public Boolean bluetoothDiscoverAndBond(
@RpcParameter(name = "deviceID",
- description = "Name or MAC address of a bluetooth device.")
- String deviceID) {
+ description = "Name or MAC address of a bluetooth device.")
+ String deviceID) {
mBluetoothAdapter.cancelDiscovery();
if (listeningDevices.containsKey(deviceID)) {
Log.d("This device is already in the process of discovery and bonding.");
@@ -536,11 +658,11 @@
}
@Rpc(description = "Unbond a device.",
- returns = "Whether the device was successfully unbonded.")
+ returns = "Whether the device was successfully unbonded.")
public Boolean bluetoothUnbond(
@RpcParameter(name = "deviceID",
- description = "Name or MAC address of a bluetooth device.")
- String deviceID) throws Exception {
+ description = "Name or MAC address of a bluetooth device.")
+ String deviceID) throws Exception {
BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
deviceID);
return mDevice.removeBond();
@@ -549,43 +671,56 @@
@Rpc(description = "Connect to a device that is already bonded.")
public void bluetoothConnectBonded(
@RpcParameter(name = "deviceID",
- description = "Name or MAC address of a bluetooth device.")
- String deviceID) throws Exception {
+ description = "Name or MAC address of a bluetooth device.")
+ String deviceID) throws Exception {
BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
deviceID);
connectProfile(mDevice, deviceID);
}
- // TODO: Split the disconnect RPC by profiles as well for granular control over the ACL
@Rpc(description = "Disconnect from a device that is already connected.")
public void bluetoothDisconnectConnected(
@RpcParameter(name = "deviceID",
- description = "Name or MAC address of a bluetooth device.")
- String deviceID) throws Exception {
+ description = "Name or MAC address of a bluetooth device.")
+ String deviceID) throws Exception {
BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
deviceID);
disconnectProfiles(mDevice, deviceID);
}
+ @Rpc(description = "Disconnect on a profile from a device that is already connected.")
+ public void bluetoothDisconnectConnectedProfile(
+ @RpcParameter(name = "deviceID",
+ description = "Name or MAC address of a bluetooth device.")
+ String deviceID,
+ @RpcParameter(name = "profileSet",
+ description = "List of profiles to disconnect from.")
+ JSONArray profileSet
+ ) throws Exception {
+ BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
+ deviceID);
+ disconnectProfiles(mDevice, deviceID, jsonArrayToIntegerList(profileSet));
+ }
+
@Rpc(description = "Change permissions for a profile.")
public void bluetoothChangeProfileAccessPermission(
@RpcParameter(name = "deviceID",
- description = "Name or MAC address of a bluetooth device.")
- String deviceID,
+ description = "Name or MAC address of a bluetooth device.")
+ String deviceID,
@RpcParameter(name = "profileID",
- description = "Number of Profile to change access permission")
- Integer profileID,
+ description = "Number of Profile to change access permission")
+ Integer profileID,
@RpcParameter(name = "access",
- description = "Access level 0 = Unknown, 1 = Allowed, 2 = Rejected")
- Integer access
- ) throws Exception {
+ description = "Access level 0 = Unknown, 1 = Allowed, 2 = Rejected")
+ Integer access
+ ) throws Exception {
if (access < 0 || access > 2) {
Log.w("Unsupported access level.");
return;
}
BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
deviceID);
- switch(profileID) {
+ switch (profileID) {
case BluetoothProfile.PBAP:
mDevice.setPhonebookAccessPermission(access);
break;
@@ -597,8 +732,12 @@
@Override
public void shutdown() {
- for(BroadcastReceiver receiver : listeningDevices.values()) {
- mService.unregisterReceiver(receiver);
+ for (BroadcastReceiver receiver : listeningDevices.values()) {
+ try {
+ mService.unregisterReceiver(receiver);
+ } catch (IllegalArgumentException ex) {
+ Log.e("Failed to unregister " + ex);
+ }
}
listeningDevices.clear();
mService.unregisterReceiver(mPairingHelper);
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothMediaFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothMediaFacade.java
new file mode 100644
index 0000000..6c6aef4
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothMediaFacade.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * 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.Intent;
+import android.content.ComponentName;
+import android.content.Context;
+
+import android.media.MediaMetadata;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.media.browse.MediaBrowser;
+import android.media.session.MediaController;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+import com.googlecode.android_scripting.Log;
+
+import java.util.List;
+
+/**
+ * SL4A Facade for dealing with Bluetooth Media related test cases
+ * The APIs here can be used on a AVRCP CT and TG, depending on what is tested.
+ */
+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 bluetoothPkgName = "com.android.bluetooth";
+ private static final String browserServiceName =
+ "com.android.bluetooth.a2dpsink.mbs.A2dpMediaBrowserService";
+
+ 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();
+ }
+
+ /**
+ * Class method called from {@link BluetoothAvrcpMediaBrowserService} to post an Event through
+ * EventFacade back to the RPC client.
+ *
+ * @param playbackState PlaybackState change that is posted as an Event to the client.
+ */
+ public static void dispatchPlaybackStateChanged(int playbackState) {
+ switch (playbackState) {
+ case PlaybackState.STATE_PLAYING:
+ mEventFacade.postEvent(EVENT_PLAY_RECEIVED, new Bundle());
+ break;
+ case PlaybackState.STATE_PAUSED:
+ mEventFacade.postEvent(EVENT_PAUSE_RECEIVED, new Bundle());
+ break;
+ case PlaybackState.STATE_SKIPPING_TO_NEXT:
+ mEventFacade.postEvent(EVENT_SKIP_NEXT_RECEIVED, new Bundle());
+ break;
+ case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
+ mEventFacade.postEvent(EVENT_SKIP_PREV_RECEIVED, new Bundle());
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * To register to MediaSession for callbacks on updates
+ */
+ 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 ");
+ }
+ }
+
+ /**
+ * To get the MediaController for the currently active MediaSession
+ */
+ private class SessionChangeListener
+ implements MediaSessionManager.OnActiveSessionsChangedListener {
+ @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());
+ }
+ }
+ // Whenever the list of ActiveSessions change, iterate through the list of active
+ // session and look for the one that belongs to the BluetoothAvrcpMediaBrowserService.
+ // If found, update our MediaController, we don't care for the other Active Media
+ // Sessions.
+ if (controllers.size() > 0) {
+ for (int i = 0; i < controllers.size(); i++) {
+ MediaController controller = (MediaController) controllers.get(i);
+ if (controller.getTag().contains(BluetoothAvrcpMediaBrowserService.getTag())) {
+ setCurrentMediaController(controller);
+ return;
+ }
+ }
+ setCurrentMediaController(null);
+ } else {
+ setCurrentMediaController(null);
+ }
+ }
+ }
+
+ /**
+ * Callback on <code>MediaBrowser.connect()</code>
+ */
+ 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 MediaController.
+ * On the AVRCP CT side (Carkitt for ex), this MediaController
+ * would be the one associated with the
+ * com.android.bluetooth.a2dpsink.mbs.A2dpMediaBrowserService.
+ * On the AVRCP TG side (Phone for ex), this MediaController would
+ * be the one associated with the
+ * com.googlecode.android_scripting.facade.bluetooth.BluetoothAvrcpMediaBrowserService
+ */
+ 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 diff media controller
+ 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) {
+ // The new media controller is null probably because it doesn't support Transport
+ // Controls
+ Log.d(TAG + " Clearing MediaController " + mMediaController.getTag());
+ mMediaController.unregisterCallback(mMediaCtrlCallback);
+ mMediaController = controller;
+ }
+ }
+
+ // 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
+ @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;
+ }
+ }
+
+ // This is usually called from the AVRCP CT device (Carkitt) to connect to the
+ // existing com.android.bluetooth.a2dpsink.mbs.A2dpMediaBrowserservice
+ @Rpc(description = "Connect a MediaBrowser to the A2dpMediaBrowserservice in the Carkitt")
+ public void bluetoothMediaConnectToA2dpMediaBrowserService() {
+ ComponentName compName;
+ // Create a MediaBrowser to connect to the A2dpMBS
+ if (mBrowser == null) {
+ compName = new ComponentName(bluetoothPkgName, browserServiceName);
+ // 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
+ }
+
+ // This is usually called from the AVRCP TG device (Phone)
+ @Rpc(description = "Start the BluetoothAvrcpMediaBrowserService.")
+ public void bluetoothMediaAvrcpMediaBrowserServiceStart() {
+ Log.d(TAG + "Staring BluetoothAvrcpMediaBrowserService");
+ // Start the Avrcp Media Browser service. Starting it sets it to active.
+ Intent startIntent = new Intent(mContext, BluetoothAvrcpMediaBrowserService.class);
+ mContext.startService(startIntent);
+ }
+
+ @Rpc(description = "Stop the BluetoothAvrcpMediaBrowserService.")
+ public void bluetoothMediaAvrcpMediaBrowserServiceStop() {
+ Log.d(TAG + "Stopping BluetoothAvrcpMediaBrowserService");
+ // Stop the Avrcp Media Browser service.
+ Intent stopIntent = new Intent(mContext, BluetoothAvrcpMediaBrowserService.class);
+ mContext.stopService(stopIntent);
+ }
+
+ @Override
+ public void shutdown() {
+ setCurrentMediaController(null);
+ }
+}
diff --git a/ScriptingLayer/src/com/googlecode/android_scripting/facade/FacadeConfiguration.java b/ScriptingLayer/src/com/googlecode/android_scripting/facade/FacadeConfiguration.java
index 866ab6b..a42ff81 100644
--- a/ScriptingLayer/src/com/googlecode/android_scripting/facade/FacadeConfiguration.java
+++ b/ScriptingLayer/src/com/googlecode/android_scripting/facade/FacadeConfiguration.java
@@ -39,6 +39,7 @@
import com.googlecode.android_scripting.facade.bluetooth.BluetoothLeAdvertiseFacade;
import com.googlecode.android_scripting.facade.bluetooth.BluetoothLeScanFacade;
import com.googlecode.android_scripting.facade.bluetooth.BluetoothMapFacade;
+import com.googlecode.android_scripting.facade.bluetooth.BluetoothMediaFacade;
import com.googlecode.android_scripting.facade.bluetooth.BluetoothPbapClientFacade;
import com.googlecode.android_scripting.facade.bluetooth.BluetoothPanFacade;
import com.googlecode.android_scripting.facade.bluetooth.BluetoothRfcommFacade;
@@ -122,6 +123,7 @@
sFacadeClassList.add(BluetoothHidFacade.class);
sFacadeClassList.add(BluetoothMapFacade.class);
sFacadeClassList.add(BluetoothPanFacade.class);
+ sFacadeClassList.add(BluetoothMediaFacade.class);
sFacadeClassList.add(BluetoothRfcommFacade.class);
sFacadeClassList.add(WebCamFacade.class);
sFacadeClassList.add(WifiP2pManagerFacade.class);
diff --git a/ScriptingLayerForAndroid/AndroidManifest.xml b/ScriptingLayerForAndroid/AndroidManifest.xml
index bce4fc7..eef182f 100644
--- a/ScriptingLayerForAndroid/AndroidManifest.xml
+++ b/ScriptingLayerForAndroid/AndroidManifest.xml
@@ -194,6 +194,11 @@
<action android:name="com.googlecode.android_scripting.service.FacadeService.ACTION_BIND" />
</intent-filter>
</service>
+ <service android:name=".facade.bluetooth.BluetoothAvrcpMediaBrowserService">
+ <intent-filter>
+ <action android:name="android.media.browse.MediaBrowserService"/>
+ </intent-filter>
+ </service>
<activity android:name=".activity.InterpreterManager" android:launchMode="singleTask" android:configChanges="keyboardHidden|orientation" />
<activity android:name=".activity.LogcatViewer" android:launchMode="singleTask" android:configChanges="keyboardHidden|orientation" />
<activity android:name=".activity.ScriptsLiveFolder" android:label="Scripts" android:icon="@drawable/live_folder" android:configChanges="keyboardHidden|orientation">