Java tutorial
// Copyright 2016 The Vanadium 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 io.v.android.impl.google.discovery.plugins.ble; import android.Manifest; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.bluetooth.le.AdvertiseCallback; import android.bluetooth.le.AdvertiseData; import android.bluetooth.le.AdvertiseSettings; import android.bluetooth.le.BluetoothLeAdvertiser; 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.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.os.ParcelUuid; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.util.Log; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import io.v.android.v23.V; import io.v.v23.context.VContext; /** * A BLE Driver for Android. * * This Driver also support discovery over Bluetooth classic by * - Each peripheral makes the device discoverable for a specified duration. * - A central device discovers near-by devices through Bluetooth classic, * tries to connect and check each device whether the device has any * services that the central is looking for. A central device will fetch * services through Gatt over BR/EDR. */ public class Driver implements BluetoothScanner.Handler, GattReader.Handler { static final String TAG = "BleDriver"; /** * An interface for passing scanned advertisements. */ public interface ScanHandler { /** * Called with each discovery update. */ void onDiscovered(String uuid, Map<String, byte[]> characteristics, int rssi); } private final Context mContext; private final BluetoothAdapter mBluetoothAdapter; private BluetoothAdvertiser mClassicAdvertiser; private static int sClassicDiscoverableDurationInSec; private BluetoothLeAdvertiser mLeAdvertiser; private Map<UUID, AdvertiseCallback> mLeAdvertiseCallbacks; private GattServer mGattServer; private final Map<String, BluetoothGattService> mServices; private BluetoothScanner mClassicScanner; private static boolean sClassicScanEnabled; private BluetoothLeScanner mLeScanner; private ScanCallback mLeScanCallback; private GattReader mGattReader; private Set<UUID> mScanUuids; private ParcelUuid mScanBaseUuid, mScanMaskUuid; private ScanHandler mScanHandler; private Map<BluetoothDevice, Integer> mScanSeens; private boolean mEnabled; private int mOnServiceReadCallbacks; private final class BluetoothAdapterStatusReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (!intent.getAction().equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { return; } switch (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) { case BluetoothAdapter.STATE_ON: resume(); break; case BluetoothAdapter.STATE_OFF: pause(); break; } } } /** * Create a new BLE driver for Android. * * @param vContext Vanadium context. */ public Driver(VContext vContext) { mContext = V.getAndroidContext(vContext); if (mContext == null) { throw new IllegalStateException("AndroidContext not available"); } mServices = new HashMap<>(); BluetoothManager manager = ((BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE)); mBluetoothAdapter = manager.getAdapter(); if (mBluetoothAdapter == null) { Log.w(TAG, "BluetoothAdapter not available"); return; } if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { Log.w(TAG, "ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION not granted, " + "Bluetooth discovery will not be happening"); return; } mContext.registerReceiver(new BluetoothAdapterStatusReceiver(), new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); if (!mBluetoothAdapter.isEnabled()) { // Prompt user to turn on Bluetooth. Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); enableBtIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(enableBtIntent); return; } resume(); } private synchronized void resume() { if (mEnabled) { return; } if (!mBluetoothAdapter.isEnabled()) { return; } mEnabled = true; resumeAdvertising(); resumeScanning(); Log.i(TAG, "started"); } private synchronized void pause() { if (!mEnabled) { return; } mEnabled = false; pauseAdvertising(); pauseScanning(); Log.i(TAG, "stopped"); } public void addService(final String uuid, Map<String, byte[]> characteristics) { BluetoothGattService service = new BluetoothGattService(UUID.fromString(uuid), BluetoothGattService.SERVICE_TYPE_PRIMARY); for (Map.Entry<String, byte[]> entry : characteristics.entrySet()) { BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic( UUID.fromString(entry.getKey()), BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ); characteristic.setValue(entry.getValue()); service.addCharacteristic(characteristic); } synchronized (this) { if (mServices.put(uuid, service) != null) { throw new IllegalStateException("already being advertised: " + uuid); } if (mEnabled) { startAdvertising(service); } } } public synchronized void removeService(String uuid) { BluetoothGattService service = mServices.remove(uuid); if (service == null) { return; } if (mEnabled) { stopAdvertising(service); } } private synchronized void startAdvertising(BluetoothGattService service) { mGattServer.addService(service); synchronized (Driver.class) { mClassicAdvertiser.addService(service.getUuid(), sClassicDiscoverableDurationInSec); sClassicDiscoverableDurationInSec = 0; } if (mLeAdvertiser != null) { final UUID uuid = service.getUuid(); AdvertiseSettings settings = new AdvertiseSettings.Builder() .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED).setConnectable(true).build(); AdvertiseData data = new AdvertiseData.Builder().addServiceUuid(new ParcelUuid(uuid)) .setIncludeTxPowerLevel(true).build(); AdvertiseCallback callback = new AdvertiseCallback() { @Override public void onStartFailure(int errorCode) { Log.e(TAG, "startAdvertising failed: " + uuid + ", errorCode:" + errorCode); } }; // TODO(jhahn): The maximum number of simultaneous advertisements is limited by the chipset. // Rotate active advertisements periodically if the total number of advertisement exceeds // the limit. mLeAdvertiser.startAdvertising(settings, data, callback); mLeAdvertiseCallbacks.put(uuid, callback); } } private synchronized void stopAdvertising(BluetoothGattService service) { mGattServer.removeService(service); mClassicAdvertiser.removeService(service.getUuid()); if (mLeAdvertiser != null) { AdvertiseCallback callback = mLeAdvertiseCallbacks.remove(service.getUuid()); mLeAdvertiser.stopAdvertising(callback); } } private synchronized void resumeAdvertising() { mGattServer = new GattServer(mContext); mClassicAdvertiser = new BluetoothAdvertiser(mContext, mBluetoothAdapter); if (mBluetoothAdapter.isMultipleAdvertisementSupported()) { mLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser(); mLeAdvertiseCallbacks = new HashMap<>(); } for (BluetoothGattService service : mServices.values()) { startAdvertising(service); } } private synchronized void pauseAdvertising() { mGattServer.close(); mGattServer = null; mClassicAdvertiser.close(); mClassicAdvertiser = null; // mLeAdvertiser is invalidated when BluetoothAdapter is turned off. // We don't need to stop any active advertising. mLeAdvertiser = null; mLeAdvertiseCallbacks = null; } public synchronized void startScan(String[] uuids, String baseUuid, String maskUuid, ScanHandler handler) { if (mScanHandler != null) { throw new IllegalStateException("scan already started"); } ImmutableSet.Builder<UUID> builder = ImmutableSet.builder(); if (uuids != null) { for (String uuid : uuids) { builder.add(UUID.fromString(uuid)); } } mScanUuids = builder.build(); mScanBaseUuid = ParcelUuid.fromString(baseUuid); mScanMaskUuid = ParcelUuid.fromString(maskUuid); mScanHandler = handler; if (mEnabled) { startScanning(); } } public synchronized void stopScan() { if (mScanHandler == null) { return; } if (mEnabled) { stopScanning(); } mScanUuids = null; mScanBaseUuid = null; mScanMaskUuid = null; mScanHandler = null; } private synchronized void startScanning() { mScanSeens = new HashMap<>(); mGattReader = new GattReader(mContext, mScanUuids, mScanBaseUuid.getUuid(), mScanMaskUuid.getUuid(), this); synchronized (Driver.class) { if (sClassicScanEnabled) { // Note that BluetoothLeScan will be started when BluetoothScan finishes. mClassicScanner.startScan(mScanUuids); sClassicScanEnabled = false; } else { startBluetoothLeScanner(); } } } private synchronized void startBluetoothLeScanner() { if (mLeScanner == null) { return; } ImmutableList.Builder<ScanFilter> builder = new ImmutableList.Builder(); for (UUID uuid : mScanUuids) { builder.add(new ScanFilter.Builder().setServiceUuid(new ParcelUuid(uuid)).build()); } List<ScanFilter> filters = builder.build(); ScanSettings settings = new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_BALANCED).build(); // ScanFilter doesn't work with startScan() if there are too many - more than 63bits - ignore // bits. So we call startScan() without a scan filter for base/mask uuids and match scan results // against it. final ScanFilter matcher = new ScanFilter.Builder().setServiceUuid(mScanBaseUuid, mScanMaskUuid).build(); mLeScanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) { // This callback will never be called with this callback type, since the // scan setting is for CALLBACK_TYPE_ALL_MATCHES. But just for safety. return; } if (!matcher.matches(result)) { return; } BluetoothDevice device = result.getDevice(); synchronized (Driver.this) { if (mScanSeens != null && mScanSeens.put(device, result.getRssi()) == null) { mGattReader.readDevice(device); } } } @Override public void onScanFailed(int errorCode) { Log.e(TAG, "startScan failed: " + errorCode); } }; mLeScanner.startScan(filters, settings, mLeScanCallback); } private synchronized void stopScanning() { mClassicScanner.stopScan(); if (mLeScanCallback != null) { mLeScanner.stopScan(mLeScanCallback); mLeScanCallback = null; } mGattReader.close(true); mGattReader = null; mScanSeens = null; } private synchronized void resumeScanning() { mClassicScanner = new BluetoothScanner(mContext, mBluetoothAdapter, this); mLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); if (mScanHandler != null) { startScanning(); } } private synchronized void pauseScanning() { mClassicScanner.close(); mClassicScanner = null; if (mScanHandler != null) { // mLeScanner is invalidated when BluetoothAdapter is turned off. // We don't need to stop any active scan or Gatt read. mGattReader.close(false); mGattReader = null; mLeScanner = null; mLeScanCallback = null; mScanSeens = null; } } public synchronized void onBluetoothDiscoveryFinished(Map<BluetoothDevice, Integer> found) { if (mScanSeens == null) { return; } // Start to read services through Gatt. // // TODO(jhahn): Do we need to retry when Gatt read fails? for (Map.Entry<BluetoothDevice, Integer> e : found.entrySet()) { mScanSeens.put(e.getKey(), e.getValue()); mGattReader.readDevice(e.getKey()); } // Now start BluetoothLeScan. startBluetoothLeScanner(); } public void onGattRead(BluetoothDevice device, BluetoothGattService service) { Map<String, byte[]> characteristics; ImmutableMap.Builder<String, byte[]> builder = new ImmutableMap.Builder(); for (BluetoothGattCharacteristic c : service.getCharacteristics()) { builder.put(c.getUuid().toString(), c.getValue()); } characteristics = builder.build(); synchronized (this) { if (mScanSeens == null) { return; } Integer rssi = mScanSeens.get(device); if (rssi == null) { return; } mScanHandler.onDiscovered(service.getUuid().toString(), characteristics, rssi); mOnServiceReadCallbacks++; } } public synchronized void onGattReadFailed(BluetoothDevice device) { if (mScanSeens == null) { return; } // Remove the seen record to retry to read the device. mScanSeens.remove(device); } /** * Set the Duration of Bluetooth discoverability in seconds. This will be applied for * the next addService() only one time. * * TODO(jhahn): Find a better API to set Bluetooth discovery options. */ public static synchronized void setBluetoothDiscoverableDuration(int durationInSec) { sClassicDiscoverableDurationInSec = durationInSec; } /** * Enable Bluetooth scan. This will be applied for the next startScan() only one time. * * TODO(jhahn): Find a better API to set Bluetooth discovery options. */ public static synchronized void setBluetoothScanEnabled(boolean enabled) { sClassicScanEnabled = enabled; } public synchronized String debugString() { if (mBluetoothAdapter == null) { return "Not available"; } StringBuilder b = new StringBuilder().append("BluetoothAdapter: "); switch (mBluetoothAdapter.getState()) { case BluetoothAdapter.STATE_ON: b.append("ON"); break; case BluetoothAdapter.STATE_TURNING_ON: b.append("Turning on"); break; case BluetoothAdapter.STATE_OFF: b.append("OFF"); break; case BluetoothAdapter.STATE_TURNING_OFF: b.append("Turning off"); break; default: b.append("Unknown state"); break; } b.append("\n"); b.append("ENABLED: ").append(mEnabled).append("\n"); if (mServices.size() > 0) { b.append("ADVERTISING ").append(mServices.size()).append(" services\n"); } if (mLeScanCallback != null) { b.append("SCANNING\n"); } b.append("OnServiceReadCallbacks: ").append(mOnServiceReadCallbacks).append("\n"); return b.toString(); } }