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 */ 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 */ 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 */ 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 postStartFailure(callback, 116 AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED); 117 return; 118 } 119 if (totalBytes(advertiseData) > MAX_ADVERTISING_DATA_BYTES || 120 totalBytes(scanResponse) > MAX_ADVERTISING_DATA_BYTES) { 121 postStartFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE); 122 return; 123 } 124 if (mLeAdvertisers.containsKey(callback)) { 125 postStartFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED); 126 return; 127 } 128 129 IBluetoothGatt gatt; 130 try { 131 gatt = mBluetoothManager.getBluetoothGatt(); 132 } catch (RemoteException e) { 133 Log.e(TAG, "Failed to get Bluetooth gatt - ", e); 134 postStartFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR); 135 return; 136 } 137 AdvertiseCallbackWrapper wrapper = new AdvertiseCallbackWrapper(callback, advertiseData, 138 scanResponse, settings, gatt); 139 wrapper.startRegisteration(); 140 } 141 } 142 143 /** 144 * Stop Bluetooth LE advertising. The {@code callback} must be the same one use in 145 * {@link BluetoothLeAdvertiser#startAdvertising}. 146 * <p> 147 * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission. 148 * 149 * @param callback {@link AdvertiseCallback} identifies the advertising instance to stop. 150 */ 151 public void stopAdvertising(final AdvertiseCallback callback) { 152 synchronized (mLeAdvertisers) { 153 BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter); 154 if (callback == null) { 155 throw new IllegalArgumentException("callback cannot be null"); 156 } 157 AdvertiseCallbackWrapper wrapper = mLeAdvertisers.get(callback); 158 if (wrapper == null) return; 159 wrapper.stopAdvertising(); 160 } 161 } 162 163 /** 164 * Cleans up advertise clients. Should be called when bluetooth is down. 165 * 166 * @hide 167 */ 168 public void cleanup() { 169 mLeAdvertisers.clear(); 170 } 171 172 // Compute the size of the advertise data. 173 private int totalBytes(AdvertiseData data) { 174 if (data == null) return 0; 175 int size = FLAGS_FIELD_BYTES; // flags field is always set. 176 if (data.getServiceUuids() != null) { 177 int num16BitUuids = 0; 178 int num32BitUuids = 0; 179 int num128BitUuids = 0; 180 for (ParcelUuid uuid : data.getServiceUuids()) { 181 if (BluetoothUuid.is16BitUuid(uuid)) { 182 ++num16BitUuids; 183 } else if (BluetoothUuid.is32BitUuid(uuid)) { 184 ++num32BitUuids; 185 } else { 186 ++num128BitUuids; 187 } 188 } 189 // 16 bit service uuids are grouped into one field when doing advertising. 190 if (num16BitUuids != 0) { 191 size += OVERHEAD_BYTES_PER_FIELD + 192 num16BitUuids * BluetoothUuid.UUID_BYTES_16_BIT; 193 } 194 // 32 bit service uuids are grouped into one field when doing advertising. 195 if (num32BitUuids != 0) { 196 size += OVERHEAD_BYTES_PER_FIELD + 197 num32BitUuids * BluetoothUuid.UUID_BYTES_32_BIT; 198 } 199 // 128 bit service uuids are grouped into one field when doing advertising. 200 if (num128BitUuids != 0) { 201 size += OVERHEAD_BYTES_PER_FIELD + 202 num128BitUuids * BluetoothUuid.UUID_BYTES_128_BIT; 203 } 204 } 205 for (ParcelUuid uuid : data.getServiceData().keySet()) { 206 size += OVERHEAD_BYTES_PER_FIELD + SERVICE_DATA_UUID_LENGTH 207 + byteLength(data.getServiceData().get(uuid)); 208 } 209 for (int i = 0; i < data.getManufacturerSpecificData().size(); ++i) { 210 size += OVERHEAD_BYTES_PER_FIELD + MANUFACTURER_SPECIFIC_DATA_LENGTH + 211 byteLength(data.getManufacturerSpecificData().valueAt(i)); 212 } 213 if (data.getIncludeTxPowerLevel()) { 214 size += OVERHEAD_BYTES_PER_FIELD + 1; // tx power level value is one byte. 215 } 216 if (data.getIncludeDeviceName() && mBluetoothAdapter.getName() != null) { 217 size += OVERHEAD_BYTES_PER_FIELD + mBluetoothAdapter.getName().length(); 218 } 219 return size; 220 } 221 222 private int byteLength(byte[] array) { 223 return array == null ? 0 : array.length; 224 } 225 226 /** 227 * Bluetooth GATT interface callbacks for advertising. 228 */ 229 private class AdvertiseCallbackWrapper extends BluetoothGattCallbackWrapper { 230 private static final int LE_CALLBACK_TIMEOUT_MILLIS = 2000; 231 private final AdvertiseCallback mAdvertiseCallback; 232 private final AdvertiseData mAdvertisement; 233 private final AdvertiseData mScanResponse; 234 private final AdvertiseSettings mSettings; 235 private final IBluetoothGatt mBluetoothGatt; 236 237 // mClientIf 0: not registered 238 // -1: scan stopped 239 // >0: registered and scan started 240 private int mClientIf; 241 private boolean mIsAdvertising = false; 242 243 public AdvertiseCallbackWrapper(AdvertiseCallback advertiseCallback, 244 AdvertiseData advertiseData, AdvertiseData scanResponse, 245 AdvertiseSettings settings, 246 IBluetoothGatt bluetoothGatt) { 247 mAdvertiseCallback = advertiseCallback; 248 mAdvertisement = advertiseData; 249 mScanResponse = scanResponse; 250 mSettings = settings; 251 mBluetoothGatt = bluetoothGatt; 252 mClientIf = 0; 253 } 254 255 public void startRegisteration() { 256 synchronized (this) { 257 if (mClientIf == -1) return; 258 259 try { 260 UUID uuid = UUID.randomUUID(); 261 mBluetoothGatt.registerClient(new ParcelUuid(uuid), this); 262 wait(LE_CALLBACK_TIMEOUT_MILLIS); 263 } catch (InterruptedException | RemoteException e) { 264 Log.e(TAG, "Failed to start registeration", e); 265 } 266 if (mClientIf > 0 && mIsAdvertising) { 267 mLeAdvertisers.put(mAdvertiseCallback, this); 268 } else if (mClientIf <= 0) { 269 // Post internal error if registration failed. 270 postStartFailure(mAdvertiseCallback, 271 AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR); 272 } else { 273 // Unregister application if it's already registered but advertise failed. 274 try { 275 mBluetoothGatt.unregisterClient(mClientIf); 276 mClientIf = -1; 277 } catch (RemoteException e) { 278 Log.e(TAG, "remote exception when unregistering", e); 279 } 280 } 281 } 282 } 283 284 public void stopAdvertising() { 285 synchronized (this) { 286 try { 287 mBluetoothGatt.stopMultiAdvertising(mClientIf); 288 wait(LE_CALLBACK_TIMEOUT_MILLIS); 289 } catch (InterruptedException | RemoteException e) { 290 Log.e(TAG, "Failed to stop advertising", e); 291 } 292 // Advertise callback should have been removed from LeAdvertisers when 293 // onMultiAdvertiseCallback was called. In case onMultiAdvertiseCallback is never 294 // invoked and wait timeout expires, remove callback here. 295 if (mLeAdvertisers.containsKey(mAdvertiseCallback)) { 296 mLeAdvertisers.remove(mAdvertiseCallback); 297 } 298 } 299 } 300 301 /** 302 * Application interface registered - app is ready to go 303 */ 304 @Override 305 public void onClientRegistered(int status, int clientIf) { 306 Log.d(TAG, "onClientRegistered() - status=" + status + " clientIf=" + clientIf); 307 synchronized (this) { 308 if (status == BluetoothGatt.GATT_SUCCESS) { 309 mClientIf = clientIf; 310 try { 311 mBluetoothGatt.startMultiAdvertising(mClientIf, mAdvertisement, 312 mScanResponse, mSettings); 313 return; 314 } catch (RemoteException e) { 315 Log.e(TAG, "failed to start advertising", e); 316 } 317 } 318 // Registration failed. 319 mClientIf = -1; 320 notifyAll(); 321 } 322 } 323 324 @Override 325 public void onMultiAdvertiseCallback(int status, boolean isStart, 326 AdvertiseSettings settings) { 327 synchronized (this) { 328 if (isStart) { 329 if (status == AdvertiseCallback.ADVERTISE_SUCCESS) { 330 // Start success 331 mIsAdvertising = true; 332 postStartSuccess(mAdvertiseCallback, settings); 333 } else { 334 // Start failure. 335 postStartFailure(mAdvertiseCallback, status); 336 } 337 } else { 338 // unregister client for stop. 339 try { 340 mBluetoothGatt.unregisterClient(mClientIf); 341 mClientIf = -1; 342 mIsAdvertising = false; 343 mLeAdvertisers.remove(mAdvertiseCallback); 344 } catch (RemoteException e) { 345 Log.e(TAG, "remote exception when unregistering", e); 346 } 347 } 348 notifyAll(); 349 } 350 351 } 352 } 353 354 private void postStartFailure(final AdvertiseCallback callback, final int error) { 355 mHandler.post(new Runnable() { 356 @Override 357 public void run() { 358 callback.onStartFailure(error); 359 } 360 }); 361 } 362 363 private void postStartSuccess(final AdvertiseCallback callback, 364 final AdvertiseSettings settings) { 365 mHandler.post(new Runnable() { 366 367 @Override 368 public void run() { 369 callback.onStartSuccess(settings); 370 } 371 }); 372 } 373 } 374