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/bluetooth | |
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/bluetooth')
3 files changed, 441 insertions, 0 deletions
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<String, BluetoothDevice> 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<Bundle> 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<Bundle> 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<Bundle> btGetPairedDevices() + throws BluetoothAdapterSnippetException, InterruptedException, JSONException { + ArrayList<Bundle> 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. + * + * <p>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<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() {} +} |