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