Home | History | Annotate | Download | only in le
      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 
     17 package android.bluetooth.le;
     18 
     19 import android.bluetooth.BluetoothAdapter;
     20 import android.bluetooth.BluetoothGatt;
     21 import android.bluetooth.BluetoothGattCallbackWrapper;
     22 import android.bluetooth.BluetoothUuid;
     23 import android.bluetooth.IBluetoothGatt;
     24 import android.bluetooth.IBluetoothManager;
     25 import android.os.Handler;
     26 import android.os.Looper;
     27 import android.os.ParcelUuid;
     28 import android.os.RemoteException;
     29 import android.util.Log;
     30 
     31 import java.util.HashMap;
     32 import java.util.Map;
     33 import 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  */
     48 public 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: scan stopped
    241         // >0: registered and scan 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                     // Post internal error if registration failed.
    272                     postStartFailure(mAdvertiseCallback,
    273                             AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR);
    274                 } else {
    275                     // Unregister application if it's already registered but advertise failed.
    276                     try {
    277                         mBluetoothGatt.unregisterClient(mClientIf);
    278                         mClientIf = -1;
    279                     } catch (RemoteException e) {
    280                         Log.e(TAG, "remote exception when unregistering", e);
    281                     }
    282                 }
    283             }
    284         }
    285 
    286         public void stopAdvertising() {
    287             synchronized (this) {
    288                 try {
    289                     mBluetoothGatt.stopMultiAdvertising(mClientIf);
    290                     wait(LE_CALLBACK_TIMEOUT_MILLIS);
    291                 } catch (InterruptedException | RemoteException e) {
    292                     Log.e(TAG, "Failed to stop advertising", e);
    293                 }
    294                 // Advertise callback should have been removed from LeAdvertisers when
    295                 // onMultiAdvertiseCallback was called. In case onMultiAdvertiseCallback is never
    296                 // invoked and wait timeout expires, remove callback here.
    297                 if (mLeAdvertisers.containsKey(mAdvertiseCallback)) {
    298                     mLeAdvertisers.remove(mAdvertiseCallback);
    299                 }
    300             }
    301         }
    302 
    303         /**
    304          * Application interface registered - app is ready to go
    305          */
    306         @Override
    307         public void onClientRegistered(int status, int clientIf) {
    308             Log.d(TAG, "onClientRegistered() - status=" + status + " clientIf=" + clientIf);
    309             synchronized (this) {
    310                 if (status == BluetoothGatt.GATT_SUCCESS) {
    311                     mClientIf = clientIf;
    312                     try {
    313                         mBluetoothGatt.startMultiAdvertising(mClientIf, mAdvertisement,
    314                                 mScanResponse, mSettings);
    315                         return;
    316                     } catch (RemoteException e) {
    317                         Log.e(TAG, "failed to start advertising", e);
    318                     }
    319                 }
    320                 // Registration failed.
    321                 mClientIf = -1;
    322                 notifyAll();
    323             }
    324         }
    325 
    326         @Override
    327         public void onMultiAdvertiseCallback(int status, boolean isStart,
    328                 AdvertiseSettings settings) {
    329             synchronized (this) {
    330                 if (isStart) {
    331                     if (status == AdvertiseCallback.ADVERTISE_SUCCESS) {
    332                         // Start success
    333                         mIsAdvertising = true;
    334                         postStartSuccess(mAdvertiseCallback, settings);
    335                     } else {
    336                         // Start failure.
    337                         postStartFailure(mAdvertiseCallback, status);
    338                     }
    339                 } else {
    340                     // unregister client for stop.
    341                     try {
    342                         mBluetoothGatt.unregisterClient(mClientIf);
    343                         mClientIf = -1;
    344                         mIsAdvertising = false;
    345                         mLeAdvertisers.remove(mAdvertiseCallback);
    346                     } catch (RemoteException e) {
    347                         Log.e(TAG, "remote exception when unregistering", e);
    348                     }
    349                 }
    350                 notifyAll();
    351             }
    352 
    353         }
    354     }
    355 
    356     private void postStartFailure(final AdvertiseCallback callback, final int error) {
    357         mHandler.post(new Runnable() {
    358             @Override
    359             public void run() {
    360                 callback.onStartFailure(error);
    361             }
    362         });
    363     }
    364 
    365     private void postStartSuccess(final AdvertiseCallback callback,
    366             final AdvertiseSettings settings) {
    367         mHandler.post(new Runnable() {
    368 
    369             @Override
    370             public void run() {
    371                 callback.onStartSuccess(settings);
    372             }
    373         });
    374     }
    375 }
    376