/*
* 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;
import android.bluetooth.BluetoothLeAdvertiseScanData.AdvertisementData;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcel;
import android.os.ParcelUuid;
import android.os.Parcelable;
import android.os.RemoteException;
import android.util.Log;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* This class provides a way to perform Bluetooth LE advertise operations, such as start and stop
* advertising. An advertiser can broadcast up to 31 bytes of advertisement data represented by
* {@link BluetoothLeAdvertiseScanData.AdvertisementData}.
*
* 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 BluetoothLeAdvertiseScanData.AdvertisementData
*/
public class BluetoothLeAdvertiser {
private static final String TAG = "BluetoothLeAdvertiser";
/**
* The {@link Settings} provide a way to adjust advertising preferences for each individual
* advertisement. Use {@link Settings.Builder} to create a {@link Settings} instance.
*/
public static final class Settings implements Parcelable {
/**
* Perform Bluetooth LE advertising in low power mode. This is the default and preferred
* advertising mode as it consumes the least power.
*/
public static final int ADVERTISE_MODE_LOW_POWER = 0;
/**
* Perform Bluetooth LE advertising in balanced power mode. This is balanced between
* advertising frequency and power consumption.
*/
public static final int ADVERTISE_MODE_BALANCED = 1;
/**
* Perform Bluetooth LE advertising in low latency, high power mode. This has the highest
* power consumption and should not be used for background continuous advertising.
*/
public static final int ADVERTISE_MODE_LOW_LATENCY = 2;
/**
* Advertise using the lowest transmission(tx) power level. An app can use low transmission
* power to restrict the visibility range of its advertising packet.
*/
public static final int ADVERTISE_TX_POWER_ULTRA_LOW = 0;
/**
* Advertise using low tx power level.
*/
public static final int ADVERTISE_TX_POWER_LOW = 1;
/**
* Advertise using medium tx power level.
*/
public static final int ADVERTISE_TX_POWER_MEDIUM = 2;
/**
* Advertise using high tx power level. This is corresponding to largest visibility range of
* the advertising packet.
*/
public static final int ADVERTISE_TX_POWER_HIGH = 3;
/**
* Non-connectable undirected advertising event, as defined in Bluetooth Specification V4.0
* vol6, part B, section 4.4.2 - Advertising state.
*/
public static final int ADVERTISE_TYPE_NON_CONNECTABLE = 0;
/**
* Scannable undirected advertise type, as defined in same spec mentioned above. This event
* type allows a scanner to send a scan request asking additional information about the
* advertiser.
*/
public static final int ADVERTISE_TYPE_SCANNABLE = 1;
/**
* Connectable undirected advertising type, as defined in same spec mentioned above. This
* event type allows a scanner to send scan request asking additional information about the
* advertiser. It also allows an initiator to send a connect request for connection.
*/
public static final int ADVERTISE_TYPE_CONNECTABLE = 2;
private final int mAdvertiseMode;
private final int mAdvertiseTxPowerLevel;
private final int mAdvertiseEventType;
private Settings(int advertiseMode, int advertiseTxPowerLevel,
int advertiseEventType) {
mAdvertiseMode = advertiseMode;
mAdvertiseTxPowerLevel = advertiseTxPowerLevel;
mAdvertiseEventType = advertiseEventType;
}
private Settings(Parcel in) {
mAdvertiseMode = in.readInt();
mAdvertiseTxPowerLevel = in.readInt();
mAdvertiseEventType = in.readInt();
}
/**
* Creates a {@link Builder} to construct a {@link Settings} object.
*/
public static Builder newBuilder() {
return new Builder();
}
/**
* Returns the advertise mode.
*/
public int getMode() {
return mAdvertiseMode;
}
/**
* Returns the tx power level for advertising.
*/
public int getTxPowerLevel() {
return mAdvertiseTxPowerLevel;
}
/**
* Returns the advertise event type.
*/
public int getType() {
return mAdvertiseEventType;
}
@Override
public String toString() {
return "Settings [mAdvertiseMode=" + mAdvertiseMode + ", mAdvertiseTxPowerLevel="
+ mAdvertiseTxPowerLevel + ", mAdvertiseEventType=" + mAdvertiseEventType + "]";
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mAdvertiseMode);
dest.writeInt(mAdvertiseTxPowerLevel);
dest.writeInt(mAdvertiseEventType);
}
public static final Parcelable.Creator CREATOR =
new Creator() {
@Override
public Settings[] newArray(int size) {
return new Settings[size];
}
@Override
public Settings createFromParcel(Parcel in) {
return new Settings(in);
}
};
/**
* Builder class for {@link BluetoothLeAdvertiser.Settings}. Caller should use
* {@link Settings#newBuilder()} to get an instance of the builder.
*/
public static final class Builder {
private int mMode = ADVERTISE_MODE_LOW_POWER;
private int mTxPowerLevel = ADVERTISE_TX_POWER_MEDIUM;
private int mType = ADVERTISE_TYPE_NON_CONNECTABLE;
// Private constructor, use Settings.newBuilder() get an instance of BUILDER.
private Builder() {
}
/**
* Set advertise mode to control the advertising power and latency.
*
* @param advertiseMode Bluetooth LE Advertising mode, can only be one of
* {@link Settings#ADVERTISE_MODE_LOW_POWER},
* {@link Settings#ADVERTISE_MODE_BALANCED}, or
* {@link Settings#ADVERTISE_MODE_LOW_LATENCY}.
* @throws IllegalArgumentException If the advertiseMode is invalid.
*/
public Builder advertiseMode(int advertiseMode) {
if (advertiseMode < ADVERTISE_MODE_LOW_POWER
|| advertiseMode > ADVERTISE_MODE_LOW_LATENCY) {
throw new IllegalArgumentException("unknown mode " + advertiseMode);
}
mMode = advertiseMode;
return this;
}
/**
* Set advertise tx power level to control the transmission power level for the
* advertising.
*
* @param txPowerLevel Transmission power of Bluetooth LE Advertising, can only be one
* of {@link Settings#ADVERTISE_TX_POWER_ULTRA_LOW},
* {@link Settings#ADVERTISE_TX_POWER_LOW},
* {@link Settings#ADVERTISE_TX_POWER_MEDIUM} or
* {@link Settings#ADVERTISE_TX_POWER_HIGH}.
* @throws IllegalArgumentException If the {@code txPowerLevel} is invalid.
*/
public Builder txPowerLevel(int txPowerLevel) {
if (txPowerLevel < ADVERTISE_TX_POWER_ULTRA_LOW
|| txPowerLevel > ADVERTISE_TX_POWER_HIGH) {
throw new IllegalArgumentException("unknown tx power level " + txPowerLevel);
}
mTxPowerLevel = txPowerLevel;
return this;
}
/**
* Set advertise type to control the event type of advertising.
*
* @param type Bluetooth LE Advertising type, can be either
* {@link Settings#ADVERTISE_TYPE_NON_CONNECTABLE},
* {@link Settings#ADVERTISE_TYPE_SCANNABLE} or
* {@link Settings#ADVERTISE_TYPE_CONNECTABLE}.
* @throws IllegalArgumentException If the {@code type} is invalid.
*/
public Builder type(int type) {
if (type < ADVERTISE_TYPE_NON_CONNECTABLE
|| type > ADVERTISE_TYPE_CONNECTABLE) {
throw new IllegalArgumentException("unknown advertise type " + type);
}
mType = type;
return this;
}
/**
* Build the {@link Settings} object.
*/
public Settings build() {
return new Settings(mMode, mTxPowerLevel, mType);
}
}
}
/**
* Callback of Bluetooth LE advertising, which is used to deliver operation status for start and
* stop advertising.
*/
public interface AdvertiseCallback {
/**
* The operation is success.
*
* @hide
*/
public static final int SUCCESS = 0;
/**
* Fails to start advertising as the advertisement data contains services that are not added
* to the local bluetooth Gatt server.
*/
public static final int ADVERTISING_SERVICE_UNKNOWN = 1;
/**
* Fails to start advertising as system runs out of quota for advertisers.
*/
public static final int TOO_MANY_ADVERTISERS = 2;
/**
* Fails to start advertising as the advertising is already started.
*/
public static final int ADVERTISING_ALREADY_STARTED = 3;
/**
* Fails to stop advertising as the advertising is not started.
*/
public static final int ADVERISING_NOT_STARTED = 4;
/**
* Operation fails due to bluetooth controller failure.
*/
public static final int CONTROLLER_FAILURE = 5;
/**
* Callback when advertising operation succeeds.
*
* @param settingsInEffect The actual settings used for advertising, which may be different
* from what the app asks.
*/
public void onSuccess(Settings settingsInEffect);
/**
* Callback when advertising operation fails.
*
* @param errorCode Error code for failures.
*/
public void onFailure(int errorCode);
}
private final IBluetoothGatt mBluetoothGatt;
private final Handler mHandler;
private final Map
mLeAdvertisers = new HashMap();
// Package private constructor, use BluetoothAdapter.getLeAdvertiser() instead.
BluetoothLeAdvertiser(IBluetoothGatt bluetoothGatt) {
mBluetoothGatt = bluetoothGatt;
mHandler = new Handler(Looper.getMainLooper());
}
/**
* 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 AdvertisementData mAdvertisement;
private final AdvertisementData mScanResponse;
private final Settings mSettings;
private final IBluetoothGatt mBluetoothGatt;
// mLeHandle 0: not registered
// -1: scan stopped
// >0: registered and scan started
private int mLeHandle;
private boolean isAdvertising = false;
public AdvertiseCallbackWrapper(AdvertiseCallback advertiseCallback,
AdvertisementData advertiseData, AdvertisementData scanResponse, Settings settings,
IBluetoothGatt bluetoothGatt) {
mAdvertiseCallback = advertiseCallback;
mAdvertisement = advertiseData;
mScanResponse = scanResponse;
mSettings = settings;
mBluetoothGatt = bluetoothGatt;
mLeHandle = 0;
}
public boolean advertiseStarted() {
boolean started = false;
synchronized (this) {
if (mLeHandle == -1) {
return false;
}
try {
wait(LE_CALLBACK_TIMEOUT_MILLIS);
} catch (InterruptedException e) {
Log.e(TAG, "Callback reg wait interrupted: " + e);
}
started = (mLeHandle > 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) {
mLeHandle = clientIf;
try {
mBluetoothGatt.startMultiAdvertising(mLeHandle, mAdvertisement,
mScanResponse, mSettings);
} catch (RemoteException e) {
Log.e(TAG, "fail to start le advertise: " + e);
mLeHandle = -1;
notifyAll();
} catch (Exception e) {
Log.e(TAG, "fail to start advertise: " + e.getStackTrace());
}
} else {
// registration failed
mLeHandle = -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 onAdvertiseStateChange(int advertiseState, int status) {
// no op
}
@Override
public void onMultiAdvertiseCallback(int status) {
synchronized (this) {
if (status == 0) {
isAdvertising = !isAdvertising;
if (!isAdvertising) {
try {
mBluetoothGatt.unregisterClient(mLeHandle);
mLeHandle = -1;
} catch (RemoteException e) {
Log.e(TAG, "remote exception when unregistering", e);
}
}
mAdvertiseCallback.onSuccess(null);
} else {
mAdvertiseCallback.onFailure(status);
}
notifyAll();
}
}
/**
* Callback reporting LE ATT MTU.
*
* @hide
*/
public void onConfigureMTU(String address, int mtu, int status) {
// no op
}
}
/**
* Start Bluetooth LE Advertising.
*
* @param settings {@link Settings} for Bluetooth LE advertising.
* @param advertiseData {@link AdvertisementData} to be advertised.
* @param callback {@link AdvertiseCallback} for advertising status.
*/
public void startAdvertising(Settings settings,
AdvertisementData advertiseData, final AdvertiseCallback callback) {
startAdvertising(settings, advertiseData, null, callback);
}
/**
* Start Bluetooth LE Advertising.
* @param settings {@link Settings} for Bluetooth LE advertising.
* @param advertiseData {@link AdvertisementData} to be advertised in advertisement packet.
* @param scanResponse {@link AdvertisementData} for scan response.
* @param callback {@link AdvertiseCallback} for advertising status.
*/
public void startAdvertising(Settings settings,
AdvertisementData advertiseData, AdvertisementData scanResponse,
final AdvertiseCallback callback) {
if (callback == null) {
throw new IllegalArgumentException("callback cannot be null");
}
if (mLeAdvertisers.containsKey(settings)) {
postCallbackFailure(callback, AdvertiseCallback.ADVERTISING_ALREADY_STARTED);
return;
}
AdvertiseCallbackWrapper wrapper = new AdvertiseCallbackWrapper(callback, advertiseData,
scanResponse, settings, mBluetoothGatt);
UUID uuid = UUID.randomUUID();
try {
mBluetoothGatt.registerClient(new ParcelUuid(uuid), wrapper);
if (wrapper.advertiseStarted()) {
mLeAdvertisers.put(settings, wrapper);
}
} catch (RemoteException e) {
Log.e(TAG, "failed to stop advertising", e);
}
}
/**
* Stop Bluetooth LE advertising. Returns immediately, the operation status will be delivered
* through the {@code callback}.
*
* Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
*
* @param settings {@link Settings} used to start Bluetooth LE advertising.
* @param callback {@link AdvertiseCallback} for delivering stopping advertising status.
*/
public void stopAdvertising(final Settings settings, final AdvertiseCallback callback) {
if (callback == null) {
throw new IllegalArgumentException("callback cannot be null");
}
AdvertiseCallbackWrapper wrapper = mLeAdvertisers.get(settings);
if (wrapper == null) {
postCallbackFailure(callback, AdvertiseCallback.ADVERISING_NOT_STARTED);
return;
}
try {
mBluetoothGatt.stopMultiAdvertising(wrapper.mLeHandle);
if (wrapper.advertiseStopped()) {
mLeAdvertisers.remove(settings);
}
} catch (RemoteException e) {
Log.e(TAG, "failed to stop advertising", e);
}
}
private void postCallbackFailure(final AdvertiseCallback callback, final int error) {
mHandler.post(new Runnable() {
@Override
public void run() {
callback.onFailure(error);
}
});
}
}