/* * 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 mLeAdvertisers = new HashMap(); /** * Use BluetoothAdapter.getLeAdvertiser() instead. * * @param bluetoothManager BluetoothManager that conducts overall Bluetooth Management * @hide */ public BluetoothLeAdvertiser(IBluetoothManager bluetoothManager) { mBluetoothManager = bluetoothManager; mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mHandler = new Handler(Looper.getMainLooper()); } /** * Start Bluetooth LE Advertising. On success, the {@code advertiseData} will be broadcasted. * Returns immediately, the operation status is delivered through {@code callback}. *

* 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 results) { // no op } @Override public void onFoundOrLost(boolean onFound, String address, int rssi, byte[] advData) { // no op } } //TODO: move this api to a common util class. private void checkAdapterState() { if (mBluetoothAdapter.getState() != mBluetoothAdapter.STATE_ON) { throw new IllegalStateException("BT Adapter is not turned ON"); } } private void postCallbackFailure(final AdvertiseCallback callback, final int error) { mHandler.post(new Runnable() { @Override public void run() { callback.onStartFailure(error); } }); } }