/* * Copyright (C) 2014 The Android Open Source Project * * 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 android.bluetooth.le; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothUuid; import android.bluetooth.IBluetoothGatt; import android.bluetooth.IBluetoothGattCallback; import android.bluetooth.IBluetoothManager; import android.os.Handler; import android.os.Looper; import android.os.ParcelUuid; import android.os.RemoteException; import android.util.Log; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; /** * This class provides a way to perform Bluetooth LE advertise operations, such as starting and * stopping advertising. An advertiser can broadcast up to 31 bytes of advertisement data * represented by {@link AdvertiseData}. *
* To get an instance of {@link BluetoothLeAdvertiser}, call the * {@link BluetoothAdapter#getBluetoothLeAdvertiser()} method. *
* Note: Most of the methods here require {@link android.Manifest.permission#BLUETOOTH_ADMIN}
* permission.
*
* @see AdvertiseData
*/
public final class BluetoothLeAdvertiser {
private static final String TAG = "BluetoothLeAdvertiser";
private static final int MAX_ADVERTISING_DATA_BYTES = 31;
// Each fields need one byte for field length and another byte for field type.
private static final int OVERHEAD_BYTES_PER_FIELD = 2;
// Flags field will be set by system.
private static final int FLAGS_FIELD_BYTES = 3;
private static final int MANUFACTURER_SPECIFIC_DATA_LENGTH = 2;
private static final int SERVICE_DATA_UUID_LENGTH = 2;
private final IBluetoothManager mBluetoothManager;
private final Handler mHandler;
private BluetoothAdapter mBluetoothAdapter;
private final Map
* Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
*
* @param settings Settings for Bluetooth LE advertising.
* @param advertiseData Advertisement data to be broadcasted.
* @param callback Callback for advertising status.
*/
public void startAdvertising(AdvertiseSettings settings,
AdvertiseData advertiseData, final AdvertiseCallback callback) {
startAdvertising(settings, advertiseData, null, callback);
}
/**
* Start Bluetooth LE Advertising. The {@code advertiseData} will be broadcasted if the
* operation succeeds. The {@code scanResponse} is returned when a scanning device sends an
* active scan request. This method returns immediately, the operation status is delivered
* through {@code callback}.
*
* Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
*
* @param settings Settings for Bluetooth LE advertising.
* @param advertiseData Advertisement data to be advertised in advertisement packet.
* @param scanResponse Scan response associated with the advertisement data.
* @param callback Callback for advertising status.
*/
public void startAdvertising(AdvertiseSettings settings,
AdvertiseData advertiseData, AdvertiseData scanResponse,
final AdvertiseCallback callback) {
checkAdapterState();
if (callback == null) {
throw new IllegalArgumentException("callback cannot be null");
}
if (totalBytes(advertiseData) > MAX_ADVERTISING_DATA_BYTES ||
totalBytes(scanResponse) > MAX_ADVERTISING_DATA_BYTES) {
postCallbackFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE);
return;
}
if (mLeAdvertisers.containsKey(callback)) {
postCallbackFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED);
return;
}
IBluetoothGatt gatt;
try {
gatt = mBluetoothManager.getBluetoothGatt();
} catch (RemoteException e) {
Log.e(TAG, "Failed to get Bluetooth gatt - ", e);
postCallbackFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR);
return;
}
if (!mBluetoothAdapter.isMultipleAdvertisementSupported()) {
postCallbackFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED);
return;
}
AdvertiseCallbackWrapper wrapper = new AdvertiseCallbackWrapper(callback, advertiseData,
scanResponse, settings, gatt);
UUID uuid = UUID.randomUUID();
try {
gatt.registerClient(new ParcelUuid(uuid), wrapper);
if (wrapper.advertiseStarted()) {
mLeAdvertisers.put(callback, wrapper);
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to stop advertising", e);
}
}
/**
* Stop Bluetooth LE advertising. The {@code callback} must be the same one use in
* {@link BluetoothLeAdvertiser#startAdvertising}.
*
* Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
*
* @param callback {@link AdvertiseCallback} identifies the advertising instance to stop.
*/
public void stopAdvertising(final AdvertiseCallback callback) {
checkAdapterState();
if (callback == null) {
throw new IllegalArgumentException("callback cannot be null");
}
AdvertiseCallbackWrapper wrapper = mLeAdvertisers.get(callback);
if (wrapper == null)
return;
try {
IBluetoothGatt gatt = mBluetoothManager.getBluetoothGatt();
if (gatt != null)
gatt.stopMultiAdvertising(wrapper.mClientIf);
if (wrapper.advertiseStopped()) {
mLeAdvertisers.remove(callback);
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to stop advertising", e);
}
}
// Compute the size of the advertise data.
private int totalBytes(AdvertiseData data) {
if (data == null) {
return 0;
}
int size = FLAGS_FIELD_BYTES; // flags field is always set.
if (data.getServiceUuids() != null) {
int num16BitUuids = 0;
int num32BitUuids = 0;
int num128BitUuids = 0;
for (ParcelUuid uuid : data.getServiceUuids()) {
if (BluetoothUuid.is16BitUuid(uuid)) {
++num16BitUuids;
} else if (BluetoothUuid.is32BitUuid(uuid)) {
++num32BitUuids;
} else {
++num128BitUuids;
}
}
// 16 bit service uuids are grouped into one field when doing advertising.
if (num16BitUuids != 0) {
size += OVERHEAD_BYTES_PER_FIELD +
num16BitUuids * BluetoothUuid.UUID_BYTES_16_BIT;
}
// 32 bit service uuids are grouped into one field when doing advertising.
if (num32BitUuids != 0) {
size += OVERHEAD_BYTES_PER_FIELD +
num32BitUuids * BluetoothUuid.UUID_BYTES_32_BIT;
}
// 128 bit service uuids are grouped into one field when doing advertising.
if (num128BitUuids != 0) {
size += OVERHEAD_BYTES_PER_FIELD +
num128BitUuids * BluetoothUuid.UUID_BYTES_128_BIT;
}
}
if (data.getServiceDataUuid() != null) {
size += OVERHEAD_BYTES_PER_FIELD + SERVICE_DATA_UUID_LENGTH
+ byteLength(data.getServiceData());
}
if (data.getManufacturerId() > 0) {
size += OVERHEAD_BYTES_PER_FIELD + MANUFACTURER_SPECIFIC_DATA_LENGTH +
byteLength(data.getManufacturerSpecificData());
}
if (data.getIncludeTxPowerLevel()) {
size += OVERHEAD_BYTES_PER_FIELD + 1; // tx power level value is one byte.
}
if (data.getIncludeDeviceName() && mBluetoothAdapter.getName() != null) {
size += OVERHEAD_BYTES_PER_FIELD + mBluetoothAdapter.getName().length();
}
return size;
}
private int byteLength(byte[] array) {
return array == null ? 0 : array.length;
}
/**
* Bluetooth GATT interface callbacks for advertising.
*/
private static class AdvertiseCallbackWrapper extends IBluetoothGattCallback.Stub {
private static final int LE_CALLBACK_TIMEOUT_MILLIS = 2000;
private final AdvertiseCallback mAdvertiseCallback;
private final AdvertiseData mAdvertisement;
private final AdvertiseData mScanResponse;
private final AdvertiseSettings mSettings;
private final IBluetoothGatt mBluetoothGatt;
// mClientIf 0: not registered
// -1: scan stopped
// >0: registered and scan started
private int mClientIf;
private boolean isAdvertising = false;
public AdvertiseCallbackWrapper(AdvertiseCallback advertiseCallback,
AdvertiseData advertiseData, AdvertiseData scanResponse,
AdvertiseSettings settings,
IBluetoothGatt bluetoothGatt) {
mAdvertiseCallback = advertiseCallback;
mAdvertisement = advertiseData;
mScanResponse = scanResponse;
mSettings = settings;
mBluetoothGatt = bluetoothGatt;
mClientIf = 0;
}
public boolean advertiseStarted() {
boolean started = false;
synchronized (this) {
if (mClientIf == -1) {
return false;
}
try {
wait(LE_CALLBACK_TIMEOUT_MILLIS);
} catch (InterruptedException e) {
Log.e(TAG, "Callback reg wait interrupted: ", e);
}
started = (mClientIf > 0 && isAdvertising);
}
return started;
}
public boolean advertiseStopped() {
synchronized (this) {
try {
wait(LE_CALLBACK_TIMEOUT_MILLIS);
} catch (InterruptedException e) {
Log.e(TAG, "Callback reg wait interrupted: " + e);
}
return !isAdvertising;
}
}
/**
* Application interface registered - app is ready to go
*/
@Override
public void onClientRegistered(int status, int clientIf) {
Log.d(TAG, "onClientRegistered() - status=" + status + " clientIf=" + clientIf);
synchronized (this) {
if (status == BluetoothGatt.GATT_SUCCESS) {
mClientIf = clientIf;
try {
mBluetoothGatt.startMultiAdvertising(mClientIf, mAdvertisement,
mScanResponse, mSettings);
} catch (RemoteException e) {
Log.e(TAG, "fail to start le advertise: " + e);
mClientIf = -1;
notifyAll();
}
} else {
// registration failed
mClientIf = -1;
notifyAll();
}
}
}
@Override
public void onClientConnectionState(int status, int clientIf,
boolean connected, String address) {
// no op
}
@Override
public void onScanResult(String address, int rssi, byte[] advData) {
// no op
}
@Override
public void onGetService(String address, int srvcType,
int srvcInstId, ParcelUuid srvcUuid) {
// no op
}
@Override
public void onGetIncludedService(String address, int srvcType,
int srvcInstId, ParcelUuid srvcUuid,
int inclSrvcType, int inclSrvcInstId,
ParcelUuid inclSrvcUuid) {
// no op
}
@Override
public void onGetCharacteristic(String address, int srvcType,
int srvcInstId, ParcelUuid srvcUuid,
int charInstId, ParcelUuid charUuid,
int charProps) {
// no op
}
@Override
public void onGetDescriptor(String address, int srvcType,
int srvcInstId, ParcelUuid srvcUuid,
int charInstId, ParcelUuid charUuid,
int descInstId, ParcelUuid descUuid) {
// no op
}
@Override
public void onSearchComplete(String address, int status) {
// no op
}
@Override
public void onCharacteristicRead(String address, int status, int srvcType,
int srvcInstId, ParcelUuid srvcUuid,
int charInstId, ParcelUuid charUuid, byte[] value) {
// no op
}
@Override
public void onCharacteristicWrite(String address, int status, int srvcType,
int srvcInstId, ParcelUuid srvcUuid,
int charInstId, ParcelUuid charUuid) {
// no op
}
@Override
public void onNotify(String address, int srvcType,
int srvcInstId, ParcelUuid srvcUuid,
int charInstId, ParcelUuid charUuid,
byte[] value) {
// no op
}
@Override
public void onDescriptorRead(String address, int status, int srvcType,
int srvcInstId, ParcelUuid srvcUuid,
int charInstId, ParcelUuid charUuid,
int descInstId, ParcelUuid descrUuid, byte[] value) {
// no op
}
@Override
public void onDescriptorWrite(String address, int status, int srvcType,
int srvcInstId, ParcelUuid srvcUuid,
int charInstId, ParcelUuid charUuid,
int descInstId, ParcelUuid descrUuid) {
// no op
}
@Override
public void onExecuteWrite(String address, int status) {
// no op
}
@Override
public void onReadRemoteRssi(String address, int rssi, int status) {
// no op
}
@Override
public void onMultiAdvertiseCallback(int status) {
// TODO: This logic needs to be re-visited to account
// for whether the scan has actually been started
// or not. Toggling the isAdvertising does not seem
// correct.
synchronized (this) {
if (status == AdvertiseCallback.ADVERTISE_SUCCESS) {
isAdvertising = !isAdvertising;
if (!isAdvertising) {
try {
mBluetoothGatt.unregisterClient(mClientIf);
mClientIf = -1;
} catch (RemoteException e) {
Log.e(TAG, "remote exception when unregistering", e);
}
} else {
mAdvertiseCallback.onStartSuccess(null);
}
} else {
if (!isAdvertising)
mAdvertiseCallback.onStartFailure(status);
}
notifyAll();
}
}
/**
* Callback reporting LE ATT MTU.
*
* @hide
*/
@Override
public void onConfigureMTU(String address, int mtu, int status) {
// no op
}
@Override
public void onConnectionCongested(String address, boolean congested) {
// no op
}
@Override
public void onBatchScanResults(List