1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package android.bluetooth.le; 18 19 import static android.bluetooth.le.BluetoothLeUtils.getSyncTimeout; 20 21 import android.annotation.Nullable; 22 import android.annotation.RequiresPermission; 23 import android.annotation.SuppressLint; 24 import android.bluetooth.Attributable; 25 import android.bluetooth.BluetoothAdapter; 26 import android.bluetooth.BluetoothDevice; 27 import android.bluetooth.IBluetoothGatt; 28 import android.bluetooth.IBluetoothManager; 29 import android.bluetooth.annotations.RequiresBluetoothLocationPermission; 30 import android.bluetooth.annotations.RequiresBluetoothScanPermission; 31 import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission; 32 import android.content.AttributionSource; 33 import android.os.Handler; 34 import android.os.Looper; 35 import android.os.RemoteException; 36 import android.util.Log; 37 38 import com.android.modules.utils.SynchronousResultReceiver; 39 40 import java.util.IdentityHashMap; 41 import java.util.Map; 42 import java.util.Objects; 43 import java.util.concurrent.TimeoutException; 44 45 /** 46 * This class provides methods to perform periodic advertising related 47 * operations. An application can register for periodic advertisements using 48 * {@link PeriodicAdvertisingManager#registerSync}. 49 * <p> 50 * Use {@link BluetoothAdapter#getPeriodicAdvertisingManager()} to get an 51 * instance of {@link PeriodicAdvertisingManager}. 52 * 53 * @hide 54 */ 55 public final class PeriodicAdvertisingManager { 56 57 private static final String TAG = "PeriodicAdvertisingManager"; 58 59 private static final int SKIP_MIN = 0; 60 private static final int SKIP_MAX = 499; 61 private static final int TIMEOUT_MIN = 10; 62 private static final int TIMEOUT_MAX = 16384; 63 64 private static final int SYNC_STARTING = -1; 65 66 private final BluetoothAdapter mBluetoothAdapter; 67 private final IBluetoothManager mBluetoothManager; 68 private final AttributionSource mAttributionSource; 69 70 /* maps callback, to callback wrapper and sync handle */ 71 Map<PeriodicAdvertisingCallback, 72 IPeriodicAdvertisingCallback /* callbackWrapper */> mCallbackWrappers; 73 74 /** 75 * Use {@link BluetoothAdapter#getBluetoothLeScanner()} instead. 76 * 77 * @param bluetoothManager BluetoothManager that conducts overall Bluetooth Management. 78 * @hide 79 */ PeriodicAdvertisingManager(BluetoothAdapter bluetoothAdapter)80 public PeriodicAdvertisingManager(BluetoothAdapter bluetoothAdapter) { 81 mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter); 82 mBluetoothManager = mBluetoothAdapter.getBluetoothManager(); 83 mAttributionSource = mBluetoothAdapter.getAttributionSource(); 84 mCallbackWrappers = new IdentityHashMap<>(); 85 } 86 87 /** 88 * Synchronize with periodic advertising pointed to by the {@code scanResult}. 89 * The {@code scanResult} used must contain a valid advertisingSid. First 90 * call to registerSync will use the {@code skip} and {@code timeout} provided. 91 * Subsequent calls from other apps, trying to sync with same set will reuse 92 * existing sync, thus {@code skip} and {@code timeout} values will not take 93 * effect. The values in effect will be returned in 94 * {@link PeriodicAdvertisingCallback#onSyncEstablished}. 95 * 96 * @param scanResult Scan result containing advertisingSid. 97 * @param skip The number of periodic advertising packets that can be skipped after a successful 98 * receive. Must be between 0 and 499. 99 * @param timeout Synchronization timeout for the periodic advertising. One unit is 10ms. Must 100 * be between 10 (100ms) and 16384 (163.84s). 101 * @param callback Callback used to deliver all operations status. 102 * @throws IllegalArgumentException if {@code scanResult} is null or {@code skip} is invalid or 103 * {@code timeout} is invalid or {@code callback} is null. 104 */ 105 @RequiresLegacyBluetoothAdminPermission 106 @RequiresBluetoothScanPermission 107 @RequiresBluetoothLocationPermission 108 @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN) registerSync(ScanResult scanResult, int skip, int timeout, PeriodicAdvertisingCallback callback)109 public void registerSync(ScanResult scanResult, int skip, int timeout, 110 PeriodicAdvertisingCallback callback) { 111 registerSync(scanResult, skip, timeout, callback, null); 112 } 113 114 /** 115 * Synchronize with periodic advertising pointed to by the {@code scanResult}. 116 * The {@code scanResult} used must contain a valid advertisingSid. First 117 * call to registerSync will use the {@code skip} and {@code timeout} provided. 118 * Subsequent calls from other apps, trying to sync with same set will reuse 119 * existing sync, thus {@code skip} and {@code timeout} values will not take 120 * effect. The values in effect will be returned in 121 * {@link PeriodicAdvertisingCallback#onSyncEstablished}. 122 * 123 * @param scanResult Scan result containing advertisingSid. 124 * @param skip The number of periodic advertising packets that can be skipped after a successful 125 * receive. Must be between 0 and 499. 126 * @param timeout Synchronization timeout for the periodic advertising. One unit is 10ms. Must 127 * be between 10 (100ms) and 16384 (163.84s). 128 * @param callback Callback used to deliver all operations status. 129 * @param handler thread upon which the callbacks will be invoked. 130 * @throws IllegalArgumentException if {@code scanResult} is null or {@code skip} is invalid or 131 * {@code timeout} is invalid or {@code callback} is null. 132 */ 133 @RequiresLegacyBluetoothAdminPermission 134 @RequiresBluetoothScanPermission 135 @RequiresBluetoothLocationPermission 136 @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN) registerSync(ScanResult scanResult, int skip, int timeout, PeriodicAdvertisingCallback callback, Handler handler)137 public void registerSync(ScanResult scanResult, int skip, int timeout, 138 PeriodicAdvertisingCallback callback, Handler handler) { 139 if (callback == null) { 140 throw new IllegalArgumentException("callback can't be null"); 141 } 142 143 if (scanResult == null) { 144 throw new IllegalArgumentException("scanResult can't be null"); 145 } 146 147 if (scanResult.getAdvertisingSid() == ScanResult.SID_NOT_PRESENT) { 148 throw new IllegalArgumentException("scanResult must contain a valid sid"); 149 } 150 151 if (skip < SKIP_MIN || skip > SKIP_MAX) { 152 throw new IllegalArgumentException( 153 "timeout must be between " + TIMEOUT_MIN + " and " + TIMEOUT_MAX); 154 } 155 156 if (timeout < TIMEOUT_MIN || timeout > TIMEOUT_MAX) { 157 throw new IllegalArgumentException( 158 "timeout must be between " + TIMEOUT_MIN + " and " + TIMEOUT_MAX); 159 } 160 161 IBluetoothGatt gatt; 162 try { 163 gatt = mBluetoothManager.getBluetoothGatt(); 164 } catch (RemoteException e) { 165 Log.e(TAG, "Failed to get Bluetooth gatt - ", e); 166 callback.onSyncEstablished(0, scanResult.getDevice(), scanResult.getAdvertisingSid(), 167 skip, timeout, 168 PeriodicAdvertisingCallback.SYNC_NO_RESOURCES); 169 return; 170 } 171 172 if (handler == null) { 173 handler = new Handler(Looper.getMainLooper()); 174 } 175 176 IPeriodicAdvertisingCallback wrapped = wrap(callback, handler); 177 mCallbackWrappers.put(callback, wrapped); 178 179 try { 180 final SynchronousResultReceiver recv = SynchronousResultReceiver.get(); 181 gatt.registerSync(scanResult, skip, timeout, wrapped, mAttributionSource, recv); 182 recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null); 183 } catch (TimeoutException | RemoteException e) { 184 Log.e(TAG, "Failed to register sync - ", e); 185 return; 186 } 187 } 188 189 /** 190 * Cancel pending attempt to create sync, or terminate existing sync. 191 * 192 * @param callback Callback used to deliver all operations status. 193 * @throws IllegalArgumentException if {@code callback} is null, or not a properly registered 194 * callback. 195 */ 196 @RequiresLegacyBluetoothAdminPermission 197 @RequiresBluetoothScanPermission 198 @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN) unregisterSync(PeriodicAdvertisingCallback callback)199 public void unregisterSync(PeriodicAdvertisingCallback callback) { 200 if (callback == null) { 201 throw new IllegalArgumentException("callback can't be null"); 202 } 203 204 IBluetoothGatt gatt; 205 try { 206 gatt = mBluetoothManager.getBluetoothGatt(); 207 } catch (RemoteException e) { 208 Log.e(TAG, "Failed to get Bluetooth gatt - ", e); 209 return; 210 } 211 212 IPeriodicAdvertisingCallback wrapper = mCallbackWrappers.remove(callback); 213 if (wrapper == null) { 214 throw new IllegalArgumentException("callback was not properly registered"); 215 } 216 217 try { 218 final SynchronousResultReceiver recv = SynchronousResultReceiver.get(); 219 gatt.unregisterSync(wrapper, mAttributionSource, recv); 220 recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null); 221 } catch (TimeoutException | RemoteException e) { 222 Log.e(TAG, "Failed to cancel sync creation - ", e); 223 return; 224 } 225 } 226 227 /** 228 * Transfer periodic sync 229 * 230 * @hide 231 */ transferSync(BluetoothDevice bda, int serviceData, int syncHandle)232 public void transferSync(BluetoothDevice bda, int serviceData, int syncHandle) { 233 IBluetoothGatt gatt; 234 try { 235 gatt = mBluetoothManager.getBluetoothGatt(); 236 } catch (RemoteException e) { 237 Log.e(TAG, "Failed to get Bluetooth gatt - ", e); 238 PeriodicAdvertisingCallback callback = null; 239 for (PeriodicAdvertisingCallback cb : mCallbackWrappers.keySet()) { 240 callback = cb; 241 } 242 if (callback != null) { 243 callback.onSyncTransferred(bda, 244 PeriodicAdvertisingCallback.SYNC_NO_RESOURCES); 245 } 246 return; 247 } 248 try { 249 final SynchronousResultReceiver recv = SynchronousResultReceiver.get(); 250 gatt.transferSync(bda, serviceData , syncHandle, mAttributionSource, recv); 251 recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null); 252 } catch (TimeoutException | RemoteException e) { 253 Log.e(TAG, "Failed to register sync - ", e); 254 return; 255 } 256 } 257 258 /** 259 * Transfer set info 260 * 261 * @hide 262 */ transferSetInfo(BluetoothDevice bda, int serviceData, int advHandle, PeriodicAdvertisingCallback callback)263 public void transferSetInfo(BluetoothDevice bda, int serviceData, 264 int advHandle, PeriodicAdvertisingCallback callback) { 265 transferSetInfo(bda, serviceData, advHandle, callback, null); 266 } 267 268 /** 269 * Transfer set info 270 * 271 * @hide 272 */ transferSetInfo(BluetoothDevice bda, int serviceData, int advHandle, PeriodicAdvertisingCallback callback, @Nullable Handler handler)273 public void transferSetInfo(BluetoothDevice bda, int serviceData, 274 int advHandle, PeriodicAdvertisingCallback callback, 275 @Nullable Handler handler) { 276 if (callback == null) { 277 throw new IllegalArgumentException("callback can't be null"); 278 } 279 IBluetoothGatt gatt; 280 try { 281 gatt = mBluetoothManager.getBluetoothGatt(); 282 } catch (RemoteException e) { 283 Log.e(TAG, "Failed to get Bluetooth gatt - ", e); 284 return; 285 } 286 if (handler == null) { 287 handler = new Handler(Looper.getMainLooper()); 288 } 289 IPeriodicAdvertisingCallback wrapper = wrap(callback, handler); 290 if (wrapper == null) { 291 throw new IllegalArgumentException("callback was not properly registered"); 292 } 293 try { 294 final SynchronousResultReceiver recv = SynchronousResultReceiver.get(); 295 gatt.transferSetInfo(bda, serviceData , advHandle, wrapper, mAttributionSource, recv); 296 recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null); 297 } catch (RemoteException | TimeoutException e) { 298 Log.e(TAG, "Failed to register sync - ", e); 299 return; 300 } 301 302 } 303 304 @SuppressLint("AndroidFrameworkBluetoothPermission") wrap(PeriodicAdvertisingCallback callback, Handler handler)305 private IPeriodicAdvertisingCallback wrap(PeriodicAdvertisingCallback callback, 306 Handler handler) { 307 return new IPeriodicAdvertisingCallback.Stub() { 308 public void onSyncEstablished(int syncHandle, BluetoothDevice device, 309 int advertisingSid, int skip, int timeout, int status) { 310 Attributable.setAttributionSource(device, mAttributionSource); 311 handler.post(new Runnable() { 312 @Override 313 public void run() { 314 callback.onSyncEstablished(syncHandle, device, advertisingSid, skip, 315 timeout, 316 status); 317 318 if (status != PeriodicAdvertisingCallback.SYNC_SUCCESS) { 319 // App can still unregister the sync until notified it failed. Remove 320 // callback 321 // after app was notifed. 322 mCallbackWrappers.remove(callback); 323 } 324 } 325 }); 326 } 327 328 public void onPeriodicAdvertisingReport(PeriodicAdvertisingReport report) { 329 handler.post(new Runnable() { 330 @Override 331 public void run() { 332 callback.onPeriodicAdvertisingReport(report); 333 } 334 }); 335 } 336 337 public void onSyncLost(int syncHandle) { 338 handler.post(new Runnable() { 339 @Override 340 public void run() { 341 callback.onSyncLost(syncHandle); 342 // App can still unregister the sync until notified it's lost. 343 // Remove callback after app was notifed. 344 mCallbackWrappers.remove(callback); 345 } 346 }); 347 } 348 349 public void onSyncTransferred(BluetoothDevice device, int status) { 350 handler.post(new Runnable() { 351 @Override 352 public void run() { 353 callback.onSyncTransferred(device, status); 354 } 355 }); 356 } 357 358 public void onBigInfoAdvertisingReport(int syncHandle, boolean encrypted) { 359 handler.post(new Runnable() { 360 @Override 361 public void run() { 362 callback.onBigInfoAdvertisingReport(syncHandle, encrypted); 363 } 364 }); 365 } 366 }; 367 } 368 } 369