diff options
author | Ang Li <angli@google.com> | 2017-06-12 10:39:47 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-06-12 10:39:47 -0700 |
commit | 4e01d97997994bd3fdae1448ebbbba626d7fe50f (patch) | |
tree | b53b343516919ca903cddec92e358e58c28e851d /src/main/java/com/google/android/mobly/snippet/bundled | |
parent | ec29dcc0819d1e31af39f3c1102f5349fa29ed88 (diff) | |
download | mobly-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.java | 30 | ||||
-rw-r--r-- | src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java | 113 | ||||
-rw-r--r-- | src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java | 10 |
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(); |