Home | History | Annotate | Download | only in bluetooth
      1 /*
      2  * Copyright (C) 2017 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 com.googlecode.android_scripting.facade.bluetooth;
     18 
     19 import android.bluetooth.BluetoothAdapter;
     20 import android.bluetooth.BluetoothDevice;
     21 import android.bluetooth.le.AdvertiseData;
     22 import android.bluetooth.le.AdvertisingSet;
     23 import android.bluetooth.le.AdvertisingSetCallback;
     24 import android.bluetooth.le.AdvertisingSetParameters;
     25 import android.bluetooth.le.PeriodicAdvertisingParameters;
     26 import android.os.Bundle;
     27 import android.os.ParcelUuid;
     28 
     29 import com.googlecode.android_scripting.Log;
     30 import com.googlecode.android_scripting.MainThread;
     31 import com.googlecode.android_scripting.facade.EventFacade;
     32 import com.googlecode.android_scripting.facade.FacadeManager;
     33 import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
     34 import com.googlecode.android_scripting.rpc.Rpc;
     35 import com.googlecode.android_scripting.rpc.RpcParameter;
     36 
     37 import java.util.HashMap;
     38 import java.util.Iterator;
     39 import java.util.Map;
     40 import java.util.concurrent.Callable;
     41 
     42 import org.json.JSONArray;
     43 import org.json.JSONObject;
     44 
     45 /**
     46  * BluetoothLe AdvertisingSet functions.
     47  */
     48 public class BluetoothLeAdvertisingSetFacade extends RpcReceiver {
     49 
     50     private static int sAdvertisingSetCount;
     51     private static int sAdvertisingSetCallbackCount;
     52     private final EventFacade mEventFacade;
     53     private final HashMap<Integer, MyAdvertisingSetCallback> mAdvertisingSetCallbacks;
     54     private final HashMap<Integer, AdvertisingSet> mAdvertisingSets;
     55     private BluetoothAdapter mBluetoothAdapter;
     56 
     57     private static final Map<String, Integer> ADV_PHYS = new HashMap<>();
     58     static {
     59         ADV_PHYS.put("PHY_LE_1M", BluetoothDevice.PHY_LE_1M);
     60         ADV_PHYS.put("PHY_LE_2M", BluetoothDevice.PHY_LE_2M);
     61         ADV_PHYS.put("PHY_LE_CODED", BluetoothDevice.PHY_LE_CODED);
     62     }
     63 
     64     public BluetoothLeAdvertisingSetFacade(FacadeManager manager) {
     65         super(manager);
     66         mBluetoothAdapter = MainThread.run(manager.getService(),
     67                 new Callable<BluetoothAdapter>() {
     68                     @Override
     69                     public BluetoothAdapter call() throws Exception {
     70                         return BluetoothAdapter.getDefaultAdapter();
     71                     }
     72                 });
     73         mEventFacade = manager.getReceiver(EventFacade.class);
     74         mAdvertisingSetCallbacks = new HashMap<>();
     75         mAdvertisingSets = new HashMap<>();
     76     }
     77 
     78     /**
     79      * Constructs a MyAdvertisingSetCallback obj and returns its index
     80      *
     81      * @return MyAdvertisingSetCallback.index
     82      */
     83     @Rpc(description = "Generate a new MyAdvertisingSetCallback Object")
     84     public Integer bleAdvSetGenCallback() {
     85         int index = ++sAdvertisingSetCallbackCount;
     86         MyAdvertisingSetCallback callback = new MyAdvertisingSetCallback(index);
     87         mAdvertisingSetCallbacks.put(callback.index, callback);
     88         return callback.index;
     89     }
     90 
     91     /**
     92      * Converts String or JSONArray representation of byte array into raw byte array
     93      */
     94     public byte[] somethingToByteArray(Object something) throws Exception {
     95         if (something instanceof String) {
     96             String s = (String) something;
     97             int len = s.length();
     98             byte[] data = new byte[len / 2];
     99             for (int i = 0; i < len; i += 2) {
    100                 data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
    101                                      + Character.digit(s.charAt(i + 1), 16));
    102             }
    103             return data;
    104         } else if (something instanceof JSONArray) {
    105             JSONArray arr = (JSONArray) something;
    106             int len = arr.length();
    107             byte[] data = new byte[len];
    108             for (int i = 0; i < len; i++) {
    109                 data[i] = (byte) arr.getInt(i);
    110             }
    111             return data;
    112         } else {
    113             throw new IllegalArgumentException("Don't know how to convert "
    114                 + something.getClass().getName() + " to byte array!");
    115         }
    116     }
    117 
    118     /**
    119      * Converts JSONObject representation of AdvertiseData into actual object.
    120      */
    121     public AdvertiseData buildAdvData(JSONObject params) throws Exception {
    122         AdvertiseData.Builder builder = new AdvertiseData.Builder();
    123 
    124         Iterator<String> keys = params.keys();
    125         while (keys.hasNext()) {
    126             String key = keys.next();
    127 
    128             /** Python doesn't have multi map, if advertise data should repeat use
    129               * serviceUuid, serviceUuid2, serviceUuid3... . For that use "startsWith"
    130               */
    131             if (key.startsWith("manufacturerData")) {
    132                 JSONArray manuf = params.getJSONArray(key);
    133                 if (manuf.length() != 2) {
    134                     throw new IllegalArgumentException(
    135                         "manufacturerData should contain exactly two elements");
    136                 }
    137                 int manufId = manuf.getInt(0);
    138                 byte[] data = somethingToByteArray(manuf.get(1));
    139                 builder.addManufacturerData(manufId, data);
    140             } else if (key.startsWith("serviceData")) {
    141                 JSONArray serDat = params.getJSONArray(key);
    142                 ParcelUuid uuid = ParcelUuid.fromString(serDat.getString(0));
    143                 byte[] data = somethingToByteArray(serDat.get(1));
    144                 builder.addServiceData(uuid, data);
    145             } else if (key.startsWith("serviceUuid")) {
    146                 builder.addServiceUuid(ParcelUuid.fromString(params.getString(key)));
    147             } else if (key.startsWith("includeDeviceName")) {
    148                 builder.setIncludeDeviceName(params.getBoolean(key));
    149             } else if (key.startsWith("includeTxPowerLevel")) {
    150                 builder.setIncludeTxPowerLevel(params.getBoolean(key));
    151             } else {
    152                 throw new IllegalArgumentException("Unknown AdvertiseData field " + key);
    153             }
    154         }
    155 
    156         return builder.build();
    157     }
    158 
    159     /**
    160      * Converts JSONObject representation of AdvertisingSetParameters into actual object.
    161      */
    162     public AdvertisingSetParameters buildParameters(JSONObject params) throws Exception {
    163         AdvertisingSetParameters.Builder builder = new AdvertisingSetParameters.Builder();
    164 
    165         Iterator<String> keys = params.keys();
    166         while (keys.hasNext()) {
    167             String key = keys.next();
    168 
    169             if (key.equals("connectable")) {
    170                 builder.setConnectable(params.getBoolean(key));
    171             } else if (key.equals("scannable")) {
    172                 builder.setScannable(params.getBoolean(key));
    173             } else if (key.equals("legacyMode")) {
    174                 builder.setLegacyMode(params.getBoolean(key));
    175             } else if (key.equals("anonymous")) {
    176                 builder.setAnonymous(params.getBoolean(key));
    177             } else if (key.equals("includeTxPower")) {
    178                 builder.setIncludeTxPower(params.getBoolean(key));
    179             } else if (key.equals("primaryPhy")) {
    180                 builder.setPrimaryPhy(ADV_PHYS.get(params.getString(key)));
    181             } else if (key.equals("secondaryPhy")) {
    182                 builder.setSecondaryPhy(ADV_PHYS.get(params.getString(key)));
    183             } else if (key.equals("interval")) {
    184                 builder.setInterval(params.getInt(key));
    185             } else if (key.equals("txPowerLevel")) {
    186                 builder.setTxPowerLevel(params.getInt(key));
    187             } else {
    188                 throw new IllegalArgumentException("Unknown AdvertisingSetParameters field " + key);
    189             }
    190         }
    191 
    192         return builder.build();
    193     }
    194 
    195     /**
    196      * Converts JSONObject representation of PeriodicAdvertisingParameters into actual object.
    197      */
    198     public PeriodicAdvertisingParameters buildPeriodicParameters(JSONObject params)
    199             throws Exception {
    200         PeriodicAdvertisingParameters.Builder builder = new PeriodicAdvertisingParameters.Builder();
    201 
    202         Iterator<String> keys = params.keys();
    203         while (keys.hasNext()) {
    204             String key = keys.next();
    205 
    206             if (key.equals("includeTxPower")) {
    207                 builder.setIncludeTxPower(params.getBoolean(key));
    208             } else if (key.equals("interval")) {
    209                 builder.setInterval(params.getInt(key));
    210             } else {
    211                 throw new IllegalArgumentException(
    212                         "Unknown PeriodicAdvertisingParameters field " + key);
    213             }
    214         }
    215 
    216         return builder.build();
    217     }
    218 
    219     /**
    220      * Starts ble advertising
    221      *
    222      * @throws Exception
    223      */
    224     @Rpc(description = "Starts ble advertisement")
    225     public void bleAdvSetStartAdvertisingSet(
    226             @RpcParameter(name = "params") JSONObject parametersJson,
    227             @RpcParameter(name = "data") JSONObject dataJson,
    228             @RpcParameter(name = "scanResponse") JSONObject scanResponseJson,
    229             @RpcParameter(name = "periodicParameters") JSONObject periodicParametersJson,
    230             @RpcParameter(name = "periodicDataIndex") JSONObject periodicDataJson,
    231             @RpcParameter(name = "duration") Integer duration,
    232             @RpcParameter(name = "maxExtAdvEvents") Integer maxExtAdvEvents,
    233             @RpcParameter(name = "callbackIndex") Integer callbackIndex) throws Exception {
    234 
    235         AdvertisingSetParameters parameters = null;
    236         if (parametersJson != null) {
    237             parameters = buildParameters(parametersJson);
    238         }
    239 
    240         AdvertiseData data = null;
    241         if (dataJson != null) {
    242             data = buildAdvData(dataJson);
    243         }
    244 
    245         AdvertiseData scanResponse = null;
    246         if (scanResponseJson != null) {
    247             scanResponse = buildAdvData(scanResponseJson);
    248         }
    249 
    250         PeriodicAdvertisingParameters periodicParameters = null;
    251         if (periodicParametersJson != null) {
    252             periodicParameters = buildPeriodicParameters(periodicParametersJson);
    253         }
    254 
    255         AdvertiseData periodicData = null;
    256         if (periodicDataJson != null) {
    257             periodicData = buildAdvData(periodicDataJson);
    258         }
    259 
    260         MyAdvertisingSetCallback callback = mAdvertisingSetCallbacks.get(callbackIndex);
    261         if (callback != null) {
    262             Log.d("starting le advertising set on callback index: " + callbackIndex);
    263             mBluetoothAdapter.getBluetoothLeAdvertiser().startAdvertisingSet(
    264                     parameters, data, scanResponse, periodicParameters, periodicData, callback);
    265         } else {
    266             throw new Exception("Invalid callbackIndex input" + callbackIndex);
    267         }
    268     }
    269 
    270     /**
    271      * Get the address associated with this Advertising set. This method returns immediately,
    272      * the operation result is delivered through callback.onOwnAddressRead().
    273      * This is for PTS only.
    274      */
    275     @Rpc(description = "Get own address")
    276     public void bleAdvSetGetOwnAddress(
    277             @RpcParameter(name = "setIndex") Integer setIndex) throws Exception {
    278         mAdvertisingSets.get(setIndex).getOwnAddress();
    279     }
    280 
    281     /**
    282      * Enables Advertising. This method returns immediately, the operation status is
    283      * delivered through callback.onAdvertisingEnabled().
    284      *
    285      * @param enable whether the advertising should be enabled (true), or disabled (false)
    286      * @param duration advertising duration, in 10ms unit. Valid range is from 1 (10ms) to
    287      *                     65535 (655,350 ms)
    288      * @param maxExtAdvEvents maximum number of extended advertising events the
    289      *                     controller shall attempt to send prior to terminating the extended
    290      *                     advertising, even if the duration has not expired. Valid range is
    291      *                     from 1 to 255.
    292      */
    293     @Rpc(description = "Enable/disable advertising")
    294     public void bleAdvSetEnableAdvertising(
    295             @RpcParameter(name = "setIndex") Integer setIndex,
    296             @RpcParameter(name = "enable") Boolean enable,
    297             @RpcParameter(name = "duration") Integer duration,
    298             @RpcParameter(name = "maxExtAdvEvents") Integer maxExtAdvEvents) throws Exception {
    299         mAdvertisingSets.get(setIndex).enableAdvertising(enable, duration, maxExtAdvEvents);
    300     }
    301 
    302     /**
    303      * Set/update data being Advertised. Make sure that data doesn't exceed the size limit for
    304      * specified AdvertisingSetParameters. This method returns immediately, the operation status is
    305      * delivered through callback.onAdvertisingDataSet().
    306      *
    307      * Advertising data must be empty if non-legacy scannable advertising is used.
    308      *
    309      * @param dataJson Advertisement data to be broadcasted. Size must not exceed
    310      *                     {@link BluetoothAdapter#getLeMaximumAdvertisingDataLength}. If the
    311      *                     advertisement is connectable, three bytes will be added for flags. If the
    312      *                     update takes place when the advertising set is enabled, the data can be
    313      *                     maximum 251 bytes long.
    314      */
    315     @Rpc(description = "Set advertise data")
    316     public void bleAdvSetSetAdvertisingData(
    317             @RpcParameter(name = "setIndex") Integer setIndex,
    318             @RpcParameter(name = "data") JSONObject dataJson) throws Exception {
    319         AdvertiseData data = null;
    320         if (dataJson != null) {
    321             data = buildAdvData(dataJson);
    322         }
    323 
    324         Log.i("setAdvertisingData()");
    325         mAdvertisingSets.get(setIndex).setAdvertisingData(data);
    326     }
    327 
    328     /**
    329      * Stops a ble advertising set
    330      *
    331      * @param index the id of the advertising set to stop
    332      * @throws Exception
    333      */
    334     @Rpc(description = "Stops an ongoing ble advertising set")
    335     public void bleAdvSetStopAdvertisingSet(
    336             @RpcParameter(name = "index")
    337             Integer index) throws Exception {
    338         MyAdvertisingSetCallback callback = mAdvertisingSetCallbacks.remove(index);
    339         if (callback == null) {
    340             throw new Exception("Invalid index input:" + index);
    341         }
    342 
    343         Log.d("stopping le advertising set " + index);
    344         mBluetoothAdapter.getBluetoothLeAdvertiser().stopAdvertisingSet(callback);
    345     }
    346 
    347     private class MyAdvertisingSetCallback extends AdvertisingSetCallback {
    348         public Integer index;
    349         public Integer setIndex = -1;
    350         String mEventType;
    351 
    352         MyAdvertisingSetCallback(int idx) {
    353             index = idx;
    354             mEventType = "AdvertisingSet";
    355         }
    356 
    357         @Override
    358         public void onAdvertisingSetStarted(AdvertisingSet advertisingSet, int txPower,
    359                     int status) {
    360             Log.d("onAdvertisingSetStarted" + mEventType + " " + index);
    361             Bundle results = new Bundle();
    362             results.putString("Type", "onAdvertisingSetStarted");
    363             results.putInt("status", status);
    364             if (advertisingSet != null) {
    365                 setIndex = ++sAdvertisingSetCount;
    366                 mAdvertisingSets.put(setIndex, advertisingSet);
    367                 results.putInt("setId", setIndex);
    368             } else {
    369                 mAdvertisingSetCallbacks.remove(index);
    370             }
    371             mEventFacade.postEvent(mEventType + index + "onAdvertisingSetStarted", results);
    372         }
    373 
    374         @Override
    375         public void onAdvertisingSetStopped(AdvertisingSet advertisingSet) {
    376             Log.d("onAdvertisingSetStopped" + mEventType + " " + index);
    377             Bundle results = new Bundle();
    378             results.putString("Type", "onAdvertisingSetStopped");
    379             mEventFacade.postEvent(mEventType + index + "onAdvertisingSetStopped", results);
    380         }
    381 
    382         @Override
    383         public void onAdvertisingEnabled(AdvertisingSet advertisingSet, boolean enable,
    384                 int status) {
    385             sendGeneric("onAdvertisingEnabled", setIndex, status, enable);
    386         }
    387 
    388         @Override
    389         public void onAdvertisingDataSet(AdvertisingSet advertisingSet, int status) {
    390             sendGeneric("onAdvertisingDataSet", setIndex, status);
    391         }
    392 
    393         @Override
    394         public void onScanResponseDataSet(AdvertisingSet advertisingSet, int status) {
    395             sendGeneric("onScanResponseDataSet", setIndex, status);
    396         }
    397 
    398         @Override
    399         public void onAdvertisingParametersUpdated(AdvertisingSet advertisingSet, int txPower,
    400                 int status) {
    401             sendGeneric("onAdvertisingParametersUpdated", setIndex, status);
    402         }
    403 
    404         @Override
    405         public void onPeriodicAdvertisingParametersUpdated(AdvertisingSet advertisingSet,
    406                 int status) {
    407             sendGeneric("onPeriodicAdvertisingParametersUpdated", setIndex, status);
    408         }
    409 
    410         @Override
    411         public void onPeriodicAdvertisingDataSet(AdvertisingSet advertisingSet, int status) {
    412             sendGeneric("onPeriodicAdvertisingDataSet", setIndex, status);
    413         }
    414 
    415         @Override
    416         public void onPeriodicAdvertisingEnabled(AdvertisingSet advertisingSet, boolean enable,
    417                 int status) {
    418             sendGeneric("onPeriodicAdvertisingEnabled", setIndex, status, enable);
    419         }
    420 
    421         @Override
    422         public void onOwnAddressRead(AdvertisingSet advertisingSet, int addressType,
    423                 String address) {
    424             Log.d("onOwnAddressRead" + mEventType + " " + setIndex);
    425             Bundle results = new Bundle();
    426             results.putInt("setId", setIndex);
    427             results.putInt("addressType", addressType);
    428             results.putString("address", address);
    429             mEventFacade.postEvent(mEventType + setIndex + "onOwnAddressRead", results);
    430         }
    431 
    432         public void sendGeneric(String cb, int setIndex, int status) {
    433             sendGeneric(cb, setIndex, status, null);
    434         }
    435 
    436         public void sendGeneric(String cb, int setIndex, int status, Boolean enable) {
    437             Log.d(cb + mEventType + " " + index);
    438             Bundle results = new Bundle();
    439             results.putInt("setId", setIndex);
    440             results.putInt("status", status);
    441             if (enable != null) results.putBoolean("enable", enable);
    442             mEventFacade.postEvent(mEventType + index + cb, results);
    443         }
    444     }
    445 
    446     @Override
    447     public void shutdown() {
    448         if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) {
    449             Iterator<Map.Entry<Integer, MyAdvertisingSetCallback>> it =
    450                     mAdvertisingSetCallbacks.entrySet().iterator();
    451             while (it.hasNext()) {
    452                 Map.Entry<Integer, MyAdvertisingSetCallback> entry = it.next();
    453                 MyAdvertisingSetCallback advertisingSetCb = entry.getValue();
    454                 it.remove();
    455 
    456                 if (advertisingSetCb == null) continue;
    457 
    458                 Log.d("shutdown() stopping le advertising set " + advertisingSetCb.index);
    459                 try {
    460                     mBluetoothAdapter.getBluetoothLeAdvertiser()
    461                         .stopAdvertisingSet(advertisingSetCb);
    462                 } catch (NullPointerException e) {
    463                     Log.e("Failed to stop ble advertising.", e);
    464                 }
    465             }
    466         }
    467         mAdvertisingSetCallbacks.clear();
    468         mAdvertisingSets.clear();
    469     }
    470 }
    471