/* * 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.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresNoPermission; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGattServer; import android.bluetooth.BluetoothUuid; import android.bluetooth.IBluetoothGatt; import android.bluetooth.annotations.RequiresBluetoothAdvertisePermission; import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission; import android.content.AttributionSource; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.ParcelUuid; import android.os.RemoteException; import android.util.Log; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * 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. * * @see AdvertiseData */ public final class BluetoothLeAdvertiser { private static final String TAG = "BluetoothLeAdvertiser"; private static final int MAX_LEGACY_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 final BluetoothAdapter mBluetoothAdapter; private final AttributionSource mAttributionSource; private final Handler mHandler; private final Map mLegacyAdvertisers = new HashMap<>(); private final Map mCallbackWrappers = Collections.synchronizedMap(new HashMap<>()); private final Map mAdvertisingSets = Collections.synchronizedMap(new HashMap<>()); /** * Use BluetoothAdapter.getLeAdvertiser() instead. * * @hide */ public BluetoothLeAdvertiser(BluetoothAdapter bluetoothAdapter) { mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter); mAttributionSource = mBluetoothAdapter.getAttributionSource(); 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}. * * @param settings Settings for Bluetooth LE advertising. * @param advertiseData Advertisement data to be broadcasted. * @param callback Callback for advertising status. */ @RequiresLegacyBluetoothAdminPermission @RequiresBluetoothAdvertisePermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE) 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}. * * @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. */ @RequiresLegacyBluetoothAdminPermission @RequiresBluetoothAdvertisePermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE) public void startAdvertising( AdvertiseSettings settings, AdvertiseData advertiseData, AdvertiseData scanResponse, final AdvertiseCallback callback) { synchronized (mLegacyAdvertisers) { BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter); if (callback == null) { throw new IllegalArgumentException("callback cannot be null"); } boolean isConnectable = settings.isConnectable(); boolean isDiscoverable = settings.isDiscoverable(); boolean hasFlags = isConnectable && isDiscoverable; if (totalBytes(advertiseData, hasFlags) > MAX_LEGACY_ADVERTISING_DATA_BYTES || totalBytes(scanResponse, false) > MAX_LEGACY_ADVERTISING_DATA_BYTES) { postStartFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE); return; } if (mLegacyAdvertisers.containsKey(callback)) { postStartFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED); return; } AdvertisingSetParameters.Builder parameters = new AdvertisingSetParameters.Builder(); parameters.setLegacyMode(true); parameters.setConnectable(isConnectable); parameters.setDiscoverable(isDiscoverable); parameters.setScannable(true); // legacy advertisements we support are always scannable parameters.setOwnAddressType(settings.getOwnAddressType()); if (settings.getMode() == AdvertiseSettings.ADVERTISE_MODE_LOW_POWER) { parameters.setInterval(1600); // 1s } else if (settings.getMode() == AdvertiseSettings.ADVERTISE_MODE_BALANCED) { parameters.setInterval(400); // 250ms } else if (settings.getMode() == AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) { parameters.setInterval(160); // 100ms } if (settings.getTxPowerLevel() == AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW) { Log.d(TAG, "TxPower == ADVERTISE_TX_POWER_ULTRA_LOW"); parameters.setTxPowerLevel(-21); } else if (settings.getTxPowerLevel() == AdvertiseSettings.ADVERTISE_TX_POWER_LOW) { Log.d(TAG, "TxPower == ADVERTISE_TX_POWER_LOW"); parameters.setTxPowerLevel(-15); } else if (settings.getTxPowerLevel() == AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) { Log.d(TAG, "TxPower == ADVERTISE_TX_POWER_MEDIUM"); parameters.setTxPowerLevel(-7); } else if (settings.getTxPowerLevel() == AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) { Log.d(TAG, "TxPower == ADVERTISE_TX_POWER_HIGH"); parameters.setTxPowerLevel(1); } int duration = 0; int timeoutMillis = settings.getTimeout(); if (timeoutMillis > 0) { duration = (timeoutMillis < 10) ? 1 : timeoutMillis / 10; } AdvertisingSetCallback wrapped = wrapOldCallback(callback, settings); mLegacyAdvertisers.put(callback, wrapped); startAdvertisingSet( parameters.build(), advertiseData, scanResponse, null, null, duration, 0, wrapped); } } @SuppressLint({ "AndroidFrameworkBluetoothPermission", "AndroidFrameworkRequiresPermission", }) AdvertisingSetCallback wrapOldCallback(AdvertiseCallback callback, AdvertiseSettings settings) { return new AdvertisingSetCallback() { @Override public void onAdvertisingSetStarted( AdvertisingSet advertisingSet, int txPower, int status) { if (status != AdvertisingSetCallback.ADVERTISE_SUCCESS) { postStartFailure(callback, status); return; } postStartSuccess(callback, settings); } /* Legacy advertiser is disabled on timeout */ @Override public void onAdvertisingEnabled( AdvertisingSet advertisingSet, boolean enabled, int status) { if (enabled) { Log.e( TAG, "Legacy advertiser should be only disabled on timeout," + " but was enabled!"); return; } stopAdvertising(callback); } }; } /** * Stop Bluetooth LE advertising. The {@code callback} must be the same one use in {@link * BluetoothLeAdvertiser#startAdvertising}. * * @param callback {@link AdvertiseCallback} identifies the advertising instance to stop. */ @RequiresLegacyBluetoothAdminPermission @RequiresBluetoothAdvertisePermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE) public void stopAdvertising(final AdvertiseCallback callback) { synchronized (mLegacyAdvertisers) { if (callback == null) { throw new IllegalArgumentException("callback cannot be null"); } AdvertisingSetCallback wrapper = mLegacyAdvertisers.get(callback); if (wrapper == null) return; stopAdvertisingSet(wrapper); mLegacyAdvertisers.remove(callback); } } /** * Creates a new advertising set. If operation succeed, device will start advertising. This * method returns immediately, the operation status is delivered through {@code * callback.onAdvertisingSetStarted()}. * *

* * @param parameters advertising set parameters. * @param advertiseData Advertisement data to be broadcasted. Size must not exceed {@link * BluetoothAdapter#getLeMaximumAdvertisingDataLength}. If the advertisement is connectable, * three bytes will be added for flags. * @param scanResponse Scan response associated with the advertisement data. Size must not * exceed {@link BluetoothAdapter#getLeMaximumAdvertisingDataLength}. * @param periodicParameters periodic advertisng parameters. If null, periodic advertising will * not be started. * @param periodicData Periodic advertising data. Size must not exceed {@link * BluetoothAdapter#getLeMaximumAdvertisingDataLength}. * @param callback Callback for advertising set. * @throws IllegalArgumentException when any of the data parameter exceed the maximum allowable * size, or unsupported advertising PHY is selected, or when attempt to use Periodic * Advertising feature is made when it's not supported by the controller. */ @RequiresLegacyBluetoothAdminPermission @RequiresBluetoothAdvertisePermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE) public void startAdvertisingSet( AdvertisingSetParameters parameters, AdvertiseData advertiseData, AdvertiseData scanResponse, PeriodicAdvertisingParameters periodicParameters, AdvertiseData periodicData, AdvertisingSetCallback callback) { startAdvertisingSet( parameters, advertiseData, scanResponse, periodicParameters, periodicData, 0, 0, callback, new Handler(Looper.getMainLooper())); } /** * Creates a new advertising set. If operation succeed, device will start advertising. This * method returns immediately, the operation status is delivered through {@code * callback.onAdvertisingSetStarted()}. * *

* * @param parameters advertising set parameters. * @param advertiseData Advertisement data to be broadcasted. Size must not exceed {@link * BluetoothAdapter#getLeMaximumAdvertisingDataLength}. If the advertisement is connectable, * three bytes will be added for flags. * @param scanResponse Scan response associated with the advertisement data. Size must not * exceed {@link BluetoothAdapter#getLeMaximumAdvertisingDataLength}. * @param periodicParameters periodic advertisng parameters. If null, periodic advertising will * not be started. * @param periodicData Periodic advertising data. Size must not exceed {@link * BluetoothAdapter#getLeMaximumAdvertisingDataLength}. * @param callback Callback for advertising set. * @param handler thread upon which the callbacks will be invoked. * @throws IllegalArgumentException when any of the data parameter exceed the maximum allowable * size, or unsupported advertising PHY is selected, or when attempt to use Periodic * Advertising feature is made when it's not supported by the controller. */ @RequiresLegacyBluetoothAdminPermission @RequiresBluetoothAdvertisePermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE) public void startAdvertisingSet( AdvertisingSetParameters parameters, AdvertiseData advertiseData, AdvertiseData scanResponse, PeriodicAdvertisingParameters periodicParameters, AdvertiseData periodicData, AdvertisingSetCallback callback, Handler handler) { startAdvertisingSet( parameters, advertiseData, scanResponse, periodicParameters, periodicData, 0, 0, callback, handler); } /** * Creates a new advertising set. If operation succeed, device will start advertising. This * method returns immediately, the operation status is delivered through {@code * callback.onAdvertisingSetStarted()}. * *

* * @param parameters advertising set parameters. * @param advertiseData Advertisement data to be broadcasted. Size must not exceed {@link * BluetoothAdapter#getLeMaximumAdvertisingDataLength}. If the advertisement is connectable, * three bytes will be added for flags. * @param scanResponse Scan response associated with the advertisement data. Size must not * exceed {@link BluetoothAdapter#getLeMaximumAdvertisingDataLength}. * @param periodicParameters periodic advertisng parameters. If null, periodic advertising will * not be started. * @param periodicData Periodic advertising data. Size must not exceed {@link * BluetoothAdapter#getLeMaximumAdvertisingDataLength}. * @param duration advertising duration, in 10ms unit. Valid range is from 1 (10ms) to 65535 * (655,350 ms). 0 means advertising should continue until stopped. * @param maxExtendedAdvertisingEvents maximum number of extended advertising events the * controller shall attempt to send prior to terminating the extended advertising, even if * the duration has not expired. Valid range is from 1 to 255. 0 means no maximum. * @param callback Callback for advertising set. * @throws IllegalArgumentException when any of the data parameter exceed the maximum allowable * size, or unsupported advertising PHY is selected, or when attempt to use Periodic * Advertising feature is made when it's not supported by the controller. */ @RequiresLegacyBluetoothAdminPermission @RequiresBluetoothAdvertisePermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE) public void startAdvertisingSet( AdvertisingSetParameters parameters, AdvertiseData advertiseData, AdvertiseData scanResponse, PeriodicAdvertisingParameters periodicParameters, AdvertiseData periodicData, int duration, int maxExtendedAdvertisingEvents, AdvertisingSetCallback callback) { startAdvertisingSet( parameters, advertiseData, scanResponse, periodicParameters, periodicData, duration, maxExtendedAdvertisingEvents, callback, new Handler(Looper.getMainLooper())); } /** * Creates a new advertising set. If operation succeed, device will start advertising. This * method returns immediately, the operation status is delivered through {@code * callback.onAdvertisingSetStarted()}. * *

* * @param parameters Advertising set parameters. * @param advertiseData Advertisement data to be broadcasted. Size must not exceed {@link * BluetoothAdapter#getLeMaximumAdvertisingDataLength}. If the advertisement is connectable, * three bytes will be added for flags. * @param scanResponse Scan response associated with the advertisement data. Size must not * exceed {@link BluetoothAdapter#getLeMaximumAdvertisingDataLength} * @param periodicParameters Periodic advertisng parameters. If null, periodic advertising will * not be started. * @param periodicData Periodic advertising data. Size must not exceed {@link * BluetoothAdapter#getLeMaximumAdvertisingDataLength} * @param duration advertising duration, in 10ms unit. Valid range is from 1 (10ms) to 65535 * (655,350 ms). 0 means advertising should continue until stopped. * @param maxExtendedAdvertisingEvents maximum number of extended advertising events the * controller shall attempt to send prior to terminating the extended advertising, even if * the duration has not expired. Valid range is from 1 to 255. 0 means no maximum. * @param callback Callback for advertising set. * @param handler Thread upon which the callbacks will be invoked. * @throws IllegalArgumentException When any of the data parameter exceed the maximum allowable * size, or unsupported advertising PHY is selected, or when attempt to use Periodic * Advertising feature is made when it's not supported by the controller, or when * maxExtendedAdvertisingEvents is used on a controller that doesn't support the LE Extended * Advertising */ @RequiresLegacyBluetoothAdminPermission @RequiresBluetoothAdvertisePermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE) public void startAdvertisingSet( AdvertisingSetParameters parameters, AdvertiseData advertiseData, AdvertiseData scanResponse, PeriodicAdvertisingParameters periodicParameters, AdvertiseData periodicData, int duration, int maxExtendedAdvertisingEvents, AdvertisingSetCallback callback, Handler handler) { startAdvertisingSet( parameters, advertiseData, scanResponse, periodicParameters, periodicData, duration, maxExtendedAdvertisingEvents, null, callback, handler); } /** * Creates a new advertising set. If operation succeed, device will start advertising. This * method returns immediately, the operation status is delivered through {@code * callback.onAdvertisingSetStarted()}. * *

If the {@code gattServer} is provided, connections to this advertisement will only see the * services/characteristics in this server, rather than the union of all GATT services (across * all opened servers). * * @param parameters Advertising set parameters. * @param advertiseData Advertisement data to be broadcasted. Size must not exceed {@link * BluetoothAdapter#getLeMaximumAdvertisingDataLength}. If the advertisement is connectable, * three bytes will be added for flags. * @param scanResponse Scan response associated with the advertisement data. Size must not * exceed {@link BluetoothAdapter#getLeMaximumAdvertisingDataLength} * @param periodicParameters Periodic advertisng parameters. If null, periodic advertising will * not be started. * @param periodicData Periodic advertising data. Size must not exceed {@link * BluetoothAdapter#getLeMaximumAdvertisingDataLength} * @param duration advertising duration, in 10ms unit. Valid range is from 1 (10ms) to 65535 * (655,350 ms). 0 means advertising should continue until stopped. * @param maxExtendedAdvertisingEvents maximum number of extended advertising events the * controller shall attempt to send prior to terminating the extended advertising, even if * the duration has not expired. Valid range is from 1 to 255. 0 means no maximum. * @param gattServer the GATT server that will "own" connections derived from this advertising * set. * @param callback Callback for advertising set. * @param handler Thread upon which the callbacks will be invoked. * @throws IllegalArgumentException When any of the data parameter exceed the maximum allowable * size, or unsupported advertising PHY is selected, or when attempt to use Periodic * Advertising feature is made when it's not supported by the controller, or when * maxExtendedAdvertisingEvents is used on a controller that doesn't support the LE Extended * Advertising * @hide */ @SystemApi @SuppressLint("ExecutorRegistration") @RequiresBluetoothAdvertisePermission @RequiresPermission( allOf = { android.Manifest.permission.BLUETOOTH_PRIVILEGED, android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_CONNECT, }) public void startAdvertisingSet( @NonNull AdvertisingSetParameters parameters, @Nullable AdvertiseData advertiseData, @Nullable AdvertiseData scanResponse, @Nullable PeriodicAdvertisingParameters periodicParameters, @Nullable AdvertiseData periodicData, int duration, int maxExtendedAdvertisingEvents, @Nullable BluetoothGattServer gattServer, @Nullable AdvertisingSetCallback callback, @SuppressLint("ListenerLast") @NonNull Handler handler) { BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter); if (callback == null) { throw new IllegalArgumentException("callback cannot be null"); } boolean isConnectable = parameters.isConnectable(); boolean isDiscoverable = parameters.isDiscoverable(); boolean hasFlags = isConnectable && isDiscoverable; if (parameters.isLegacy()) { if (totalBytes(advertiseData, hasFlags) > MAX_LEGACY_ADVERTISING_DATA_BYTES) { throw new IllegalArgumentException("Legacy advertising data too big"); } if (totalBytes(scanResponse, false) > MAX_LEGACY_ADVERTISING_DATA_BYTES) { throw new IllegalArgumentException("Legacy scan response data too big"); } } else { boolean supportCodedPhy = mBluetoothAdapter.isLeCodedPhySupported(); boolean support2MPhy = mBluetoothAdapter.isLe2MPhySupported(); int pphy = parameters.getPrimaryPhy(); int sphy = parameters.getSecondaryPhy(); if (pphy == BluetoothDevice.PHY_LE_CODED && !supportCodedPhy) { throw new IllegalArgumentException("Unsupported primary PHY selected"); } if ((sphy == BluetoothDevice.PHY_LE_CODED && !supportCodedPhy) || (sphy == BluetoothDevice.PHY_LE_2M && !support2MPhy)) { throw new IllegalArgumentException("Unsupported secondary PHY selected"); } int maxData = mBluetoothAdapter.getLeMaximumAdvertisingDataLength(); if (totalBytes(advertiseData, hasFlags) > maxData) { throw new IllegalArgumentException("Advertising data too big"); } if (totalBytes(scanResponse, false) > maxData) { throw new IllegalArgumentException("Scan response data too big"); } if (totalBytes(periodicData, false) > maxData) { throw new IllegalArgumentException("Periodic advertising data too big"); } boolean supportPeriodic = mBluetoothAdapter.isLePeriodicAdvertisingSupported(); if (periodicParameters != null && !supportPeriodic) { throw new IllegalArgumentException( "Controller does not support LE Periodic Advertising"); } } if (maxExtendedAdvertisingEvents < 0 || maxExtendedAdvertisingEvents > 255) { throw new IllegalArgumentException( "maxExtendedAdvertisingEvents out of range: " + maxExtendedAdvertisingEvents); } if (maxExtendedAdvertisingEvents != 0 && !mBluetoothAdapter.isLePeriodicAdvertisingSupported()) { throw new IllegalArgumentException( "Can't use maxExtendedAdvertisingEvents with controller that don't support " + "LE Extended Advertising"); } if (duration < 0 || duration > 65535) { throw new IllegalArgumentException("duration out of range: " + duration); } IBluetoothGatt gatt = mBluetoothAdapter.getBluetoothGatt(); if (gatt == null) { Log.e(TAG, "Bluetooth GATT is null"); postStartSetFailure( handler, callback, AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR); return; } IAdvertisingSetCallback wrapped = wrap(callback, handler); if (mCallbackWrappers.putIfAbsent(callback, wrapped) != null) { throw new IllegalArgumentException( "callback instance already associated with advertising"); } try { gatt.startAdvertisingSet( parameters, advertiseData, scanResponse, periodicParameters, periodicData, duration, maxExtendedAdvertisingEvents, gattServer == null ? 0 : gattServer.getServerIf(), wrapped, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "Failed to start advertising set - ", e); postStartSetFailure( handler, callback, AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR); return; } catch (SecurityException e) { mCallbackWrappers.remove(callback); throw e; } } /** * Used to dispose of a {@link AdvertisingSet} object, obtained with {@link * BluetoothLeAdvertiser#startAdvertisingSet}. */ @RequiresLegacyBluetoothAdminPermission @RequiresBluetoothAdvertisePermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE) public void stopAdvertisingSet(AdvertisingSetCallback callback) { if (callback == null) { throw new IllegalArgumentException("callback cannot be null"); } IAdvertisingSetCallback wrapped = mCallbackWrappers.remove(callback); if (wrapped == null) { return; } IBluetoothGatt gatt = mBluetoothAdapter.getBluetoothGatt(); if (gatt == null) { Log.e(TAG, "Bluetooth GATT is null"); return; } try { gatt.stopAdvertisingSet(wrapped, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "Failed to stop advertising - ", e); } } /** * Cleans up advertisers. Should be called when bluetooth is down. * * @hide */ @RequiresNoPermission public void cleanup() { mLegacyAdvertisers.clear(); mCallbackWrappers.clear(); mAdvertisingSets.clear(); } // Compute the size of advertisement data or scan resp @RequiresBluetoothAdvertisePermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE) private int totalBytes(AdvertiseData data, boolean isFlagsIncluded) { if (data == null) return 0; // Flags field is omitted if the advertising is not connectable. int size = (isFlagsIncluded) ? FLAGS_FIELD_BYTES : 0; 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.getServiceSolicitationUuids() != null) { int num16BitUuids = 0; int num32BitUuids = 0; int num128BitUuids = 0; for (ParcelUuid uuid : data.getServiceSolicitationUuids()) { 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; } } for (TransportDiscoveryData transportDiscoveryData : data.getTransportDiscoveryData()) { size += OVERHEAD_BYTES_PER_FIELD + transportDiscoveryData.totalBytes(); } for (ParcelUuid uuid : data.getServiceData().keySet()) { int uuidLen = BluetoothUuid.uuidToBytes(uuid).length; size += OVERHEAD_BYTES_PER_FIELD + uuidLen + byteLength(data.getServiceData().get(uuid)); } for (int i = 0; i < data.getManufacturerSpecificData().size(); ++i) { size += OVERHEAD_BYTES_PER_FIELD + MANUFACTURER_SPECIFIC_DATA_LENGTH + byteLength(data.getManufacturerSpecificData().valueAt(i)); } if (data.getIncludeTxPowerLevel()) { size += OVERHEAD_BYTES_PER_FIELD + 1; // tx power level value is one byte. } if (data.getIncludeDeviceName()) { final int length = mBluetoothAdapter.getNameLengthForAdvertise(); if (length >= 0) { size += OVERHEAD_BYTES_PER_FIELD + length; } } return size; } private int byteLength(byte[] array) { return array == null ? 0 : array.length; } @SuppressLint("AndroidFrameworkBluetoothPermission") IAdvertisingSetCallback wrap(AdvertisingSetCallback callback, Handler handler) { return new IAdvertisingSetCallback.Stub() { @Override public void onAdvertisingSetStarted( IBinder gattBinder, int advertiserId, int txPower, int status) { handler.post( () -> { if (status != AdvertisingSetCallback.ADVERTISE_SUCCESS) { callback.onAdvertisingSetStarted(null, 0, status); mCallbackWrappers.remove(callback); return; } AdvertisingSet advertisingSet = new AdvertisingSet( IBluetoothGatt.Stub.asInterface(gattBinder), advertiserId, mBluetoothAdapter, mAttributionSource); mAdvertisingSets.put(advertiserId, advertisingSet); callback.onAdvertisingSetStarted(advertisingSet, txPower, status); }); } @Override public void onOwnAddressRead(int advertiserId, int addressType, String address) { handler.post( () -> { AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId); callback.onOwnAddressRead(advertisingSet, addressType, address); }); } @Override public void onAdvertisingSetStopped(int advertiserId) { handler.post( () -> { AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId); callback.onAdvertisingSetStopped(advertisingSet); mAdvertisingSets.remove(advertiserId); mCallbackWrappers.remove(callback); }); } @Override public void onAdvertisingEnabled(int advertiserId, boolean enabled, int status) { handler.post( () -> { AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId); callback.onAdvertisingEnabled(advertisingSet, enabled, status); }); } @Override public void onAdvertisingDataSet(int advertiserId, int status) { handler.post( () -> { AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId); callback.onAdvertisingDataSet(advertisingSet, status); }); } @Override public void onScanResponseDataSet(int advertiserId, int status) { handler.post( () -> { AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId); callback.onScanResponseDataSet(advertisingSet, status); }); } @Override public void onAdvertisingParametersUpdated(int advertiserId, int txPower, int status) { handler.post( () -> { AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId); callback.onAdvertisingParametersUpdated( advertisingSet, txPower, status); }); } @Override public void onPeriodicAdvertisingParametersUpdated(int advertiserId, int status) { handler.post( () -> { AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId); callback.onPeriodicAdvertisingParametersUpdated(advertisingSet, status); }); } @Override public void onPeriodicAdvertisingDataSet(int advertiserId, int status) { handler.post( () -> { AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId); callback.onPeriodicAdvertisingDataSet(advertisingSet, status); }); } @Override public void onPeriodicAdvertisingEnabled(int advertiserId, boolean enable, int status) { handler.post( () -> { AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId); callback.onPeriodicAdvertisingEnabled(advertisingSet, enable, status); }); } }; } @SuppressLint("AndroidFrameworkBluetoothPermission") private void postStartSetFailure( Handler handler, final AdvertisingSetCallback callback, final int error) { handler.post( new Runnable() { @Override public void run() { callback.onAdvertisingSetStarted(null, 0, error); } }); } @SuppressLint("AndroidFrameworkBluetoothPermission") private void postStartFailure(final AdvertiseCallback callback, final int error) { mHandler.post( new Runnable() { @Override public void run() { callback.onStartFailure(error); } }); } @SuppressLint("AndroidFrameworkBluetoothPermission") private void postStartSuccess( final AdvertiseCallback callback, final AdvertiseSettings settings) { mHandler.post( new Runnable() { @Override public void run() { callback.onStartSuccess(settings); } }); } }