| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.device.bluetooth; |
| |
| import android.Manifest; |
| import android.annotation.SuppressLint; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothGatt; |
| import android.bluetooth.BluetoothGattCallback; |
| import android.bluetooth.BluetoothGattCharacteristic; |
| import android.bluetooth.BluetoothGattDescriptor; |
| import android.bluetooth.BluetoothGattService; |
| import android.bluetooth.le.BluetoothLeScanner; |
| import android.bluetooth.le.ScanCallback; |
| import android.bluetooth.le.ScanFilter; |
| import android.bluetooth.le.ScanResult; |
| import android.bluetooth.le.ScanSettings; |
| import android.content.Context; |
| import android.content.pm.PackageManager; |
| import android.os.Build; |
| import android.os.ParcelUuid; |
| import android.util.SparseArray; |
| |
| import org.jni_zero.CalledByNative; |
| import org.jni_zero.JNINamespace; |
| |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.Log; |
| import org.chromium.base.ThreadUtils; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.UUID; |
| |
| /** |
| * Wrapper classes around android.bluetooth.* classes that provide an |
| * indirection layer enabling fake implementations when running tests. |
| * |
| * Each Wrapper base class accepts an Android API object and passes through |
| * calls to it. When under test, Fake subclasses override all methods that |
| * pass through to the Android object and instead provide fake implementations. |
| */ |
| @JNINamespace("device") |
| class Wrappers { |
| private static final String TAG = "Bluetooth"; |
| |
| public static final int DEVICE_CLASS_UNSPECIFIED = 0x1F00; |
| |
| /** |
| * Wraps base.ThreadUtils. |
| * base.ThreadUtils has a set of static method to interact with the |
| * UI Thread. To be able to provide a set of test methods, ThreadUtilsWrapper |
| * uses the factory pattern. |
| */ |
| static class ThreadUtilsWrapper { |
| private static Factory sFactory; |
| |
| private static ThreadUtilsWrapper sInstance; |
| |
| protected ThreadUtilsWrapper() {} |
| |
| /** |
| * Returns the singleton instance of ThreadUtilsWrapper, creating it if needed. |
| */ |
| public static ThreadUtilsWrapper getInstance() { |
| if (sInstance == null) { |
| if (sFactory == null) { |
| sInstance = new ThreadUtilsWrapper(); |
| } else { |
| sInstance = sFactory.create(); |
| } |
| } |
| return sInstance; |
| } |
| |
| public void runOnUiThread(Runnable r) { |
| ThreadUtils.runOnUiThread(r); |
| } |
| |
| /** |
| * Instantiate this to explain how to create a ThreadUtilsWrapper instance in |
| * ThreadUtilsWrapper.getInstance(). |
| */ |
| public interface Factory { public ThreadUtilsWrapper create(); } |
| |
| /** |
| * Call this to use a different subclass of ThreadUtilsWrapper throughout the program. |
| */ |
| public static void setFactory(Factory factory) { |
| sFactory = factory; |
| sInstance = null; |
| } |
| } |
| |
| /** |
| * Wraps android.bluetooth.BluetoothAdapter. |
| */ |
| static class BluetoothAdapterWrapper { |
| private final BluetoothAdapter mAdapter; |
| protected final Context mContext; |
| protected BluetoothLeScannerWrapper mScannerWrapper; |
| |
| /** |
| * Creates a BluetoothAdapterWrapper using the default |
| * android.bluetooth.BluetoothAdapter. May fail if the default adapter |
| * is not available or if the application does not have sufficient |
| * permissions. |
| */ |
| @CalledByNative("BluetoothAdapterWrapper") |
| public static BluetoothAdapterWrapper createWithDefaultAdapter() { |
| // In Android Q and earlier the BLUETOOTH and BLUETOOTH_ADMIN permissions must be |
| // granted in the manifest. In Android S and later the BLUETOOTH_SCAN and |
| // BLUETOOTH_CONNECT permissions can be requested at runtime after fetching the default |
| // adapter. |
| if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { |
| final boolean hasPermission = |
| ContextUtils.getApplicationContext().checkCallingOrSelfPermission( |
| Manifest.permission.BLUETOOTH) |
| == PackageManager.PERMISSION_GRANTED |
| && ContextUtils.getApplicationContext().checkCallingOrSelfPermission( |
| Manifest.permission.BLUETOOTH_ADMIN) |
| == PackageManager.PERMISSION_GRANTED; |
| |
| if (!hasPermission) { |
| Log.w( |
| TAG, |
| "BluetoothAdapterWrapper.create failed: Lacking Bluetooth" |
| + " permissions."); |
| return null; |
| } |
| } |
| |
| // Only Low Energy currently supported, see BluetoothAdapterAndroid class note. |
| final boolean hasLowEnergyFeature = |
| ContextUtils.getApplicationContext().getPackageManager().hasSystemFeature( |
| PackageManager.FEATURE_BLUETOOTH_LE); |
| if (!hasLowEnergyFeature) { |
| Log.i(TAG, "BluetoothAdapterWrapper.create failed: No Low Energy support."); |
| return null; |
| } |
| |
| BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); |
| if (adapter == null) { |
| Log.i(TAG, "BluetoothAdapterWrapper.create failed: Default adapter not found."); |
| return null; |
| } else { |
| return new BluetoothAdapterWrapper(adapter, ContextUtils.getApplicationContext()); |
| } |
| } |
| |
| public BluetoothAdapterWrapper(BluetoothAdapter adapter, Context context) { |
| mAdapter = adapter; |
| mContext = context; |
| } |
| |
| public boolean disable() { |
| return mAdapter.disable(); |
| } |
| |
| public boolean enable() { |
| return mAdapter.enable(); |
| } |
| |
| @SuppressLint("HardwareIds") |
| public String getAddress() { |
| return mAdapter.getAddress(); |
| } |
| |
| public BluetoothLeScannerWrapper getBluetoothLeScanner() { |
| BluetoothLeScanner scanner = mAdapter.getBluetoothLeScanner(); |
| if (scanner == null) { |
| return null; |
| } |
| if (mScannerWrapper == null) { |
| mScannerWrapper = new BluetoothLeScannerWrapper(scanner); |
| } |
| return mScannerWrapper; |
| } |
| |
| public Context getContext() { |
| return mContext; |
| } |
| |
| public String getName() { |
| return mAdapter.getName(); |
| } |
| |
| public int getScanMode() { |
| return mAdapter.getScanMode(); |
| } |
| |
| public boolean isDiscovering() { |
| return mAdapter.isDiscovering(); |
| } |
| |
| public boolean isEnabled() { |
| return mAdapter.isEnabled(); |
| } |
| } |
| |
| /** |
| * Wraps android.bluetooth.BluetoothLeScanner. |
| */ |
| static class BluetoothLeScannerWrapper { |
| protected final BluetoothLeScanner mScanner; |
| private final HashMap<ScanCallbackWrapper, ForwardScanCallbackToWrapper> mCallbacks; |
| |
| public BluetoothLeScannerWrapper(BluetoothLeScanner scanner) { |
| mScanner = scanner; |
| mCallbacks = new HashMap<ScanCallbackWrapper, ForwardScanCallbackToWrapper>(); |
| } |
| |
| public void startScan( |
| List<ScanFilter> filters, int scanSettingsScanMode, ScanCallbackWrapper callback) { |
| ScanSettings settings = |
| new ScanSettings.Builder().setScanMode(scanSettingsScanMode).build(); |
| |
| ForwardScanCallbackToWrapper callbackForwarder = |
| new ForwardScanCallbackToWrapper(callback); |
| mCallbacks.put(callback, callbackForwarder); |
| |
| mScanner.startScan(filters, settings, callbackForwarder); |
| } |
| |
| public void stopScan(ScanCallbackWrapper callback) { |
| ForwardScanCallbackToWrapper callbackForwarder = mCallbacks.remove(callback); |
| mScanner.stopScan(callbackForwarder); |
| } |
| } |
| |
| /** |
| * Implements android.bluetooth.le.ScanCallback and forwards calls through to a |
| * provided ScanCallbackWrapper instance. |
| * |
| * This class is required so that Fakes can use ScanCallbackWrapper without |
| * it extending from ScanCallback. Fakes must function even on Android |
| * versions where ScanCallback class is not defined. |
| */ |
| static class ForwardScanCallbackToWrapper extends ScanCallback { |
| final ScanCallbackWrapper mWrapperCallback; |
| |
| ForwardScanCallbackToWrapper(ScanCallbackWrapper wrapperCallback) { |
| mWrapperCallback = wrapperCallback; |
| } |
| |
| @Override |
| public void onBatchScanResults(List<ScanResult> results) { |
| ArrayList<ScanResultWrapper> resultsWrapped = |
| new ArrayList<ScanResultWrapper>(results.size()); |
| for (ScanResult result : results) { |
| resultsWrapped.add(new ScanResultWrapper(result)); |
| } |
| mWrapperCallback.onBatchScanResult(resultsWrapped); |
| } |
| |
| @Override |
| public void onScanResult(int callbackType, ScanResult result) { |
| mWrapperCallback.onScanResult(callbackType, new ScanResultWrapper(result)); |
| } |
| |
| @Override |
| public void onScanFailed(int errorCode) { |
| mWrapperCallback.onScanFailed(errorCode); |
| } |
| } |
| |
| /** |
| * Wraps android.bluetooth.le.ScanCallback, being called by ScanCallbackImpl. |
| */ |
| abstract static class ScanCallbackWrapper { |
| public abstract void onBatchScanResult(List<ScanResultWrapper> results); |
| public abstract void onScanResult(int callbackType, ScanResultWrapper result); |
| public abstract void onScanFailed(int errorCode); |
| } |
| |
| /** |
| * Wraps android.bluetooth.le.ScanResult. |
| */ |
| static class ScanResultWrapper { |
| private final ScanResult mScanResult; |
| |
| public ScanResultWrapper(ScanResult scanResult) { |
| mScanResult = scanResult; |
| } |
| |
| public BluetoothDeviceWrapper getDevice() { |
| return new BluetoothDeviceWrapper(mScanResult.getDevice()); |
| } |
| |
| public int getRssi() { |
| return mScanResult.getRssi(); |
| } |
| |
| public List<ParcelUuid> getScanRecord_getServiceUuids() { |
| return mScanResult.getScanRecord().getServiceUuids(); |
| } |
| |
| public Map<ParcelUuid, byte[]> getScanRecord_getServiceData() { |
| return mScanResult.getScanRecord().getServiceData(); |
| } |
| |
| public SparseArray<byte[]> getScanRecord_getManufacturerSpecificData() { |
| return mScanResult.getScanRecord().getManufacturerSpecificData(); |
| } |
| |
| public int getScanRecord_getTxPowerLevel() { |
| return mScanResult.getScanRecord().getTxPowerLevel(); |
| } |
| |
| public String getScanRecord_getDeviceName() { |
| return mScanResult.getScanRecord().getDeviceName(); |
| } |
| |
| public int getScanRecord_getAdvertiseFlags() { |
| return mScanResult.getScanRecord().getAdvertiseFlags(); |
| } |
| } |
| |
| /** |
| * Wraps android.bluetooth.BluetoothDevice. |
| */ |
| static class BluetoothDeviceWrapper { |
| private final BluetoothDevice mDevice; |
| private final HashMap<BluetoothGattCharacteristic, BluetoothGattCharacteristicWrapper> |
| mCharacteristicsToWrappers; |
| private final HashMap<BluetoothGattDescriptor, BluetoothGattDescriptorWrapper> |
| mDescriptorsToWrappers; |
| |
| public BluetoothDeviceWrapper(BluetoothDevice device) { |
| mDevice = device; |
| mCharacteristicsToWrappers = |
| new HashMap<BluetoothGattCharacteristic, BluetoothGattCharacteristicWrapper>(); |
| mDescriptorsToWrappers = |
| new HashMap<BluetoothGattDescriptor, BluetoothGattDescriptorWrapper>(); |
| } |
| |
| public BluetoothGattWrapper connectGatt(Context context, boolean autoConnect, |
| BluetoothGattCallbackWrapper callback, int transport) { |
| return new BluetoothGattWrapper( |
| mDevice.connectGatt(context, autoConnect, |
| new ForwardBluetoothGattCallbackToWrapper(callback, this), transport), |
| this); |
| } |
| |
| public String getAddress() { |
| return mDevice.getAddress(); |
| } |
| |
| public int getBluetoothClass_getDeviceClass() { |
| if (mDevice == null || mDevice.getBluetoothClass() == null) { |
| // BluetoothDevice.getBluetoothClass() returns null if adapter has been powered off. |
| // Return DEVICE_CLASS_UNSPECIFIED in these cases. |
| return DEVICE_CLASS_UNSPECIFIED; |
| } |
| return mDevice.getBluetoothClass().getDeviceClass(); |
| } |
| |
| public int getBondState() { |
| return mDevice.getBondState(); |
| } |
| |
| public String getName() { |
| return mDevice.getName(); |
| } |
| } |
| |
| /** |
| * Wraps android.bluetooth.BluetoothGatt. |
| */ |
| static class BluetoothGattWrapper { |
| private final BluetoothGatt mGatt; |
| private final BluetoothDeviceWrapper mDeviceWrapper; |
| |
| BluetoothGattWrapper(BluetoothGatt gatt, BluetoothDeviceWrapper deviceWrapper) { |
| mGatt = gatt; |
| mDeviceWrapper = deviceWrapper; |
| } |
| |
| public void disconnect() { |
| mGatt.disconnect(); |
| } |
| |
| public void close() { |
| mGatt.close(); |
| } |
| |
| public boolean requestMtu(int mtu) { |
| return mGatt.requestMtu(mtu); |
| } |
| |
| public void discoverServices() { |
| mGatt.discoverServices(); |
| } |
| |
| public List<BluetoothGattServiceWrapper> getServices() { |
| List<BluetoothGattService> services = mGatt.getServices(); |
| ArrayList<BluetoothGattServiceWrapper> servicesWrapped = |
| new ArrayList<BluetoothGattServiceWrapper>(services.size()); |
| for (BluetoothGattService service : services) { |
| servicesWrapped.add(new BluetoothGattServiceWrapper(service, mDeviceWrapper)); |
| } |
| return servicesWrapped; |
| } |
| |
| boolean readCharacteristic(BluetoothGattCharacteristicWrapper characteristic) { |
| return mGatt.readCharacteristic(characteristic.mCharacteristic); |
| } |
| |
| boolean setCharacteristicNotification( |
| BluetoothGattCharacteristicWrapper characteristic, boolean enable) { |
| return mGatt.setCharacteristicNotification(characteristic.mCharacteristic, enable); |
| } |
| |
| boolean writeCharacteristic(BluetoothGattCharacteristicWrapper characteristic) { |
| return mGatt.writeCharacteristic(characteristic.mCharacteristic); |
| } |
| |
| boolean readDescriptor(BluetoothGattDescriptorWrapper descriptor) { |
| return mGatt.readDescriptor(descriptor.mDescriptor); |
| } |
| |
| boolean writeDescriptor(BluetoothGattDescriptorWrapper descriptor) { |
| return mGatt.writeDescriptor(descriptor.mDescriptor); |
| } |
| } |
| |
| /** |
| * Implements android.bluetooth.BluetoothGattCallback and forwards calls through |
| * to a provided BluetoothGattCallbackWrapper instance. |
| * |
| * This class is required so that Fakes can use BluetoothGattCallbackWrapper |
| * without it extending from BluetoothGattCallback. Fakes must function even on |
| * Android versions where BluetoothGattCallback class is not defined. |
| */ |
| static class ForwardBluetoothGattCallbackToWrapper extends BluetoothGattCallback { |
| final BluetoothGattCallbackWrapper mWrapperCallback; |
| final BluetoothDeviceWrapper mDeviceWrapper; |
| |
| ForwardBluetoothGattCallbackToWrapper(BluetoothGattCallbackWrapper wrapperCallback, |
| BluetoothDeviceWrapper deviceWrapper) { |
| mWrapperCallback = wrapperCallback; |
| mDeviceWrapper = deviceWrapper; |
| } |
| |
| @Override |
| public void onCharacteristicChanged( |
| BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { |
| Log.i(TAG, "wrapper onCharacteristicChanged."); |
| mWrapperCallback.onCharacteristicChanged( |
| mDeviceWrapper.mCharacteristicsToWrappers.get(characteristic)); |
| } |
| |
| @Override |
| public void onCharacteristicRead( |
| BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { |
| mWrapperCallback.onCharacteristicRead( |
| mDeviceWrapper.mCharacteristicsToWrappers.get(characteristic), status); |
| } |
| |
| @Override |
| public void onCharacteristicWrite( |
| BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { |
| mWrapperCallback.onCharacteristicWrite( |
| mDeviceWrapper.mCharacteristicsToWrappers.get(characteristic), status); |
| } |
| |
| @Override |
| public void onDescriptorRead( |
| BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { |
| mWrapperCallback.onDescriptorRead( |
| mDeviceWrapper.mDescriptorsToWrappers.get(descriptor), status); |
| } |
| |
| @Override |
| public void onDescriptorWrite( |
| BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { |
| mWrapperCallback.onDescriptorWrite( |
| mDeviceWrapper.mDescriptorsToWrappers.get(descriptor), status); |
| } |
| |
| @Override |
| public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { |
| mWrapperCallback.onConnectionStateChange(status, newState); |
| } |
| |
| @Override |
| public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { |
| mWrapperCallback.onMtuChanged(mtu, status); |
| } |
| |
| @Override |
| public void onServicesDiscovered(BluetoothGatt gatt, int status) { |
| mWrapperCallback.onServicesDiscovered(status); |
| } |
| } |
| |
| /** |
| * Wrapper alternative to android.bluetooth.BluetoothGattCallback allowing clients and Fakes to |
| * work on older SDK versions without having a dependency on the class not defined there. |
| * |
| * BluetoothGatt gatt parameters are omitted from methods as each call would |
| * need to look up the correct BluetoothGattWrapper instance. |
| * Client code should cache the BluetoothGattWrapper provided if |
| * necessary from the initial BluetoothDeviceWrapper.connectGatt |
| * call. |
| */ |
| abstract static class BluetoothGattCallbackWrapper { |
| public abstract void onCharacteristicChanged( |
| BluetoothGattCharacteristicWrapper characteristic); |
| public abstract void onCharacteristicRead( |
| BluetoothGattCharacteristicWrapper characteristic, int status); |
| public abstract void onCharacteristicWrite( |
| BluetoothGattCharacteristicWrapper characteristic, int status); |
| public abstract void onDescriptorRead( |
| BluetoothGattDescriptorWrapper descriptor, int status); |
| public abstract void onDescriptorWrite( |
| BluetoothGattDescriptorWrapper descriptor, int status); |
| public abstract void onConnectionStateChange(int status, int newState); |
| public abstract void onMtuChanged(int mtu, int status); |
| public abstract void onServicesDiscovered(int status); |
| } |
| |
| /** |
| * Wraps android.bluetooth.BluetoothGattService. |
| */ |
| static class BluetoothGattServiceWrapper { |
| private final BluetoothGattService mService; |
| private final BluetoothDeviceWrapper mDeviceWrapper; |
| |
| public BluetoothGattServiceWrapper( |
| BluetoothGattService service, BluetoothDeviceWrapper deviceWrapper) { |
| mService = service; |
| mDeviceWrapper = deviceWrapper; |
| } |
| |
| public List<BluetoothGattCharacteristicWrapper> getCharacteristics() { |
| List<BluetoothGattCharacteristic> characteristics = mService.getCharacteristics(); |
| ArrayList<BluetoothGattCharacteristicWrapper> characteristicsWrapped = |
| new ArrayList<BluetoothGattCharacteristicWrapper>(characteristics.size()); |
| for (BluetoothGattCharacteristic characteristic : characteristics) { |
| BluetoothGattCharacteristicWrapper characteristicWrapper = |
| mDeviceWrapper.mCharacteristicsToWrappers.get(characteristic); |
| if (characteristicWrapper == null) { |
| characteristicWrapper = |
| new BluetoothGattCharacteristicWrapper(characteristic, mDeviceWrapper); |
| mDeviceWrapper.mCharacteristicsToWrappers.put( |
| characteristic, characteristicWrapper); |
| } |
| characteristicsWrapped.add(characteristicWrapper); |
| } |
| return characteristicsWrapped; |
| } |
| |
| public int getInstanceId() { |
| return mService.getInstanceId(); |
| } |
| |
| public UUID getUuid() { |
| return mService.getUuid(); |
| } |
| } |
| |
| /** |
| * Wraps android.bluetooth.BluetoothGattCharacteristic. |
| */ |
| static class BluetoothGattCharacteristicWrapper { |
| final BluetoothGattCharacteristic mCharacteristic; |
| final BluetoothDeviceWrapper mDeviceWrapper; |
| |
| public BluetoothGattCharacteristicWrapper( |
| BluetoothGattCharacteristic characteristic, BluetoothDeviceWrapper deviceWrapper) { |
| mCharacteristic = characteristic; |
| mDeviceWrapper = deviceWrapper; |
| } |
| |
| public List<BluetoothGattDescriptorWrapper> getDescriptors() { |
| List<BluetoothGattDescriptor> descriptors = mCharacteristic.getDescriptors(); |
| |
| ArrayList<BluetoothGattDescriptorWrapper> descriptorsWrapped = |
| new ArrayList<BluetoothGattDescriptorWrapper>(descriptors.size()); |
| |
| for (BluetoothGattDescriptor descriptor : descriptors) { |
| BluetoothGattDescriptorWrapper descriptorWrapper = |
| mDeviceWrapper.mDescriptorsToWrappers.get(descriptor); |
| if (descriptorWrapper == null) { |
| descriptorWrapper = |
| new BluetoothGattDescriptorWrapper(descriptor, mDeviceWrapper); |
| mDeviceWrapper.mDescriptorsToWrappers.put(descriptor, descriptorWrapper); |
| } |
| descriptorsWrapped.add(descriptorWrapper); |
| } |
| return descriptorsWrapped; |
| } |
| |
| public int getInstanceId() { |
| return mCharacteristic.getInstanceId(); |
| } |
| |
| public int getProperties() { |
| return mCharacteristic.getProperties(); |
| } |
| |
| public UUID getUuid() { |
| return mCharacteristic.getUuid(); |
| } |
| |
| public byte[] getValue() { |
| return mCharacteristic.getValue(); |
| } |
| |
| public boolean setValue(byte[] value) { |
| return mCharacteristic.setValue(value); |
| } |
| |
| public void setWriteType(int writeType) { |
| mCharacteristic.setWriteType(writeType); |
| } |
| } |
| |
| /** |
| * Wraps android.bluetooth.BluetoothGattDescriptor. |
| */ |
| static class BluetoothGattDescriptorWrapper { |
| private final BluetoothGattDescriptor mDescriptor; |
| final BluetoothDeviceWrapper mDeviceWrapper; |
| |
| public BluetoothGattDescriptorWrapper( |
| BluetoothGattDescriptor descriptor, BluetoothDeviceWrapper deviceWrapper) { |
| mDescriptor = descriptor; |
| mDeviceWrapper = deviceWrapper; |
| } |
| |
| public BluetoothGattCharacteristicWrapper getCharacteristic() { |
| return mDeviceWrapper.mCharacteristicsToWrappers.get(mDescriptor.getCharacteristic()); |
| } |
| |
| public UUID getUuid() { |
| return mDescriptor.getUuid(); |
| } |
| |
| public byte[] getValue() { |
| return mDescriptor.getValue(); |
| } |
| |
| public boolean setValue(byte[] value) { |
| return mDescriptor.setValue(value); |
| } |
| } |
| } |