1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.bluetooth.le; 18 19 import android.bluetooth.BluetoothAdapter; 20 import android.bluetooth.BluetoothGatt; 21 import android.bluetooth.BluetoothGattCallbackWrapper; 22 import android.bluetooth.BluetoothUuid; 23 import android.bluetooth.IBluetoothGatt; 24 import android.bluetooth.IBluetoothManager; 25 import android.os.Handler; 26 import android.os.Looper; 27 import android.os.ParcelUuid; 28 import android.os.RemoteException; 29 import android.util.Log; 30 31 import java.util.HashMap; 32 import java.util.Map; 33 import java.util.UUID; 34 35 /** 36 * This class provides a way to perform Bluetooth LE advertise operations, such as starting and 37 * stopping advertising. An advertiser can broadcast up to 31 bytes of advertisement data 38 * represented by {@link AdvertiseData}. 39 * <p> 40 * To get an instance of {@link BluetoothLeAdvertiser}, call the 41 * {@link BluetoothAdapter#getBluetoothLeAdvertiser()} method. 42 * <p> 43 * <b>Note:</b> Most of the methods here require {@link android.Manifest.permission#BLUETOOTH_ADMIN} 44 * permission. 45 * 46 * @see AdvertiseData 47 */ 48 public final class BluetoothLeAdvertiser { 49 50 private static final String TAG = "BluetoothLeAdvertiser"; 51 52 private static final int MAX_ADVERTISING_DATA_BYTES = 31; 53 // Each fields need one byte for field length and another byte for field type. 54 private static final int OVERHEAD_BYTES_PER_FIELD = 2; 55 // Flags field will be set by system. 56 private static final int FLAGS_FIELD_BYTES = 3; 57 private static final int MANUFACTURER_SPECIFIC_DATA_LENGTH = 2; 58 private static final int SERVICE_DATA_UUID_LENGTH = 2; 59 60 private final IBluetoothManager mBluetoothManager; 61 private final Handler mHandler; 62 private BluetoothAdapter mBluetoothAdapter; 63 private final Map<AdvertiseCallback, AdvertiseCallbackWrapper> 64 mLeAdvertisers = new HashMap<AdvertiseCallback, AdvertiseCallbackWrapper>(); 65 66 /** 67 * Use BluetoothAdapter.getLeAdvertiser() instead. 68 * 69 * @param bluetoothManager BluetoothManager that conducts overall Bluetooth Management 70 * @hide 71 */ BluetoothLeAdvertiser(IBluetoothManager bluetoothManager)72 public BluetoothLeAdvertiser(IBluetoothManager bluetoothManager) { 73 mBluetoothManager = bluetoothManager; 74 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 75 mHandler = new Handler(Looper.getMainLooper()); 76 } 77 78 /** 79 * Start Bluetooth LE Advertising. On success, the {@code advertiseData} will be broadcasted. 80 * Returns immediately, the operation status is delivered through {@code callback}. 81 * <p> 82 * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission. 83 * 84 * @param settings Settings for Bluetooth LE advertising. 85 * @param advertiseData Advertisement data to be broadcasted. 86 * @param callback Callback for advertising status. 87 */ startAdvertising(AdvertiseSettings settings, AdvertiseData advertiseData, final AdvertiseCallback callback)88 public void startAdvertising(AdvertiseSettings settings, 89 AdvertiseData advertiseData, final AdvertiseCallback callback) { 90 startAdvertising(settings, advertiseData, null, callback); 91 } 92 93 /** 94 * Start Bluetooth LE Advertising. The {@code advertiseData} will be broadcasted if the 95 * operation succeeds. The {@code scanResponse} is returned when a scanning device sends an 96 * active scan request. This method returns immediately, the operation status is delivered 97 * through {@code callback}. 98 * <p> 99 * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} 100 * 101 * @param settings Settings for Bluetooth LE advertising. 102 * @param advertiseData Advertisement data to be advertised in advertisement packet. 103 * @param scanResponse Scan response associated with the advertisement data. 104 * @param callback Callback for advertising status. 105 */ startAdvertising(AdvertiseSettings settings, AdvertiseData advertiseData, AdvertiseData scanResponse, final AdvertiseCallback callback)106 public void startAdvertising(AdvertiseSettings settings, 107 AdvertiseData advertiseData, AdvertiseData scanResponse, 108 final AdvertiseCallback callback) { 109 synchronized (mLeAdvertisers) { 110 BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter); 111 if (callback == null) { 112 throw new IllegalArgumentException("callback cannot be null"); 113 } 114 if (!mBluetoothAdapter.isMultipleAdvertisementSupported() && 115 !mBluetoothAdapter.isPeripheralModeSupported()) { 116 postStartFailure(callback, 117 AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED); 118 return; 119 } 120 boolean isConnectable = settings.isConnectable(); 121 if (totalBytes(advertiseData, isConnectable) > MAX_ADVERTISING_DATA_BYTES || 122 totalBytes(scanResponse, false) > MAX_ADVERTISING_DATA_BYTES) { 123 postStartFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE); 124 return; 125 } 126 if (mLeAdvertisers.containsKey(callback)) { 127 postStartFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED); 128 return; 129 } 130 131 IBluetoothGatt gatt; 132 try { 133 gatt = mBluetoothManager.getBluetoothGatt(); 134 } catch (RemoteException e) { 135 Log.e(TAG, "Failed to get Bluetooth gatt - ", e); 136 postStartFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR); 137 return; 138 } 139 AdvertiseCallbackWrapper wrapper = new AdvertiseCallbackWrapper(callback, advertiseData, 140 scanResponse, settings, gatt); 141 wrapper.startRegisteration(); 142 } 143 } 144 145 /** 146 * Stop Bluetooth LE advertising. The {@code callback} must be the same one use in 147 * {@link BluetoothLeAdvertiser#startAdvertising}. 148 * <p> 149 * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission. 150 * 151 * @param callback {@link AdvertiseCallback} identifies the advertising instance to stop. 152 */ stopAdvertising(final AdvertiseCallback callback)153 public void stopAdvertising(final AdvertiseCallback callback) { 154 synchronized (mLeAdvertisers) { 155 if (callback == null) { 156 throw new IllegalArgumentException("callback cannot be null"); 157 } 158 AdvertiseCallbackWrapper wrapper = mLeAdvertisers.get(callback); 159 if (wrapper == null) return; 160 wrapper.stopAdvertising(); 161 } 162 } 163 164 /** 165 * Cleans up advertise clients. Should be called when bluetooth is down. 166 * 167 * @hide 168 */ cleanup()169 public void cleanup() { 170 mLeAdvertisers.clear(); 171 } 172 173 // Compute the size of advertisement data or scan resp totalBytes(AdvertiseData data, boolean isFlagsIncluded)174 private int totalBytes(AdvertiseData data, boolean isFlagsIncluded) { 175 if (data == null) return 0; 176 // Flags field is omitted if the advertising is not connectable. 177 int size = (isFlagsIncluded) ? FLAGS_FIELD_BYTES : 0; 178 if (data.getServiceUuids() != null) { 179 int num16BitUuids = 0; 180 int num32BitUuids = 0; 181 int num128BitUuids = 0; 182 for (ParcelUuid uuid : data.getServiceUuids()) { 183 if (BluetoothUuid.is16BitUuid(uuid)) { 184 ++num16BitUuids; 185 } else if (BluetoothUuid.is32BitUuid(uuid)) { 186 ++num32BitUuids; 187 } else { 188 ++num128BitUuids; 189 } 190 } 191 // 16 bit service uuids are grouped into one field when doing advertising. 192 if (num16BitUuids != 0) { 193 size += OVERHEAD_BYTES_PER_FIELD + 194 num16BitUuids * BluetoothUuid.UUID_BYTES_16_BIT; 195 } 196 // 32 bit service uuids are grouped into one field when doing advertising. 197 if (num32BitUuids != 0) { 198 size += OVERHEAD_BYTES_PER_FIELD + 199 num32BitUuids * BluetoothUuid.UUID_BYTES_32_BIT; 200 } 201 // 128 bit service uuids are grouped into one field when doing advertising. 202 if (num128BitUuids != 0) { 203 size += OVERHEAD_BYTES_PER_FIELD + 204 num128BitUuids * BluetoothUuid.UUID_BYTES_128_BIT; 205 } 206 } 207 for (ParcelUuid uuid : data.getServiceData().keySet()) { 208 size += OVERHEAD_BYTES_PER_FIELD + SERVICE_DATA_UUID_LENGTH 209 + byteLength(data.getServiceData().get(uuid)); 210 } 211 for (int i = 0; i < data.getManufacturerSpecificData().size(); ++i) { 212 size += OVERHEAD_BYTES_PER_FIELD + MANUFACTURER_SPECIFIC_DATA_LENGTH + 213 byteLength(data.getManufacturerSpecificData().valueAt(i)); 214 } 215 if (data.getIncludeTxPowerLevel()) { 216 size += OVERHEAD_BYTES_PER_FIELD + 1; // tx power level value is one byte. 217 } 218 if (data.getIncludeDeviceName() && mBluetoothAdapter.getName() != null) { 219 size += OVERHEAD_BYTES_PER_FIELD + mBluetoothAdapter.getName().length(); 220 } 221 return size; 222 } 223 byteLength(byte[] array)224 private int byteLength(byte[] array) { 225 return array == null ? 0 : array.length; 226 } 227 228 /** 229 * Bluetooth GATT interface callbacks for advertising. 230 */ 231 private class AdvertiseCallbackWrapper extends BluetoothGattCallbackWrapper { 232 private static final int LE_CALLBACK_TIMEOUT_MILLIS = 2000; 233 private final AdvertiseCallback mAdvertiseCallback; 234 private final AdvertiseData mAdvertisement; 235 private final AdvertiseData mScanResponse; 236 private final AdvertiseSettings mSettings; 237 private final IBluetoothGatt mBluetoothGatt; 238 239 // mClientIf 0: not registered 240 // -1: advertise stopped or registration timeout 241 // >0: registered and advertising started 242 private int mClientIf; 243 private boolean mIsAdvertising = false; 244 AdvertiseCallbackWrapper(AdvertiseCallback advertiseCallback, AdvertiseData advertiseData, AdvertiseData scanResponse, AdvertiseSettings settings, IBluetoothGatt bluetoothGatt)245 public AdvertiseCallbackWrapper(AdvertiseCallback advertiseCallback, 246 AdvertiseData advertiseData, AdvertiseData scanResponse, 247 AdvertiseSettings settings, 248 IBluetoothGatt bluetoothGatt) { 249 mAdvertiseCallback = advertiseCallback; 250 mAdvertisement = advertiseData; 251 mScanResponse = scanResponse; 252 mSettings = settings; 253 mBluetoothGatt = bluetoothGatt; 254 mClientIf = 0; 255 } 256 startRegisteration()257 public void startRegisteration() { 258 synchronized (this) { 259 if (mClientIf == -1) return; 260 261 try { 262 UUID uuid = UUID.randomUUID(); 263 mBluetoothGatt.registerClient(new ParcelUuid(uuid), this); 264 wait(LE_CALLBACK_TIMEOUT_MILLIS); 265 } catch (InterruptedException | RemoteException e) { 266 Log.e(TAG, "Failed to start registeration", e); 267 } 268 if (mClientIf > 0 && mIsAdvertising) { 269 mLeAdvertisers.put(mAdvertiseCallback, this); 270 } else if (mClientIf <= 0) { 271 272 // Registration timeout, reset mClientIf to -1 so no subsequent operations can 273 // proceed. 274 if (mClientIf == 0) mClientIf = -1; 275 // Post internal error if registration failed. 276 postStartFailure(mAdvertiseCallback, 277 AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR); 278 } else { 279 // Unregister application if it's already registered but advertise failed. 280 try { 281 mBluetoothGatt.unregisterClient(mClientIf); 282 mClientIf = -1; 283 } catch (RemoteException e) { 284 Log.e(TAG, "remote exception when unregistering", e); 285 } 286 } 287 } 288 } 289 stopAdvertising()290 public void stopAdvertising() { 291 synchronized (this) { 292 try { 293 mBluetoothGatt.stopMultiAdvertising(mClientIf); 294 wait(LE_CALLBACK_TIMEOUT_MILLIS); 295 } catch (InterruptedException | RemoteException e) { 296 Log.e(TAG, "Failed to stop advertising", e); 297 } 298 // Advertise callback should have been removed from LeAdvertisers when 299 // onMultiAdvertiseCallback was called. In case onMultiAdvertiseCallback is never 300 // invoked and wait timeout expires, remove callback here. 301 if (mLeAdvertisers.containsKey(mAdvertiseCallback)) { 302 mLeAdvertisers.remove(mAdvertiseCallback); 303 } 304 } 305 } 306 307 /** 308 * Application interface registered - app is ready to go 309 */ 310 @Override onClientRegistered(int status, int clientIf)311 public void onClientRegistered(int status, int clientIf) { 312 Log.d(TAG, "onClientRegistered() - status=" + status + " clientIf=" + clientIf); 313 synchronized (this) { 314 if (status == BluetoothGatt.GATT_SUCCESS) { 315 try { 316 if (mClientIf == -1) { 317 // Registration succeeds after timeout, unregister client. 318 mBluetoothGatt.unregisterClient(clientIf); 319 } else { 320 mClientIf = clientIf; 321 mBluetoothGatt.startMultiAdvertising(mClientIf, mAdvertisement, 322 mScanResponse, mSettings); 323 } 324 return; 325 } catch (RemoteException e) { 326 Log.e(TAG, "failed to start advertising", e); 327 } 328 } 329 // Registration failed. 330 mClientIf = -1; 331 notifyAll(); 332 } 333 } 334 335 @Override onMultiAdvertiseCallback(int status, boolean isStart, AdvertiseSettings settings)336 public void onMultiAdvertiseCallback(int status, boolean isStart, 337 AdvertiseSettings settings) { 338 synchronized (this) { 339 if (isStart) { 340 if (status == AdvertiseCallback.ADVERTISE_SUCCESS) { 341 // Start success 342 mIsAdvertising = true; 343 postStartSuccess(mAdvertiseCallback, settings); 344 } else { 345 // Start failure. 346 postStartFailure(mAdvertiseCallback, status); 347 } 348 } else { 349 // unregister client for stop. 350 try { 351 mBluetoothGatt.unregisterClient(mClientIf); 352 mClientIf = -1; 353 mIsAdvertising = false; 354 mLeAdvertisers.remove(mAdvertiseCallback); 355 } catch (RemoteException e) { 356 Log.e(TAG, "remote exception when unregistering", e); 357 } 358 } 359 notifyAll(); 360 } 361 362 } 363 } 364 postStartFailure(final AdvertiseCallback callback, final int error)365 private void postStartFailure(final AdvertiseCallback callback, final int error) { 366 mHandler.post(new Runnable() { 367 @Override 368 public void run() { 369 callback.onStartFailure(error); 370 } 371 }); 372 } 373 postStartSuccess(final AdvertiseCallback callback, final AdvertiseSettings settings)374 private void postStartSuccess(final AdvertiseCallback callback, 375 final AdvertiseSettings settings) { 376 mHandler.post(new Runnable() { 377 378 @Override 379 public void run() { 380 callback.onStartSuccess(settings); 381 } 382 }); 383 } 384 } 385