From 4e01d97997994bd3fdae1448ebbbba626d7fe50f Mon Sep 17 00:00:00 2001 From: Ang Li Date: Mon, 12 Jun 2017 10:39:47 -0700 Subject: 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. --- .../snippet/bundled/BluetoothAdapterSnippet.java | 230 ---------------- .../bundled/bluetooth/BluetoothAdapterSnippet.java | 298 +++++++++++++++++++++ .../bluetooth/PairingBroadcastReceiver.java | 30 +++ .../bluetooth/profiles/BluetoothA2dpSnippet.java | 113 ++++++++ .../snippet/bundled/utils/JsonSerializer.java | 10 + 5 files changed, 451 insertions(+), 230 deletions(-) delete mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/PairingBroadcastReceiver.java create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java deleted file mode 100644 index 0f4d3e6..0000000 --- a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright (C) 2017 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.google.android.mobly.snippet.bundled; - -import android.bluetooth.BluetoothAdapter; -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 android.os.Bundle; -import android.support.test.InstrumentationRegistry; -import com.google.android.mobly.snippet.Snippet; -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; -import org.json.JSONArray; -import org.json.JSONException; - -/** Snippet class exposing Android APIs in BluetoothAdapter. */ -public class BluetoothAdapterSnippet implements Snippet { - private static class BluetoothAdapterSnippetException extends Exception { - private static final long serialVersionUID = 1; - - public BluetoothAdapterSnippetException(String msg) { - super(msg); - } - } - - private final Context mContext; - private final BluetoothAdapter mBluetoothAdapter; - private final JsonSerializer mJsonSerializer = new JsonSerializer(); - private final ArrayList mDiscoveryResults = new ArrayList<>(); - private volatile boolean mIsScanResultAvailable = false; - - public BluetoothAdapterSnippet() { - mContext = InstrumentationRegistry.getContext(); - mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - } - - @Rpc(description = "Enable bluetooth with a 30s timeout.") - public void btEnable() throws BluetoothAdapterSnippetException, InterruptedException { - if (!mBluetoothAdapter.enable()) { - throw new BluetoothAdapterSnippetException("Failed to start enabling bluetooth"); - } - if (!Utils.waitUntil(() -> mBluetoothAdapter.isEnabled(), 30)) { - throw new BluetoothAdapterSnippetException("Bluetooth did not turn on within 30s."); - } - } - - @Rpc(description = "Disable bluetooth with a 30s timeout.") - public void btDisable() throws BluetoothAdapterSnippetException, InterruptedException { - if (!mBluetoothAdapter.disable()) { - throw new BluetoothAdapterSnippetException("Failed to start disabling bluetooth"); - } - if (!Utils.waitUntil(() -> !mBluetoothAdapter.isEnabled(), 30)) { - throw new BluetoothAdapterSnippetException("Bluetooth did not turn off within 30s."); - } - } - - @Rpc(description = "Return true if Bluetooth is enabled, false otherwise.") - public boolean btIsEnabled() { - return mBluetoothAdapter.isEnabled(); - } - - @Rpc( - 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; - } - - @Rpc(description = "Set the friendly Bluetooth name of the local Bluetooth adapter.") - public void btSetName(String name) throws BluetoothAdapterSnippetException { - if (!btIsEnabled()) { - throw new BluetoothAdapterSnippetException( - "Bluetooth is not enabled, cannot set Bluetooth name."); - } - if (!mBluetoothAdapter.setName(name)) { - throw new BluetoothAdapterSnippetException( - "Failed to set local Bluetooth name to " + name); - } - } - - @Rpc(description = "Get the friendly Bluetooth name of the local Bluetooth adapter.") - public String btGetName() { - return mBluetoothAdapter.getName(); - } - - @Rpc(description = "Returns the hardware address of the local Bluetooth adapter.") - public String btGetAddress() { - return mBluetoothAdapter.getAddress(); - } - - @Rpc( - description = - "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 { - IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); - filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); - if (mBluetoothAdapter.isDiscovering()) { - mBluetoothAdapter.cancelDiscovery(); - } - mDiscoveryResults.clear(); - mIsScanResultAvailable = false; - BroadcastReceiver receiver = new BluetoothScanReceiver(); - mContext.registerReceiver(receiver, filter); - try { - if (!mBluetoothAdapter.startDiscovery()) { - throw new BluetoothAdapterSnippetException( - "Failed to initiate Bluetooth Discovery."); - } - if (!Utils.waitUntil(() -> mIsScanResultAvailable, 120)) { - throw new BluetoothAdapterSnippetException( - "Failed to get discovery results after 2 mins, timeout!"); - } - } finally { - mContext.unregisterReceiver(receiver); - } - return btGetCachedScanResults(); - } - - @Rpc(description = "Become discoverable in Bluetooth.") - public void btBecomeDiscoverable(Integer duration) throws Throwable { - if (!btIsEnabled()) { - throw new BluetoothAdapterSnippetException( - "Bluetooth is not enabled, cannot become discoverable."); - } - if (!(boolean) - Utils.invokeByReflection( - mBluetoothAdapter, - "setScanMode", - BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, - duration)) { - throw new BluetoothAdapterSnippetException("Failed to become discoverable."); - } - } - - @Rpc(description = "Stop being discoverable in Bluetooth.") - public void btStopBeingDiscoverable() throws Throwable { - if (!(boolean) - Utils.invokeByReflection( - mBluetoothAdapter, - "setScanMode", - BluetoothAdapter.SCAN_MODE_NONE, - 0 /* duration is not used for this */)) { - throw new BluetoothAdapterSnippetException("Failed to stop being discoverable."); - } - } - - @Rpc(description = "Get the list of paired bluetooth devices.") - public ArrayList btGetPairedDevices() - throws BluetoothAdapterSnippetException, InterruptedException, JSONException { - ArrayList pairedDevices = new ArrayList<>(); - for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) { - pairedDevices.add(mJsonSerializer.serializeBluetoothDevice(device)); - } - return pairedDevices; - } - - /** - * Enable Bluetooth HCI snoop log collection. - * - *

The file can be pulled from `/sdcard/btsnoop_hci.log`. - * - * @throws Throwable - */ - @RpcMinSdk(Build.VERSION_CODES.KITKAT) - @Rpc(description = "Enable Bluetooth HCI snoop log for debugging.") - public void btEnableHciSnoopLog() throws Throwable { - if (!(boolean) Utils.invokeByReflection(mBluetoothAdapter, "configHciSnoopLog", true)) { - throw new BluetoothAdapterSnippetException("Failed to enable HCI snoop log."); - } - } - - @RpcMinSdk(Build.VERSION_CODES.KITKAT) - @Rpc(description = "Disable Bluetooth HCI snoop log.") - public void btDisableHciSnoopLog() throws Throwable { - if (!(boolean) Utils.invokeByReflection(mBluetoothAdapter, "configHciSnoopLog", false)) { - throw new BluetoothAdapterSnippetException("Failed to disable HCI snoop log."); - } - } - - @Override - public void shutdown() {} - - private class BluetoothScanReceiver extends BroadcastReceiver { - - /** - * The receiver gets an ACTION_FOUND intent whenever a new device is found. - * ACTION_DISCOVERY_FINISHED intent is received when the discovery process ends. - */ - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { - mIsScanResultAvailable = true; - } else if (BluetoothDevice.ACTION_FOUND.equals(action)) { - BluetoothDevice device = - (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - mDiscoveryResults.add(device); - } - } - } -} diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java new file mode 100644 index 0000000..5971517 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2017 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.google.android.mobly.snippet.bundled.bluetooth; + +import android.bluetooth.BluetoothAdapter; +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 android.os.Bundle; +import android.support.test.InstrumentationRegistry; +import com.google.android.mobly.snippet.Snippet; +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; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentHashMap; +import org.json.JSONException; + +/** Snippet class exposing Android APIs in BluetoothAdapter. */ +public class BluetoothAdapterSnippet implements Snippet { + private static class BluetoothAdapterSnippetException extends Exception { + private static final long serialVersionUID = 1; + + public BluetoothAdapterSnippetException(String msg) { + super(msg); + } + } + + private final Context mContext; + private static final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + private final JsonSerializer mJsonSerializer = new JsonSerializer(); + private static final ConcurrentHashMap mDiscoveryResults = + new ConcurrentHashMap<>(); + private volatile boolean mIsScanResultAvailable = false; + + public BluetoothAdapterSnippet() { + mContext = InstrumentationRegistry.getContext(); + } + + /** + * 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.") + public void btEnable() throws BluetoothAdapterSnippetException, InterruptedException { + if (!mBluetoothAdapter.enable()) { + throw new BluetoothAdapterSnippetException("Failed to start enabling bluetooth"); + } + if (!Utils.waitUntil(() -> mBluetoothAdapter.isEnabled(), 30)) { + throw new BluetoothAdapterSnippetException("Bluetooth did not turn on within 30s."); + } + } + + @Rpc(description = "Disable bluetooth with a 30s timeout.") + public void btDisable() throws BluetoothAdapterSnippetException, InterruptedException { + if (!mBluetoothAdapter.disable()) { + throw new BluetoothAdapterSnippetException("Failed to start disabling bluetooth"); + } + if (!Utils.waitUntil(() -> !mBluetoothAdapter.isEnabled(), 30)) { + throw new BluetoothAdapterSnippetException("Bluetooth did not turn off within 30s."); + } + } + + @Rpc(description = "Return true if Bluetooth is enabled, false otherwise.") + public boolean btIsEnabled() { + return mBluetoothAdapter.isEnabled(); + } + + @Rpc( + description = + "Get bluetooth discovery results, which is a list of serialized BluetoothDevice objects." + ) + public ArrayList btGetCachedScanResults() { + return mJsonSerializer.serializeBluetoothDeviceList(mDiscoveryResults.values()); + } + + @Rpc(description = "Set the friendly Bluetooth name of the local Bluetooth adapter.") + public void btSetName(String name) throws BluetoothAdapterSnippetException { + if (!btIsEnabled()) { + throw new BluetoothAdapterSnippetException( + "Bluetooth is not enabled, cannot set Bluetooth name."); + } + if (!mBluetoothAdapter.setName(name)) { + throw new BluetoothAdapterSnippetException( + "Failed to set local Bluetooth name to " + name); + } + } + + @Rpc(description = "Get the friendly Bluetooth name of the local Bluetooth adapter.") + public String btGetName() { + return mBluetoothAdapter.getName(); + } + + @Rpc(description = "Returns the hardware address of the local Bluetooth adapter.") + public String btGetAddress() { + return mBluetoothAdapter.getAddress(); + } + + @Rpc( + description = + "Start discovery, wait for discovery to complete, and return results, which is a list of " + + "serialized BluetoothDevice objects." + ) + public List btDiscoverAndGetResults() + throws InterruptedException, BluetoothAdapterSnippetException { + IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); + filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); + if (mBluetoothAdapter.isDiscovering()) { + mBluetoothAdapter.cancelDiscovery(); + } + mDiscoveryResults.clear(); + mIsScanResultAvailable = false; + BroadcastReceiver receiver = new BluetoothScanReceiver(); + mContext.registerReceiver(receiver, filter); + try { + if (!mBluetoothAdapter.startDiscovery()) { + throw new BluetoothAdapterSnippetException( + "Failed to initiate Bluetooth Discovery."); + } + if (!Utils.waitUntil(() -> mIsScanResultAvailable, 120)) { + throw new BluetoothAdapterSnippetException( + "Failed to get discovery results after 2 mins, timeout!"); + } + } finally { + mContext.unregisterReceiver(receiver); + } + return btGetCachedScanResults(); + } + + @Rpc(description = "Become discoverable in Bluetooth.") + public void btBecomeDiscoverable(Integer duration) throws Throwable { + if (!btIsEnabled()) { + throw new BluetoothAdapterSnippetException( + "Bluetooth is not enabled, cannot become discoverable."); + } + if (!(boolean) + Utils.invokeByReflection( + mBluetoothAdapter, + "setScanMode", + BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, + duration)) { + throw new BluetoothAdapterSnippetException("Failed to become discoverable."); + } + } + + @Rpc(description = "Stop being discoverable in Bluetooth.") + public void btStopBeingDiscoverable() throws Throwable { + if (!(boolean) + Utils.invokeByReflection( + mBluetoothAdapter, + "setScanMode", + BluetoothAdapter.SCAN_MODE_NONE, + 0 /* duration is not used for this */)) { + throw new BluetoothAdapterSnippetException("Failed to stop being discoverable."); + } + } + + @Rpc(description = "Get the list of paired bluetooth devices.") + public List btGetPairedDevices() + throws BluetoothAdapterSnippetException, InterruptedException, JSONException { + ArrayList pairedDevices = new ArrayList<>(); + for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) { + pairedDevices.add(mJsonSerializer.serializeBluetoothDevice(device)); + } + 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. + * + *

The file can be pulled from `/sdcard/btsnoop_hci.log`. + * + * @throws Throwable + */ + @RpcMinSdk(Build.VERSION_CODES.KITKAT) + @Rpc(description = "Enable Bluetooth HCI snoop log for debugging.") + public void btEnableHciSnoopLog() throws Throwable { + if (!(boolean) Utils.invokeByReflection(mBluetoothAdapter, "configHciSnoopLog", true)) { + throw new BluetoothAdapterSnippetException("Failed to enable HCI snoop log."); + } + } + + @RpcMinSdk(Build.VERSION_CODES.KITKAT) + @Rpc(description = "Disable Bluetooth HCI snoop log.") + public void btDisableHciSnoopLog() throws Throwable { + if (!(boolean) Utils.invokeByReflection(mBluetoothAdapter, "configHciSnoopLog", false)) { + throw new BluetoothAdapterSnippetException("Failed to disable HCI snoop log."); + } + } + + @Override + public void shutdown() {} + + private class BluetoothScanReceiver extends BroadcastReceiver { + + /** + * The receiver gets an ACTION_FOUND intent whenever a new device is found. + * ACTION_DISCOVERY_FINISHED intent is received when the discovery process ends. + */ + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { + mIsScanResultAvailable = true; + } else if (BluetoothDevice.ACTION_FOUND.equals(action)) { + BluetoothDevice device = + (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_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 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 serializeBluetoothDeviceList( + Collection bluetoothDevices) { + ArrayList 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(); -- cgit v1.2.3