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