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             BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
    156             if (callback == null) {
    157                 throw new IllegalArgumentException("callback cannot be null");
    158             }
    159             AdvertiseCallbackWrapper wrapper = mLeAdvertisers.get(callback);
    160             if (wrapper == null) return;
    161             wrapper.stopAdvertising();
    162         }
    163     }
    164 
    165     /**
    166      * Cleans up advertise clients. Should be called when bluetooth is down.
    167      *
    168      * @hide
    169      */
    170     public void cleanup() {
    171         mLeAdvertisers.clear();
    172     }
    173 
    174     // Compute the size of advertisement data or scan resp
    175     private int totalBytes(AdvertiseData data, boolean isFlagsIncluded) {
    176         if (data == null) return 0;
    177         // Flags field is omitted if the advertising is not connectable.
    178         int size = (isFlagsIncluded) ? FLAGS_FIELD_BYTES : 0;
    179         if (data.getServiceUuids() != null) {
    180             int num16BitUuids = 0;
    181             int num32BitUuids = 0;
    182             int num128BitUuids = 0;
    183             for (ParcelUuid uuid : data.getServiceUuids()) {
    184                 if (BluetoothUuid.is16BitUuid(uuid)) {
    185                     ++num16BitUuids;
    186                 } else if (BluetoothUuid.is32BitUuid(uuid)) {
    187                     ++num32BitUuids;
    188                 } else {
    189                     ++num128BitUuids;
    190                 }
    191             }
    192             // 16 bit service uuids are grouped into one field when doing advertising.
    193             if (num16BitUuids != 0) {
    194                 size += OVERHEAD_BYTES_PER_FIELD +
    195                         num16BitUuids * BluetoothUuid.UUID_BYTES_16_BIT;
    196             }
    197             // 32 bit service uuids are grouped into one field when doing advertising.
    198             if (num32BitUuids != 0) {
    199                 size += OVERHEAD_BYTES_PER_FIELD +
    200                         num32BitUuids * BluetoothUuid.UUID_BYTES_32_BIT;
    201             }
    202             // 128 bit service uuids are grouped into one field when doing advertising.
    203             if (num128BitUuids != 0) {
    204                 size += OVERHEAD_BYTES_PER_FIELD +
    205                         num128BitUuids * BluetoothUuid.UUID_BYTES_128_BIT;
    206             }
    207         }
    208         for (ParcelUuid uuid : data.getServiceData().keySet()) {
    209             size += OVERHEAD_BYTES_PER_FIELD + SERVICE_DATA_UUID_LENGTH
    210                     + byteLength(data.getServiceData().get(uuid));
    211         }
    212         for (int i = 0; i < data.getManufacturerSpecificData().size(); ++i) {
    213             size += OVERHEAD_BYTES_PER_FIELD + MANUFACTURER_SPECIFIC_DATA_LENGTH +
    214                     byteLength(data.getManufacturerSpecificData().valueAt(i));
    215         }
    216         if (data.getIncludeTxPowerLevel()) {
    217             size += OVERHEAD_BYTES_PER_FIELD + 1; // tx power level value is one byte.
    218         }
    219         if (data.getIncludeDeviceName() && mBluetoothAdapter.getName() != null) {
    220             size += OVERHEAD_BYTES_PER_FIELD + mBluetoothAdapter.getName().length();
    221         }
    222         return size;
    223     }
    224 
    225     private int byteLength(byte[] array) {
    226         return array == null ? 0 : array.length;
    227     }
    228 
    229     /**
    230      * Bluetooth GATT interface callbacks for advertising.
    231      */
    232     private class AdvertiseCallbackWrapper extends BluetoothGattCallbackWrapper {
    233         private static final int LE_CALLBACK_TIMEOUT_MILLIS = 2000;
    234         private final AdvertiseCallback mAdvertiseCallback;
    235         private final AdvertiseData mAdvertisement;
    236         private final AdvertiseData mScanResponse;
    237         private final AdvertiseSettings mSettings;
    238         private final IBluetoothGatt mBluetoothGatt;
    239 
    240         // mClientIf 0: not registered
    241         // -1: scan stopped
    242         // >0: registered and scan started
    243         private int mClientIf;
    244         private boolean mIsAdvertising = false;
    245 
    246         public AdvertiseCallbackWrapper(AdvertiseCallback advertiseCallback,
    247                 AdvertiseData advertiseData, AdvertiseData scanResponse,
    248                 AdvertiseSettings settings,
    249                 IBluetoothGatt bluetoothGatt) {
    250             mAdvertiseCallback = advertiseCallback;
    251             mAdvertisement = advertiseData;
    252             mScanResponse = scanResponse;
    253             mSettings = settings;
    254             mBluetoothGatt = bluetoothGatt;
    255             mClientIf = 0;
    256         }
    257 
    258         public void startRegisteration() {
    259             synchronized (this) {
    260                 if (mClientIf == -1) return;
    261 
    262                 try {
    263                     UUID uuid = UUID.randomUUID();
    264                     mBluetoothGatt.registerClient(new ParcelUuid(uuid), this);
    265                     wait(LE_CALLBACK_TIMEOUT_MILLIS);
    266                 } catch (InterruptedException | RemoteException e) {
    267                     Log.e(TAG, "Failed to start registeration", e);
    268                 }
    269                 if (mClientIf > 0 && mIsAdvertising) {
    270                     mLeAdvertisers.put(mAdvertiseCallback, this);
    271                 } else if (mClientIf <= 0) {
    272                     // Post internal error if registration failed.
    273                     postStartFailure(mAdvertiseCallback,
    274                             AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR);
    275                 } else {
    276                     // Unregister application if it's already registered but advertise failed.
    277                     try {
    278                         mBluetoothGatt.unregisterClient(mClientIf);
    279                         mClientIf = -1;
    280                     } catch (RemoteException e) {
    281                         Log.e(TAG, "remote exception when unregistering", e);
    282                     }
    283                 }
    284             }
    285         }
    286 
    287         public void stopAdvertising() {
    288             synchronized (this) {
    289                 try {
    290                     mBluetoothGatt.stopMultiAdvertising(mClientIf);
    291                     wait(LE_CALLBACK_TIMEOUT_MILLIS);
    292                 } catch (InterruptedException | RemoteException e) {
    293                     Log.e(TAG, "Failed to stop advertising", e);
    294                 }
    295                 // Advertise callback should have been removed from LeAdvertisers when
    296                 // onMultiAdvertiseCallback was called. In case onMultiAdvertiseCallback is never
    297                 // invoked and wait timeout expires, remove callback here.
    298                 if (mLeAdvertisers.containsKey(mAdvertiseCallback)) {
    299                     mLeAdvertisers.remove(mAdvertiseCallback);
    300                 }
    301             }
    302         }
    303 
    304         /**
    305          * Application interface registered - app is ready to go
    306          */
    307         @Override
    308         public void onClientRegistered(int status, int clientIf) {
    309             Log.d(TAG, "onClientRegistered() - status=" + status + " clientIf=" + clientIf);
    310             synchronized (this) {
    311                 if (status == BluetoothGatt.GATT_SUCCESS) {
    312                     mClientIf = clientIf;
    313                     try {
    314                         mBluetoothGatt.startMultiAdvertising(mClientIf, mAdvertisement,
    315                                 mScanResponse, mSettings);
    316                         return;
    317                     } catch (RemoteException e) {
    318                         Log.e(TAG, "failed to start advertising", e);
    319                     }
    320                 }
    321                 // Registration failed.
    322                 mClientIf = -1;
    323                 notifyAll();
    324             }
    325         }
    326 
    327         @Override
    328         public void onMultiAdvertiseCallback(int status, boolean isStart,
    329                 AdvertiseSettings settings) {
    330             synchronized (this) {
    331                 if (isStart) {
    332                     if (status == AdvertiseCallback.ADVERTISE_SUCCESS) {
    333                         // Start success
    334                         mIsAdvertising = true;
    335                         postStartSuccess(mAdvertiseCallback, settings);
    336                     } else {
    337                         // Start failure.
    338                         postStartFailure(mAdvertiseCallback, status);
    339                     }
    340                 } else {
    341                     // unregister client for stop.
    342                     try {
    343                         mBluetoothGatt.unregisterClient(mClientIf);
    344                         mClientIf = -1;
    345                         mIsAdvertising = false;
    346                         mLeAdvertisers.remove(mAdvertiseCallback);
    347                     } catch (RemoteException e) {
    348                         Log.e(TAG, "remote exception when unregistering", e);
    349                     }
    350                 }
    351                 notifyAll();
    352             }
    353 
    354         }
    355     }
    356 
    357     private void postStartFailure(final AdvertiseCallback callback, final int error) {
    358         mHandler.post(new Runnable() {
    359             @Override
    360             public void run() {
    361                 callback.onStartFailure(error);
    362             }
    363         });
    364     }
    365 
    366     private void postStartSuccess(final AdvertiseCallback callback,
    367             final AdvertiseSettings settings) {
    368         mHandler.post(new Runnable() {
    369 
    370             @Override
    371             public void run() {
    372                 callback.onStartSuccess(settings);
    373             }
    374         });
    375     }
    376 }
    377