• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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