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.bluetooth.BluetoothAdapter; 20 import android.bluetooth.BluetoothDevice; 21 import android.bluetooth.IBluetoothGatt; 22 import android.bluetooth.IBluetoothManager; 23 import android.os.Handler; 24 import android.os.Looper; 25 import android.os.RemoteException; 26 import android.util.Log; 27 28 import java.util.IdentityHashMap; 29 import java.util.Map; 30 31 /** 32 * This class provides methods to perform periodic advertising related 33 * operations. An application can register for periodic advertisements using 34 * {@link PeriodicAdvertisingManager#registerSync}. 35 * <p> 36 * Use {@link BluetoothAdapter#getPeriodicAdvertisingManager()} to get an 37 * instance of {@link PeriodicAdvertisingManager}. 38 * <p> 39 * <b>Note:</b> Most of the methods here require 40 * {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission. 41 * 42 * @hide 43 */ 44 public final class PeriodicAdvertisingManager { 45 46 private static final String TAG = "PeriodicAdvertisingManager"; 47 48 private static final int SKIP_MIN = 0; 49 private static final int SKIP_MAX = 499; 50 private static final int TIMEOUT_MIN = 10; 51 private static final int TIMEOUT_MAX = 16384; 52 53 private static final int SYNC_STARTING = -1; 54 55 private final IBluetoothManager mBluetoothManager; 56 private BluetoothAdapter mBluetoothAdapter; 57 58 /* maps callback, to callback wrapper and sync handle */ 59 Map<PeriodicAdvertisingCallback, 60 IPeriodicAdvertisingCallback /* callbackWrapper */> mCallbackWrappers; 61 62 /** 63 * Use {@link BluetoothAdapter#getBluetoothLeScanner()} instead. 64 * 65 * @param bluetoothManager BluetoothManager that conducts overall Bluetooth Management. 66 * @hide 67 */ 68 public PeriodicAdvertisingManager(IBluetoothManager bluetoothManager) { 69 mBluetoothManager = bluetoothManager; 70 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 71 mCallbackWrappers = new IdentityHashMap<>(); 72 } 73 74 /** 75 * Synchronize with periodic advertising pointed to by the {@code scanResult}. 76 * The {@code scanResult} used must contain a valid advertisingSid. First 77 * call to registerSync will use the {@code skip} and {@code timeout} provided. 78 * Subsequent calls from other apps, trying to sync with same set will reuse 79 * existing sync, thus {@code skip} and {@code timeout} values will not take 80 * effect. The values in effect will be returned in 81 * {@link PeriodicAdvertisingCallback#onSyncEstablished}. 82 * 83 * @param scanResult Scan result containing advertisingSid. 84 * @param skip The number of periodic advertising packets that can be skipped after a successful 85 * receive. Must be between 0 and 499. 86 * @param timeout Synchronization timeout for the periodic advertising. One unit is 10ms. Must 87 * be between 10 (100ms) and 16384 (163.84s). 88 * @param callback Callback used to deliver all operations status. 89 * @throws IllegalArgumentException if {@code scanResult} is null or {@code skip} is invalid or 90 * {@code timeout} is invalid or {@code callback} is null. 91 */ 92 public void registerSync(ScanResult scanResult, int skip, int timeout, 93 PeriodicAdvertisingCallback callback) { 94 registerSync(scanResult, skip, timeout, callback, null); 95 } 96 97 /** 98 * Synchronize with periodic advertising pointed to by the {@code scanResult}. 99 * The {@code scanResult} used must contain a valid advertisingSid. First 100 * call to registerSync will use the {@code skip} and {@code timeout} provided. 101 * Subsequent calls from other apps, trying to sync with same set will reuse 102 * existing sync, thus {@code skip} and {@code timeout} values will not take 103 * effect. The values in effect will be returned in 104 * {@link PeriodicAdvertisingCallback#onSyncEstablished}. 105 * 106 * @param scanResult Scan result containing advertisingSid. 107 * @param skip The number of periodic advertising packets that can be skipped after a successful 108 * receive. Must be between 0 and 499. 109 * @param timeout Synchronization timeout for the periodic advertising. One unit is 10ms. Must 110 * be between 10 (100ms) and 16384 (163.84s). 111 * @param callback Callback used to deliver all operations status. 112 * @param handler thread upon which the callbacks will be invoked. 113 * @throws IllegalArgumentException if {@code scanResult} is null or {@code skip} is invalid or 114 * {@code timeout} is invalid or {@code callback} is null. 115 */ 116 public void registerSync(ScanResult scanResult, int skip, int timeout, 117 PeriodicAdvertisingCallback callback, Handler handler) { 118 if (callback == null) { 119 throw new IllegalArgumentException("callback can't be null"); 120 } 121 122 if (scanResult == null) { 123 throw new IllegalArgumentException("scanResult can't be null"); 124 } 125 126 if (scanResult.getAdvertisingSid() == ScanResult.SID_NOT_PRESENT) { 127 throw new IllegalArgumentException("scanResult must contain a valid sid"); 128 } 129 130 if (skip < SKIP_MIN || skip > SKIP_MAX) { 131 throw new IllegalArgumentException( 132 "timeout must be between " + TIMEOUT_MIN + " and " + TIMEOUT_MAX); 133 } 134 135 if (timeout < TIMEOUT_MIN || timeout > TIMEOUT_MAX) { 136 throw new IllegalArgumentException( 137 "timeout must be between " + TIMEOUT_MIN + " and " + TIMEOUT_MAX); 138 } 139 140 IBluetoothGatt gatt; 141 try { 142 gatt = mBluetoothManager.getBluetoothGatt(); 143 } catch (RemoteException e) { 144 Log.e(TAG, "Failed to get Bluetooth gatt - ", e); 145 callback.onSyncEstablished(0, scanResult.getDevice(), scanResult.getAdvertisingSid(), 146 skip, timeout, 147 PeriodicAdvertisingCallback.SYNC_NO_RESOURCES); 148 return; 149 } 150 151 if (handler == null) { 152 handler = new Handler(Looper.getMainLooper()); 153 } 154 155 IPeriodicAdvertisingCallback wrapped = wrap(callback, handler); 156 mCallbackWrappers.put(callback, wrapped); 157 158 try { 159 gatt.registerSync(scanResult, skip, timeout, wrapped); 160 } catch (RemoteException e) { 161 Log.e(TAG, "Failed to register sync - ", e); 162 return; 163 } 164 } 165 166 /** 167 * Cancel pending attempt to create sync, or terminate existing sync. 168 * 169 * @param callback Callback used to deliver all operations status. 170 * @throws IllegalArgumentException if {@code callback} is null, or not a properly registered 171 * callback. 172 */ 173 public void unregisterSync(PeriodicAdvertisingCallback callback) { 174 if (callback == null) { 175 throw new IllegalArgumentException("callback can't be null"); 176 } 177 178 IBluetoothGatt gatt; 179 try { 180 gatt = mBluetoothManager.getBluetoothGatt(); 181 } catch (RemoteException e) { 182 Log.e(TAG, "Failed to get Bluetooth gatt - ", e); 183 return; 184 } 185 186 IPeriodicAdvertisingCallback wrapper = mCallbackWrappers.remove(callback); 187 if (wrapper == null) { 188 throw new IllegalArgumentException("callback was not properly registered"); 189 } 190 191 try { 192 gatt.unregisterSync(wrapper); 193 } catch (RemoteException e) { 194 Log.e(TAG, "Failed to cancel sync creation - ", e); 195 return; 196 } 197 } 198 199 private IPeriodicAdvertisingCallback wrap(PeriodicAdvertisingCallback callback, 200 Handler handler) { 201 return new IPeriodicAdvertisingCallback.Stub() { 202 public void onSyncEstablished(int syncHandle, BluetoothDevice device, 203 int advertisingSid, int skip, int timeout, int status) { 204 205 handler.post(new Runnable() { 206 @Override 207 public void run() { 208 callback.onSyncEstablished(syncHandle, device, advertisingSid, skip, 209 timeout, 210 status); 211 212 if (status != PeriodicAdvertisingCallback.SYNC_SUCCESS) { 213 // App can still unregister the sync until notified it failed. Remove 214 // callback 215 // after app was notifed. 216 mCallbackWrappers.remove(callback); 217 } 218 } 219 }); 220 } 221 222 public void onPeriodicAdvertisingReport(PeriodicAdvertisingReport report) { 223 handler.post(new Runnable() { 224 @Override 225 public void run() { 226 callback.onPeriodicAdvertisingReport(report); 227 } 228 }); 229 } 230 231 public void onSyncLost(int syncHandle) { 232 handler.post(new Runnable() { 233 @Override 234 public void run() { 235 callback.onSyncLost(syncHandle); 236 // App can still unregister the sync until notified it's lost. 237 // Remove callback after app was notifed. 238 mCallbackWrappers.remove(callback); 239 } 240 }); 241 } 242 }; 243 } 244 } 245