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