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