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