1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.bluetooth.le;
18
19import android.bluetooth.BluetoothAdapter;
20import android.bluetooth.BluetoothGatt;
21import android.bluetooth.BluetoothGattCallbackWrapper;
22import android.bluetooth.BluetoothUuid;
23import android.bluetooth.IBluetoothGatt;
24import android.bluetooth.IBluetoothManager;
25import android.os.Handler;
26import android.os.Looper;
27import android.os.ParcelUuid;
28import android.os.RemoteException;
29import android.util.Log;
30
31import java.util.HashMap;
32import java.util.Map;
33import java.util.UUID;
34
35/**
36 * This class provides a way to perform Bluetooth LE advertise operations, such as starting and
37 * stopping advertising. An advertiser can broadcast up to 31 bytes of advertisement data
38 * represented by {@link AdvertiseData}.
39 * <p>
40 * To get an instance of {@link BluetoothLeAdvertiser}, call the
41 * {@link BluetoothAdapter#getBluetoothLeAdvertiser()} method.
42 * <p>
43 * <b>Note:</b> Most of the methods here require {@link android.Manifest.permission#BLUETOOTH_ADMIN}
44 * permission.
45 *
46 * @see AdvertiseData
47 */
48public final class BluetoothLeAdvertiser {
49
50    private static final String TAG = "BluetoothLeAdvertiser";
51
52    private static final int MAX_ADVERTISING_DATA_BYTES = 31;
53    // Each fields need one byte for field length and another byte for field type.
54    private static final int OVERHEAD_BYTES_PER_FIELD = 2;
55    // Flags field will be set by system.
56    private static final int FLAGS_FIELD_BYTES = 3;
57    private static final int MANUFACTURER_SPECIFIC_DATA_LENGTH = 2;
58    private static final int SERVICE_DATA_UUID_LENGTH = 2;
59
60    private final IBluetoothManager mBluetoothManager;
61    private final Handler mHandler;
62    private BluetoothAdapter mBluetoothAdapter;
63    private final Map<AdvertiseCallback, AdvertiseCallbackWrapper>
64            mLeAdvertisers = new HashMap<AdvertiseCallback, AdvertiseCallbackWrapper>();
65
66    /**
67     * Use BluetoothAdapter.getLeAdvertiser() instead.
68     *
69     * @param bluetoothManager BluetoothManager that conducts overall Bluetooth Management
70     * @hide
71     */
72    public BluetoothLeAdvertiser(IBluetoothManager bluetoothManager) {
73        mBluetoothManager = bluetoothManager;
74        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
75        mHandler = new Handler(Looper.getMainLooper());
76    }
77
78    /**
79     * Start Bluetooth LE Advertising. On success, the {@code advertiseData} will be broadcasted.
80     * Returns immediately, the operation status is delivered through {@code callback}.
81     * <p>
82     * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
83     *
84     * @param settings Settings for Bluetooth LE advertising.
85     * @param advertiseData Advertisement data to be broadcasted.
86     * @param callback Callback for advertising status.
87     */
88    public void startAdvertising(AdvertiseSettings settings,
89            AdvertiseData advertiseData, final AdvertiseCallback callback) {
90        startAdvertising(settings, advertiseData, null, callback);
91    }
92
93    /**
94     * Start Bluetooth LE Advertising. The {@code advertiseData} will be broadcasted if the
95     * operation succeeds. The {@code scanResponse} is returned when a scanning device sends an
96     * active scan request. This method returns immediately, the operation status is delivered
97     * through {@code callback}.
98     * <p>
99     * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
100     *
101     * @param settings Settings for Bluetooth LE advertising.
102     * @param advertiseData Advertisement data to be advertised in advertisement packet.
103     * @param scanResponse Scan response associated with the advertisement data.
104     * @param callback Callback for advertising status.
105     */
106    public void startAdvertising(AdvertiseSettings settings,
107            AdvertiseData advertiseData, AdvertiseData scanResponse,
108            final AdvertiseCallback callback) {
109        synchronized (mLeAdvertisers) {
110            BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
111            if (callback == null) {
112                throw new IllegalArgumentException("callback cannot be null");
113            }
114            if (!mBluetoothAdapter.isMultipleAdvertisementSupported() &&
115                    !mBluetoothAdapter.isPeripheralModeSupported()) {
116                postStartFailure(callback,
117                        AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED);
118                return;
119            }
120            boolean isConnectable = settings.isConnectable();
121            if (totalBytes(advertiseData, isConnectable) > MAX_ADVERTISING_DATA_BYTES ||
122                    totalBytes(scanResponse, false) > MAX_ADVERTISING_DATA_BYTES) {
123                postStartFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE);
124                return;
125            }
126            if (mLeAdvertisers.containsKey(callback)) {
127                postStartFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED);
128                return;
129            }
130
131            IBluetoothGatt gatt;
132            try {
133                gatt = mBluetoothManager.getBluetoothGatt();
134            } catch (RemoteException e) {
135                Log.e(TAG, "Failed to get Bluetooth gatt - ", e);
136                postStartFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR);
137                return;
138            }
139            AdvertiseCallbackWrapper wrapper = new AdvertiseCallbackWrapper(callback, advertiseData,
140                    scanResponse, settings, gatt);
141            wrapper.startRegisteration();
142        }
143    }
144
145    /**
146     * Stop Bluetooth LE advertising. The {@code callback} must be the same one use in
147     * {@link BluetoothLeAdvertiser#startAdvertising}.
148     * <p>
149     * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
150     *
151     * @param callback {@link AdvertiseCallback} identifies the advertising instance to stop.
152     */
153    public void stopAdvertising(final AdvertiseCallback callback) {
154        synchronized (mLeAdvertisers) {
155            if (callback == null) {
156                throw new IllegalArgumentException("callback cannot be null");
157            }
158            AdvertiseCallbackWrapper wrapper = mLeAdvertisers.get(callback);
159            if (wrapper == null) return;
160            wrapper.stopAdvertising();
161        }
162    }
163
164    /**
165     * Cleans up advertise clients. Should be called when bluetooth is down.
166     *
167     * @hide
168     */
169    public void cleanup() {
170        mLeAdvertisers.clear();
171    }
172
173    // Compute the size of advertisement data or scan resp
174    private int totalBytes(AdvertiseData data, boolean isFlagsIncluded) {
175        if (data == null) return 0;
176        // Flags field is omitted if the advertising is not connectable.
177        int size = (isFlagsIncluded) ? FLAGS_FIELD_BYTES : 0;
178        if (data.getServiceUuids() != null) {
179            int num16BitUuids = 0;
180            int num32BitUuids = 0;
181            int num128BitUuids = 0;
182            for (ParcelUuid uuid : data.getServiceUuids()) {
183                if (BluetoothUuid.is16BitUuid(uuid)) {
184                    ++num16BitUuids;
185                } else if (BluetoothUuid.is32BitUuid(uuid)) {
186                    ++num32BitUuids;
187                } else {
188                    ++num128BitUuids;
189                }
190            }
191            // 16 bit service uuids are grouped into one field when doing advertising.
192            if (num16BitUuids != 0) {
193                size += OVERHEAD_BYTES_PER_FIELD +
194                        num16BitUuids * BluetoothUuid.UUID_BYTES_16_BIT;
195            }
196            // 32 bit service uuids are grouped into one field when doing advertising.
197            if (num32BitUuids != 0) {
198                size += OVERHEAD_BYTES_PER_FIELD +
199                        num32BitUuids * BluetoothUuid.UUID_BYTES_32_BIT;
200            }
201            // 128 bit service uuids are grouped into one field when doing advertising.
202            if (num128BitUuids != 0) {
203                size += OVERHEAD_BYTES_PER_FIELD +
204                        num128BitUuids * BluetoothUuid.UUID_BYTES_128_BIT;
205            }
206        }
207        for (ParcelUuid uuid : data.getServiceData().keySet()) {
208            size += OVERHEAD_BYTES_PER_FIELD + SERVICE_DATA_UUID_LENGTH
209                    + byteLength(data.getServiceData().get(uuid));
210        }
211        for (int i = 0; i < data.getManufacturerSpecificData().size(); ++i) {
212            size += OVERHEAD_BYTES_PER_FIELD + MANUFACTURER_SPECIFIC_DATA_LENGTH +
213                    byteLength(data.getManufacturerSpecificData().valueAt(i));
214        }
215        if (data.getIncludeTxPowerLevel()) {
216            size += OVERHEAD_BYTES_PER_FIELD + 1; // tx power level value is one byte.
217        }
218        if (data.getIncludeDeviceName() && mBluetoothAdapter.getName() != null) {
219            size += OVERHEAD_BYTES_PER_FIELD + mBluetoothAdapter.getName().length();
220        }
221        return size;
222    }
223
224    private int byteLength(byte[] array) {
225        return array == null ? 0 : array.length;
226    }
227
228    /**
229     * Bluetooth GATT interface callbacks for advertising.
230     */
231    private class AdvertiseCallbackWrapper extends BluetoothGattCallbackWrapper {
232        private static final int LE_CALLBACK_TIMEOUT_MILLIS = 2000;
233        private final AdvertiseCallback mAdvertiseCallback;
234        private final AdvertiseData mAdvertisement;
235        private final AdvertiseData mScanResponse;
236        private final AdvertiseSettings mSettings;
237        private final IBluetoothGatt mBluetoothGatt;
238
239        // mClientIf 0: not registered
240        // -1: advertise stopped or registration timeout
241        // >0: registered and advertising started
242        private int mClientIf;
243        private boolean mIsAdvertising = false;
244
245        public AdvertiseCallbackWrapper(AdvertiseCallback advertiseCallback,
246                AdvertiseData advertiseData, AdvertiseData scanResponse,
247                AdvertiseSettings settings,
248                IBluetoothGatt bluetoothGatt) {
249            mAdvertiseCallback = advertiseCallback;
250            mAdvertisement = advertiseData;
251            mScanResponse = scanResponse;
252            mSettings = settings;
253            mBluetoothGatt = bluetoothGatt;
254            mClientIf = 0;
255        }
256
257        public void startRegisteration() {
258            synchronized (this) {
259                if (mClientIf == -1) return;
260
261                try {
262                    UUID uuid = UUID.randomUUID();
263                    mBluetoothGatt.registerClient(new ParcelUuid(uuid), this);
264                    wait(LE_CALLBACK_TIMEOUT_MILLIS);
265                } catch (InterruptedException | RemoteException e) {
266                    Log.e(TAG, "Failed to start registeration", e);
267                }
268                if (mClientIf > 0 && mIsAdvertising) {
269                    mLeAdvertisers.put(mAdvertiseCallback, this);
270                } else if (mClientIf <= 0) {
271
272                    // Registration timeout, reset mClientIf to -1 so no subsequent operations can
273                    // proceed.
274                    if (mClientIf == 0) mClientIf = -1;
275                    // Post internal error if registration failed.
276                    postStartFailure(mAdvertiseCallback,
277                            AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR);
278                } else {
279                    // Unregister application if it's already registered but advertise failed.
280                    try {
281                        mBluetoothGatt.unregisterClient(mClientIf);
282                        mClientIf = -1;
283                    } catch (RemoteException e) {
284                        Log.e(TAG, "remote exception when unregistering", e);
285                    }
286                }
287            }
288        }
289
290        public void stopAdvertising() {
291            synchronized (this) {
292                try {
293                    mBluetoothGatt.stopMultiAdvertising(mClientIf);
294                    wait(LE_CALLBACK_TIMEOUT_MILLIS);
295                } catch (InterruptedException | RemoteException e) {
296                    Log.e(TAG, "Failed to stop advertising", e);
297                }
298                // Advertise callback should have been removed from LeAdvertisers when
299                // onMultiAdvertiseCallback was called. In case onMultiAdvertiseCallback is never
300                // invoked and wait timeout expires, remove callback here.
301                if (mLeAdvertisers.containsKey(mAdvertiseCallback)) {
302                    mLeAdvertisers.remove(mAdvertiseCallback);
303                }
304            }
305        }
306
307        /**
308         * Application interface registered - app is ready to go
309         */
310        @Override
311        public void onClientRegistered(int status, int clientIf) {
312            Log.d(TAG, "onClientRegistered() - status=" + status + " clientIf=" + clientIf);
313            synchronized (this) {
314                if (status == BluetoothGatt.GATT_SUCCESS) {
315                    try {
316                        if (mClientIf == -1) {
317                            // Registration succeeds after timeout, unregister client.
318                            mBluetoothGatt.unregisterClient(clientIf);
319                        } else {
320                            mClientIf = clientIf;
321                            mBluetoothGatt.startMultiAdvertising(mClientIf, mAdvertisement,
322                                    mScanResponse, mSettings);
323                        }
324                        return;
325                    } catch (RemoteException e) {
326                        Log.e(TAG, "failed to start advertising", e);
327                    }
328                }
329                // Registration failed.
330                mClientIf = -1;
331                notifyAll();
332            }
333        }
334
335        @Override
336        public void onMultiAdvertiseCallback(int status, boolean isStart,
337                AdvertiseSettings settings) {
338            synchronized (this) {
339                if (isStart) {
340                    if (status == AdvertiseCallback.ADVERTISE_SUCCESS) {
341                        // Start success
342                        mIsAdvertising = true;
343                        postStartSuccess(mAdvertiseCallback, settings);
344                    } else {
345                        // Start failure.
346                        postStartFailure(mAdvertiseCallback, status);
347                    }
348                } else {
349                    // unregister client for stop.
350                    try {
351                        mBluetoothGatt.unregisterClient(mClientIf);
352                        mClientIf = -1;
353                        mIsAdvertising = false;
354                        mLeAdvertisers.remove(mAdvertiseCallback);
355                    } catch (RemoteException e) {
356                        Log.e(TAG, "remote exception when unregistering", e);
357                    }
358                }
359                notifyAll();
360            }
361
362        }
363    }
364
365    private void postStartFailure(final AdvertiseCallback callback, final int error) {
366        mHandler.post(new Runnable() {
367            @Override
368            public void run() {
369                callback.onStartFailure(error);
370            }
371        });
372    }
373
374    private void postStartSuccess(final AdvertiseCallback callback,
375            final AdvertiseSettings settings) {
376        mHandler.post(new Runnable() {
377
378            @Override
379            public void run() {
380                callback.onStartSuccess(settings);
381            }
382        });
383    }
384}
385