aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/google/android/mobly/snippet/bundled
diff options
context:
space:
mode:
authorAng Li <angli@google.com>2017-06-12 10:39:47 -0700
committerGitHub <noreply@github.com>2017-06-12 10:39:47 -0700
commit4e01d97997994bd3fdae1448ebbbba626d7fe50f (patch)
treeb53b343516919ca903cddec92e358e58c28e851d /src/main/java/com/google/android/mobly/snippet/bundled
parentec29dcc0819d1e31af39f3c1102f5349fa29ed88 (diff)
downloadmobly-bundled-snippets-4e01d97997994bd3fdae1448ebbbba626d7fe50f.tar.gz
Add basic Bluetooth pairing and A2DP connection features. (#54)
* Add support for pairing/unpairing a device. * Add support for connecting with A2DP profile. * Reorganize Bluetooth related snippets for future expansion of profile support. * Use Bundle instead of JSONObject for BluetoothDevice serialization.
Diffstat (limited to 'src/main/java/com/google/android/mobly/snippet/bundled')
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java (renamed from src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java)98
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/PairingBroadcastReceiver.java30
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java113
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java10
4 files changed, 236 insertions, 15 deletions
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java
index 0f4d3e6..5971517 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java
@@ -14,7 +14,7 @@
* the License.
*/
-package com.google.android.mobly.snippet.bundled;
+package com.google.android.mobly.snippet.bundled.bluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
@@ -31,7 +31,9 @@ import com.google.android.mobly.snippet.bundled.utils.Utils;
import com.google.android.mobly.snippet.rpc.Rpc;
import com.google.android.mobly.snippet.rpc.RpcMinSdk;
import java.util.ArrayList;
-import org.json.JSONArray;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.concurrent.ConcurrentHashMap;
import org.json.JSONException;
/** Snippet class exposing Android APIs in BluetoothAdapter. */
@@ -45,14 +47,44 @@ public class BluetoothAdapterSnippet implements Snippet {
}
private final Context mContext;
- private final BluetoothAdapter mBluetoothAdapter;
+ private static final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
private final JsonSerializer mJsonSerializer = new JsonSerializer();
- private final ArrayList<BluetoothDevice> mDiscoveryResults = new ArrayList<>();
+ private static final ConcurrentHashMap<String, BluetoothDevice> mDiscoveryResults =
+ new ConcurrentHashMap<>();
private volatile boolean mIsScanResultAvailable = false;
public BluetoothAdapterSnippet() {
mContext = InstrumentationRegistry.getContext();
- mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ }
+
+ /**
+ * Gets a {@link BluetoothDevice} that has either been paired or discovered.
+ *
+ * @param deviceAddress
+ * @return
+ */
+ public static BluetoothDevice getKnownDeviceByAddress(String deviceAddress) {
+ BluetoothDevice pairedDevice = getPairedDeviceByAddress(deviceAddress);
+ if (pairedDevice != null) {
+ return pairedDevice;
+ }
+ BluetoothDevice discoveredDevice = mDiscoveryResults.get(deviceAddress);
+ if (discoveredDevice != null) {
+ return discoveredDevice;
+ }
+ throw new NoSuchElementException(
+ "No device with address "
+ + deviceAddress
+ + " is paired or has been discovered. Cannot proceed.");
+ }
+
+ private static BluetoothDevice getPairedDeviceByAddress(String deviceAddress) {
+ for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) {
+ if (device.getAddress().equalsIgnoreCase(deviceAddress)) {
+ return device;
+ }
+ }
+ return null;
}
@Rpc(description = "Enable bluetooth with a 30s timeout.")
@@ -84,12 +116,8 @@ public class BluetoothAdapterSnippet implements Snippet {
description =
"Get bluetooth discovery results, which is a list of serialized BluetoothDevice objects."
)
- public JSONArray btGetCachedScanResults() throws JSONException {
- JSONArray results = new JSONArray();
- for (BluetoothDevice result : mDiscoveryResults) {
- results.put(mJsonSerializer.toJson(result));
- }
- return results;
+ public ArrayList<Bundle> btGetCachedScanResults() {
+ return mJsonSerializer.serializeBluetoothDeviceList(mDiscoveryResults.values());
}
@Rpc(description = "Set the friendly Bluetooth name of the local Bluetooth adapter.")
@@ -119,8 +147,8 @@ public class BluetoothAdapterSnippet implements Snippet {
"Start discovery, wait for discovery to complete, and return results, which is a list of "
+ "serialized BluetoothDevice objects."
)
- public JSONArray btDiscoverAndGetResults()
- throws InterruptedException, JSONException, BluetoothAdapterSnippetException {
+ public List<Bundle> btDiscoverAndGetResults()
+ throws InterruptedException, BluetoothAdapterSnippetException {
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
if (mBluetoothAdapter.isDiscovering()) {
@@ -174,7 +202,7 @@ public class BluetoothAdapterSnippet implements Snippet {
}
@Rpc(description = "Get the list of paired bluetooth devices.")
- public ArrayList<Bundle> btGetPairedDevices()
+ public List<Bundle> btGetPairedDevices()
throws BluetoothAdapterSnippetException, InterruptedException, JSONException {
ArrayList<Bundle> pairedDevices = new ArrayList<>();
for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) {
@@ -183,6 +211,46 @@ public class BluetoothAdapterSnippet implements Snippet {
return pairedDevices;
}
+ @Rpc(description = "Pair with a bluetooth device.")
+ public void btPairDevice(String deviceAddress) throws Throwable {
+ BluetoothDevice device = mDiscoveryResults.get(deviceAddress);
+ if (device == null) {
+ throw new NoSuchElementException(
+ "No device with address "
+ + deviceAddress
+ + " has been discovered. Cannot proceed.");
+ }
+ mContext.registerReceiver(
+ new PairingBroadcastReceiver(mContext), PairingBroadcastReceiver.filter);
+ if (!(boolean) Utils.invokeByReflection(device, "createBond")) {
+ throw new BluetoothAdapterSnippetException(
+ "Failed to initiate the pairing process to device: " + deviceAddress);
+ }
+ if (!Utils.waitUntil(() -> device.getBondState() == BluetoothDevice.BOND_BONDED, 120)) {
+ throw new BluetoothAdapterSnippetException(
+ "Failed to pair with device " + deviceAddress + " after 2min.");
+ }
+ }
+
+ @Rpc(description = "Un-pair a bluetooth device.")
+ public void btUnpairDevice(String deviceAddress) throws Throwable {
+ for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) {
+ if (device.getAddress().equalsIgnoreCase(deviceAddress)) {
+ if (!(boolean) Utils.invokeByReflection(device, "removeBond")) {
+ throw new BluetoothAdapterSnippetException(
+ "Failed to initiate the un-pairing process for device: "
+ + deviceAddress);
+ }
+ if (!Utils.waitUntil(
+ () -> device.getBondState() == BluetoothDevice.BOND_NONE, 30)) {
+ throw new BluetoothAdapterSnippetException(
+ "Failed to un-pair device " + deviceAddress + " after 30s.");
+ }
+ }
+ }
+ throw new NoSuchElementException("No device wih address " + deviceAddress + " is paired.");
+ }
+
/**
* Enable Bluetooth HCI snoop log collection.
*
@@ -223,7 +291,7 @@ public class BluetoothAdapterSnippet implements Snippet {
} else if (BluetoothDevice.ACTION_FOUND.equals(action)) {
BluetoothDevice device =
(BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
- mDiscoveryResults.add(device);
+ mDiscoveryResults.put(device.getAddress(), device);
}
}
}
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/PairingBroadcastReceiver.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/PairingBroadcastReceiver.java
new file mode 100644
index 0000000..0cfd362
--- /dev/null
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/PairingBroadcastReceiver.java
@@ -0,0 +1,30 @@
+package com.google.android.mobly.snippet.bundled.bluetooth;
+
+import android.annotation.TargetApi;
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build;
+import com.google.android.mobly.snippet.util.Log;
+
+@TargetApi(Build.VERSION_CODES.KITKAT)
+public class PairingBroadcastReceiver extends BroadcastReceiver {
+ private final Context mContext;
+ public static IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST);
+
+ public PairingBroadcastReceiver(Context context) {
+ mContext = context;
+ }
+
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(BluetoothDevice.ACTION_PAIRING_REQUEST)) {
+ BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ Log.d("Confirming pairing with device: " + device.getAddress());
+ device.setPairingConfirmation(true);
+ mContext.unregisterReceiver(this);
+ }
+ }
+}
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java
new file mode 100644
index 0000000..b218723
--- /dev/null
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java
@@ -0,0 +1,113 @@
+package com.google.android.mobly.snippet.bundled.bluetooth.profiles;
+
+import android.annotation.TargetApi;
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.content.IntentFilter;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.bundled.bluetooth.BluetoothAdapterSnippet;
+import com.google.android.mobly.snippet.bundled.bluetooth.PairingBroadcastReceiver;
+import com.google.android.mobly.snippet.bundled.utils.JsonSerializer;
+import com.google.android.mobly.snippet.bundled.utils.Utils;
+import com.google.android.mobly.snippet.rpc.Rpc;
+import com.google.android.mobly.snippet.rpc.RpcMinSdk;
+import java.util.ArrayList;
+
+public class BluetoothA2dpSnippet implements Snippet {
+ private static class BluetoothA2dpSnippetException extends Exception {
+ private static final long serialVersionUID = 1;
+
+ BluetoothA2dpSnippetException(String msg) {
+ super(msg);
+ }
+ }
+
+ private Context mContext;
+ private static boolean sIsA2dpProfileReady = false;
+ private static BluetoothA2dp sA2dpProfile;
+ private final JsonSerializer mJsonSerializer = new JsonSerializer();
+
+ public BluetoothA2dpSnippet() {
+ mContext = InstrumentationRegistry.getContext();
+ BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ bluetoothAdapter.getProfileProxy(
+ mContext, new A2dpServiceListener(), BluetoothProfile.A2DP);
+ Utils.waitUntil(() -> sIsA2dpProfileReady, 60);
+ }
+
+ private static class A2dpServiceListener implements BluetoothProfile.ServiceListener {
+ public void onServiceConnected(int var1, BluetoothProfile profile) {
+ sA2dpProfile = (BluetoothA2dp) profile;
+ sIsA2dpProfileReady = true;
+ }
+
+ public void onServiceDisconnected(int var1) {
+ sIsA2dpProfileReady = false;
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ @RpcMinSdk(Build.VERSION_CODES.KITKAT)
+ @Rpc(
+ description =
+ "Connects to a paired or discovered device with A2DP profile."
+ + "If a device has been discovered but not paired, this will pair it."
+ )
+ public void btA2dpConnect(String deviceAddress) throws Throwable {
+ BluetoothDevice device = BluetoothAdapterSnippet.getKnownDeviceByAddress(deviceAddress);
+ IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST);
+ mContext.registerReceiver(new PairingBroadcastReceiver(mContext), filter);
+ Utils.invokeByReflection(sA2dpProfile, "connect", device);
+ if (!Utils.waitUntil(
+ () -> sA2dpProfile.getConnectionState(device) == BluetoothA2dp.STATE_CONNECTED,
+ 120)) {
+ throw new BluetoothA2dpSnippetException(
+ "Failed to connect to device "
+ + device.getName()
+ + "|"
+ + device.getAddress()
+ + " with A2DP profile within 2min.");
+ }
+ }
+
+ @Rpc(description = "Disconnects a device from A2DP profile.")
+ public void btA2dpDisconnect(String deviceAddress) throws Throwable {
+ BluetoothDevice device = getConnectedBluetoothDevice(deviceAddress);
+ Utils.invokeByReflection(sA2dpProfile, "disconnect", device);
+ if (!Utils.waitUntil(
+ () -> sA2dpProfile.getConnectionState(device) == BluetoothA2dp.STATE_DISCONNECTED,
+ 120)) {
+ throw new BluetoothA2dpSnippetException(
+ "Failed to disconnect device "
+ + device.getName()
+ + "|"
+ + device.getAddress()
+ + " from A2DP profile within 2min.");
+ }
+ }
+
+ @Rpc(description = "Gets all the devices currently connected via A2DP profile.")
+ public ArrayList<Bundle> btA2dpGetConnectedDevices() {
+ return mJsonSerializer.serializeBluetoothDeviceList(sA2dpProfile.getConnectedDevices());
+ }
+
+ private BluetoothDevice getConnectedBluetoothDevice(String deviceAddress)
+ throws BluetoothA2dpSnippetException {
+ for (BluetoothDevice device : sA2dpProfile.getConnectedDevices()) {
+ if (device.getAddress().equalsIgnoreCase(deviceAddress)) {
+ return device;
+ }
+ }
+ throw new BluetoothA2dpSnippetException(
+ "No device with address " + deviceAddress + " is connected via A2DP.");
+ }
+
+ @Override
+ public void shutdown() {}
+}
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java
index c555b92..7e5ca5b 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java
@@ -33,6 +33,7 @@ import java.lang.reflect.Modifier;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
+import java.util.Collection;
import org.json.JSONException;
import org.json.JSONObject;
@@ -148,6 +149,15 @@ public class JsonSerializer {
return result;
}
+ public ArrayList<Bundle> serializeBluetoothDeviceList(
+ Collection<BluetoothDevice> bluetoothDevices) {
+ ArrayList<Bundle> results = new ArrayList<>();
+ for (BluetoothDevice device : bluetoothDevices) {
+ results.add(serializeBluetoothDevice(device));
+ }
+ return results;
+ }
+
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public Bundle serializeBleScanResult(android.bluetooth.le.ScanResult scanResult) {
Bundle result = new Bundle();