blob: ed6d53f5182d4ed219a5cf6fe152b0690cf0de54 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.le.ScanSettings;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.ParcelUuid;
import org.chromium.base.Log;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import java.util.List;
/**
* Exposes android.bluetooth.BluetoothAdapter as necessary for C++
* device::BluetoothAdapterAndroid, which implements the cross platform
* device::BluetoothAdapter.
*
* Lifetime is controlled by device::BluetoothAdapterAndroid.
*/
@JNINamespace("device")
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
final class ChromeBluetoothAdapter extends BroadcastReceiver {
private static final String TAG = "Bluetooth";
private long mNativeBluetoothAdapterAndroid;
// mAdapter is final to ensure registerReceiver is followed by unregisterReceiver.
private final Wrappers.BluetoothAdapterWrapper mAdapter;
private ScanCallback mScanCallback;
// ---------------------------------------------------------------------------------------------
// Construction and handler for C++ object destruction.
/**
* Constructs a ChromeBluetoothAdapter.
* @param nativeBluetoothAdapterAndroid Is the associated C++
* BluetoothAdapterAndroid pointer value.
* @param adapterWrapper Wraps the default android.bluetooth.BluetoothAdapter,
* but may be either null if an adapter is not available
* or a fake for testing.
*/
public ChromeBluetoothAdapter(
long nativeBluetoothAdapterAndroid, Wrappers.BluetoothAdapterWrapper adapterWrapper) {
mNativeBluetoothAdapterAndroid = nativeBluetoothAdapterAndroid;
mAdapter = adapterWrapper;
registerBroadcastReceiver();
if (adapterWrapper == null) {
Log.i(TAG, "ChromeBluetoothAdapter created with no adapterWrapper.");
} else {
Log.i(TAG, "ChromeBluetoothAdapter created with provided adapterWrapper.");
}
}
/**
* Handles C++ object being destroyed.
*/
@CalledByNative
private void onBluetoothAdapterAndroidDestruction() {
stopScan();
mNativeBluetoothAdapterAndroid = 0;
unregisterBroadcastReceiver();
}
// ---------------------------------------------------------------------------------------------
// BluetoothAdapterAndroid methods implemented in java:
// Implements BluetoothAdapterAndroid::Create.
// 'Object' type must be used for |adapterWrapper| because inner class
// Wrappers.BluetoothAdapterWrapper reference is not handled by jni_generator.py JavaToJni.
// http://crbug.com/505554
@CalledByNative
private static ChromeBluetoothAdapter create(
long nativeBluetoothAdapterAndroid, Object adapterWrapper) {
return new ChromeBluetoothAdapter(
nativeBluetoothAdapterAndroid, (Wrappers.BluetoothAdapterWrapper) adapterWrapper);
}
// Implements BluetoothAdapterAndroid::GetAddress.
@CalledByNative
private String getAddress() {
if (isPresent()) {
return mAdapter.getAddress();
} else {
return "";
}
}
// Implements BluetoothAdapterAndroid::GetName.
@CalledByNative
private String getName() {
if (isPresent()) {
return mAdapter.getName();
} else {
return "";
}
}
// Implements BluetoothAdapterAndroid::IsPresent.
@CalledByNative
private boolean isPresent() {
return mAdapter != null;
}
// Implements BluetoothAdapterAndroid::IsPowered.
@CalledByNative
private boolean isPowered() {
return isPresent() && mAdapter.isEnabled();
}
// Implements BluetoothAdapterAndroid::SetPowered.
@CalledByNative
private boolean setPowered(boolean powered) {
if (powered) {
return isPresent() && mAdapter.enable();
} else {
return isPresent() && mAdapter.disable();
}
}
// Implements BluetoothAdapterAndroid::IsDiscoverable.
@CalledByNative
private boolean isDiscoverable() {
return isPresent()
&& mAdapter.getScanMode() == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
}
// Implements BluetoothAdapterAndroid::IsDiscovering.
@CalledByNative
private boolean isDiscovering() {
return isPresent() && (mAdapter.isDiscovering() || mScanCallback != null);
}
/**
* Starts a Low Energy scan.
* @return True on success.
*/
@CalledByNative
private boolean startScan() {
Wrappers.BluetoothLeScannerWrapper scanner = mAdapter.getBluetoothLeScanner();
if (scanner == null) {
return false;
}
if (!canScan()) {
return false;
}
// scanMode note: SCAN_FAILED_FEATURE_UNSUPPORTED is caused (at least on some devices) if
// setReportDelay() is used or if SCAN_MODE_LOW_LATENCY isn't used.
int scanMode = ScanSettings.SCAN_MODE_LOW_LATENCY;
assert mScanCallback == null;
mScanCallback = new ScanCallback();
try {
scanner.startScan(null /* filters */, scanMode, mScanCallback);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Cannot start scan: " + e);
mScanCallback = null;
return false;
} catch (IllegalStateException e) {
Log.e(TAG, "Adapter is off. Cannot start scan: " + e);
mScanCallback = null;
return false;
}
return true;
}
/**
* Stops the Low Energy scan.
* @return True if a scan was in progress.
*/
@CalledByNative
private boolean stopScan() {
if (mScanCallback == null) {
return false;
}
try {
Wrappers.BluetoothLeScannerWrapper scanner = mAdapter.getBluetoothLeScanner();
if (scanner != null) {
scanner.stopScan(mScanCallback);
}
} catch (IllegalArgumentException e) {
Log.e(TAG, "Cannot stop scan: " + e);
} catch (IllegalStateException e) {
Log.e(TAG, "Adapter is off. Cannot stop scan: " + e);
}
mScanCallback = null;
return true;
}
// ---------------------------------------------------------------------------------------------
// Implementation details:
/**
* @return true if Chromium has permission to scan for Bluetooth devices.
*/
private boolean canScan() {
Wrappers.ContextWrapper context = mAdapter.getContext();
return context.checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
|| context.checkPermission(Manifest.permission.ACCESS_FINE_LOCATION);
}
private void registerBroadcastReceiver() {
if (mAdapter != null) {
mAdapter.getContext().registerReceiver(
this, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
}
}
private void unregisterBroadcastReceiver() {
if (mAdapter != null) {
mAdapter.getContext().unregisterReceiver(this);
}
}
/**
* Implements callbacks used during a Low Energy scan by notifying upon
* devices discovered or detecting a scan failure.
*/
private class ScanCallback extends Wrappers.ScanCallbackWrapper {
@Override
public void onBatchScanResult(List<Wrappers.ScanResultWrapper> results) {
Log.v(TAG, "onBatchScanResults");
}
@Override
public void onScanResult(int callbackType, Wrappers.ScanResultWrapper result) {
Log.v(TAG, "onScanResult %d %s %s", callbackType, result.getDevice().getAddress(),
result.getDevice().getName());
List<ParcelUuid> uuids = result.getScanRecord_getServiceUuids();
nativeCreateOrUpdateDeviceOnScan(mNativeBluetoothAdapterAndroid,
result.getDevice().getAddress(), result.getDevice(), uuids);
}
@Override
public void onScanFailed(int errorCode) {
Log.w(TAG, "onScanFailed: %d", errorCode);
nativeOnScanFailed(mNativeBluetoothAdapterAndroid);
}
}
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (isPresent() && BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
Log.w(TAG, "onReceive: BluetoothAdapter.ACTION_STATE_CHANGED: %s",
getBluetoothStateString(state));
switch (state) {
case BluetoothAdapter.STATE_ON:
nativeOnAdapterStateChanged(mNativeBluetoothAdapterAndroid, true);
break;
case BluetoothAdapter.STATE_OFF:
nativeOnAdapterStateChanged(mNativeBluetoothAdapterAndroid, false);
break;
default:
// do nothing
}
}
}
private String getBluetoothStateString(int state) {
switch (state) {
case BluetoothAdapter.STATE_OFF:
return "STATE_OFF";
case BluetoothAdapter.STATE_ON:
return "STATE_ON";
case BluetoothAdapter.STATE_TURNING_OFF:
return "STATE_TURNING_OFF";
case BluetoothAdapter.STATE_TURNING_ON:
return "STATE_TURNING_ON";
default:
assert false;
return "illegal state: " + state;
}
}
// ---------------------------------------------------------------------------------------------
// BluetoothAdapterAndroid C++ methods declared for access from java:
// Binds to BluetoothAdapterAndroid::OnScanFailed.
private native void nativeOnScanFailed(long nativeBluetoothAdapterAndroid);
// Binds to BluetoothAdapterAndroid::CreateOrUpdateDeviceOnScan.
// 'Object' type must be used for |bluetoothDeviceWrapper| because inner class
// Wrappers.BluetoothDeviceWrapper reference is not handled by jni_generator.py JavaToJni.
// http://crbug.com/505554
private native void nativeCreateOrUpdateDeviceOnScan(long nativeBluetoothAdapterAndroid,
String address, Object bluetoothDeviceWrapper, List<ParcelUuid> advertisedUuids);
// Binds to BluetoothAdapterAndroid::nativeOnAdapterStateChanged
private native void nativeOnAdapterStateChanged(
long nativeBluetoothAdapterAndroid, boolean powered);
}