1 /* 2 * Copyright 2021 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.android.libraries.testing.deviceshadower.internal.bluetooth; 18 19 import android.annotation.Nullable; 20 import android.bluetooth.BluetoothAdapter; 21 import android.bluetooth.BluetoothGatt; 22 import android.bluetooth.IBluetoothGattCallback; 23 import android.bluetooth.IBluetoothGattServerCallback; 24 import android.bluetooth.le.AdvertiseData; 25 import android.bluetooth.le.AdvertiseSettings; 26 import android.bluetooth.le.ScanFilter; 27 import android.bluetooth.le.ScanRecord; 28 import android.bluetooth.le.ScanResult; 29 import android.bluetooth.le.ScanSettings; 30 import android.os.Build; 31 import android.os.Build.VERSION; 32 import android.os.ParcelUuid; 33 import android.os.SystemClock; 34 35 import com.android.internal.annotations.VisibleForTesting; 36 import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl; 37 import com.android.libraries.testing.deviceshadower.internal.DeviceletImpl; 38 import com.android.libraries.testing.deviceshadower.internal.utils.GattHelper; 39 import com.android.libraries.testing.deviceshadower.internal.utils.Logger; 40 41 import com.google.common.base.Preconditions; 42 import com.google.common.primitives.Bytes; 43 44 import org.robolectric.util.ReflectionHelpers; 45 import org.robolectric.util.ReflectionHelpers.ClassParameter; 46 47 import java.util.ArrayList; 48 import java.util.Collection; 49 import java.util.HashMap; 50 import java.util.HashSet; 51 import java.util.List; 52 import java.util.Map; 53 import java.util.Set; 54 import java.util.concurrent.ConcurrentHashMap; 55 import java.util.concurrent.Future; 56 import java.util.concurrent.atomic.AtomicBoolean; 57 import java.util.concurrent.atomic.AtomicInteger; 58 59 /** 60 * Delegate to operate gatt operations. 61 */ 62 public class GattDelegate { 63 64 private static final int DEFAULT_RSSI = -50; 65 private static final Logger LOGGER = Logger.create("GattDelegate"); 66 67 // chipset properties 68 // use 2 as API 21 requires multi-advertisement support to use Le Advertising. 69 private final int mMaxAdvertiseInstances = 2; 70 private final AtomicBoolean mIsOffloadedFilteringSupported = new AtomicBoolean(false); 71 private final String mAddress; 72 private final AtomicInteger mCurrentClientIf = new AtomicInteger(0); 73 private final AtomicInteger mCurrentServerIf = new AtomicInteger(0); 74 private final AtomicBoolean mCurrentConnectionState = new AtomicBoolean(false); 75 private final Map<ParcelUuid, Service> mServices = new HashMap<>(); 76 private final Map<Integer, IBluetoothGattCallback> mClientCallbacks; 77 private final Map<Integer, IBluetoothGattServerCallback> mServerCallbacks; 78 private final Map<Integer, Advertiser> mAdvertisers; 79 private final Map<Integer, Scanner> mScanners; 80 @Nullable 81 private Request mLastRequest; 82 private boolean mConnectable = true; 83 84 /** 85 * The parameters of a request, e.g. readCharacteristic(). Subclass for each request. 86 * 87 * @see #getLastRequest() 88 */ 89 abstract static class Request { 90 91 final int mSrvcType; 92 final int mSrvcInstId; 93 final ParcelUuid mSrvcId; 94 final int mCharInstId; 95 final ParcelUuid mCharId; 96 Request(int srvcType, int srvcInstId, ParcelUuid srvcId, int charInstId, ParcelUuid charId)97 Request(int srvcType, int srvcInstId, ParcelUuid srvcId, int charInstId, 98 ParcelUuid charId) { 99 this.mSrvcType = srvcType; 100 this.mSrvcInstId = srvcInstId; 101 this.mSrvcId = srvcId; 102 this.mCharInstId = charInstId; 103 this.mCharId = charId; 104 } 105 } 106 107 /** 108 * Corresponds to {@link android.bluetooth.IBluetoothGatt#readCharacteristic}. 109 */ 110 static class ReadCharacteristicRequest extends Request { 111 ReadCharacteristicRequest( int srvcType, int srvcInstId, ParcelUuid srvcId, int charInstId, ParcelUuid charId)112 ReadCharacteristicRequest( 113 int srvcType, int srvcInstId, ParcelUuid srvcId, int charInstId, 114 ParcelUuid charId) { 115 super(srvcType, srvcInstId, srvcId, charInstId, charId); 116 } 117 } 118 119 /** 120 * Corresponds to {@link android.bluetooth.IBluetoothGatt#readDescriptor}. 121 */ 122 static class ReadDescriptorRequest extends Request { 123 124 final int mDescrInstId; 125 final ParcelUuid mDescrId; 126 ReadDescriptorRequest( int srvcType, int srvcInstId, ParcelUuid srvcId, int charInstId, ParcelUuid charId, int descrInstId, ParcelUuid descrId)127 ReadDescriptorRequest( 128 int srvcType, 129 int srvcInstId, 130 ParcelUuid srvcId, 131 int charInstId, 132 ParcelUuid charId, 133 int descrInstId, 134 ParcelUuid descrId) { 135 super(srvcType, srvcInstId, srvcId, charInstId, charId); 136 this.mDescrInstId = descrInstId; 137 this.mDescrId = descrId; 138 } 139 } 140 GattDelegate(String address)141 GattDelegate(String address) { 142 this( 143 address, 144 new HashMap<>(), 145 new HashMap<>(), 146 new ConcurrentHashMap<>(), 147 new ConcurrentHashMap<>()); 148 } 149 150 @VisibleForTesting GattDelegate( String address, Map<Integer, IBluetoothGattCallback> clientCallbacks, Map<Integer, IBluetoothGattServerCallback> serverCallbacks, Map<Integer, Advertiser> advertisers, Map<Integer, Scanner> scanners)151 GattDelegate( 152 String address, 153 Map<Integer, IBluetoothGattCallback> clientCallbacks, 154 Map<Integer, IBluetoothGattServerCallback> serverCallbacks, 155 Map<Integer, Advertiser> advertisers, 156 Map<Integer, Scanner> scanners) { 157 this.mAddress = address; 158 this.mClientCallbacks = clientCallbacks; 159 this.mServerCallbacks = serverCallbacks; 160 this.mAdvertisers = advertisers; 161 this.mScanners = scanners; 162 } 163 setRefuseConnections(boolean refuse)164 public void setRefuseConnections(boolean refuse) { 165 this.mConnectable = !refuse; 166 } 167 168 /** 169 * Used to maintain state between the request (e.g. readCharacteristic()) and sendResponse(). 170 */ 171 @Nullable getLastRequest()172 Request getLastRequest() { 173 return mLastRequest; 174 } 175 176 /** 177 * @see #getLastRequest() 178 */ setLastRequest(@ullable Request params)179 void setLastRequest(@Nullable Request params) { 180 mLastRequest = params; 181 } 182 getClientIf()183 public int getClientIf() { 184 // TODO(b/200231384): support multiple client if. 185 return mCurrentClientIf.get(); 186 } 187 getServerIf()188 public int getServerIf() { 189 // TODO(b/200231384): support multiple server if. 190 return mCurrentServerIf.get(); 191 } 192 getServerCallback(int serverIf)193 public IBluetoothGattServerCallback getServerCallback(int serverIf) { 194 return mServerCallbacks.get(serverIf); 195 } 196 getClientCallback(int clientIf)197 public IBluetoothGattCallback getClientCallback(int clientIf) { 198 return mClientCallbacks.get(clientIf); 199 } 200 registerServer(IBluetoothGattServerCallback callback)201 public int registerServer(IBluetoothGattServerCallback callback) { 202 mServerCallbacks.put(mCurrentServerIf.incrementAndGet(), callback); 203 return getServerIf(); 204 } 205 registerClient(IBluetoothGattCallback callback)206 public int registerClient(IBluetoothGattCallback callback) { 207 mClientCallbacks.put(mCurrentClientIf.incrementAndGet(), callback); 208 LOGGER.d(String.format("Client registered on %s, clientIf: %d", mAddress, getClientIf())); 209 return getClientIf(); 210 } 211 unregisterClient(int clientIf)212 public void unregisterClient(int clientIf) { 213 mClientCallbacks.remove(clientIf); 214 LOGGER.d(String.format("Client unregistered on %s, clientIf: %d", mAddress, clientIf)); 215 } 216 unregisterServer(int serverIf)217 public void unregisterServer(int serverIf) { 218 mServerCallbacks.remove(serverIf); 219 } 220 getMaxAdvertiseInstances()221 public int getMaxAdvertiseInstances() { 222 return mMaxAdvertiseInstances; 223 } 224 isOffloadedFilteringSupported()225 public boolean isOffloadedFilteringSupported() { 226 return mIsOffloadedFilteringSupported.get(); 227 } 228 connect(String address)229 public boolean connect(String address) { 230 return mConnectable; 231 } 232 disconnect(String address)233 public boolean disconnect(String address) { 234 return true; 235 } 236 clientConnectionStateChange( int state, int clientIf, boolean connected, String address)237 public void clientConnectionStateChange( 238 int state, int clientIf, boolean connected, String address) { 239 if (connected != mCurrentConnectionState.get()) { 240 mCurrentConnectionState.set(connected); 241 IBluetoothGattCallback callback = getClientCallback(clientIf); 242 if (callback != null) { 243 callback.onClientConnectionState(state, clientIf, connected, address); 244 } 245 } 246 } 247 serverConnectionStateChange( int state, int serverIf, boolean connected, String address)248 public void serverConnectionStateChange( 249 int state, int serverIf, boolean connected, String address) { 250 if (connected != mCurrentConnectionState.get()) { 251 mCurrentConnectionState.set(connected); 252 IBluetoothGattServerCallback callback = getServerCallback(serverIf); 253 if (callback != null) { 254 callback.onServerConnectionState(state, serverIf, connected, address); 255 } 256 } 257 } 258 addService(ParcelUuid uuid)259 public Service addService(ParcelUuid uuid) { 260 Service srvc = new Service(uuid); 261 mServices.put(uuid, srvc); 262 return srvc; 263 } 264 getServices()265 public Collection<Service> getServices() { 266 return mServices.values(); 267 } 268 getService(ParcelUuid uuid)269 public Service getService(ParcelUuid uuid) { 270 return mServices.get(uuid); 271 } 272 clientSetMtu(int clientIf, int mtu, String serverAddress)273 public void clientSetMtu(int clientIf, int mtu, String serverAddress) { 274 IBluetoothGattCallback callback = getClientCallback(clientIf); 275 if (callback != null && Build.VERSION.SDK_INT >= 21) { 276 callback.onConfigureMTU(serverAddress, mtu, BluetoothGatt.GATT_SUCCESS); 277 } 278 } 279 serverSetMtu(int serverIf, int mtu, String clientAddress)280 public void serverSetMtu(int serverIf, int mtu, String clientAddress) { 281 IBluetoothGattServerCallback callback = getServerCallback(serverIf); 282 if (callback != null && Build.VERSION.SDK_INT >= 22) { 283 callback.onMtuChanged(clientAddress, mtu); 284 } 285 } 286 startMultiAdvertising( int appIf, AdvertiseData advertiseData, AdvertiseData scanResponse, final AdvertiseSettings settings)287 public void startMultiAdvertising( 288 int appIf, 289 AdvertiseData advertiseData, 290 AdvertiseData scanResponse, 291 final AdvertiseSettings settings) { 292 LOGGER.d(String.format("startMultiAdvertising(%d) on %s", appIf, mAddress)); 293 final Advertiser advertiser = 294 new Advertiser( 295 appIf, 296 mAddress, 297 DeviceShadowEnvironmentImpl.getLocalBlueletImpl().mName, 298 txPowerFromFlag(settings.getTxPowerLevel()), 299 advertiseData, 300 scanResponse, 301 settings); 302 mAdvertisers.put(appIf, advertiser); 303 final IBluetoothGattCallback callback = mClientCallbacks.get(appIf); 304 @SuppressWarnings("unused") // go/futurereturn-lsc 305 Future<?> possiblyIgnoredError = 306 DeviceShadowEnvironmentImpl.run( 307 mAddress, 308 () -> { 309 callback.onMultiAdvertiseCallback( 310 BluetoothConstants.ADVERTISE_SUCCESS, true /* isStart */, 311 settings); 312 return null; 313 }); 314 } 315 316 /** 317 * Returns TxPower in dBm as measured at the source. 318 * 319 * <p>Note that this will vary by device and the values are only roughly accurate. The 320 * measurements were taken with a Nexus 6. Copied from the TxEddystone-UID app: 321 * {https://github.com/google/eddystone/blob/master/eddystone-uid/tools/txeddystone-uid/TxEddystone-UID/app/src/main/java/com/google/sample/txeddystone_uid/MainActivity.java} 322 */ txPowerFromFlag(int txPowerFlag)323 private static byte txPowerFromFlag(int txPowerFlag) { 324 switch (txPowerFlag) { 325 case AdvertiseSettings.ADVERTISE_TX_POWER_HIGH: 326 return (byte) -16; 327 case AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM: 328 return (byte) -26; 329 case AdvertiseSettings.ADVERTISE_TX_POWER_LOW: 330 return (byte) -35; 331 case AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW: 332 return (byte) -59; 333 default: 334 throw new IllegalStateException("Unknown TxPower level=" + txPowerFlag); 335 } 336 } 337 stopMultiAdvertising(int appIf)338 public void stopMultiAdvertising(int appIf) { 339 LOGGER.d(String.format("stopAdvertising(%d) on %s", appIf, mAddress)); 340 Advertiser advertiser = mAdvertisers.get(appIf); 341 if (advertiser == null) { 342 LOGGER.d(String.format("Advertising already stopped on %s, clientIf: %d", mAddress, 343 appIf)); 344 return; 345 } 346 mAdvertisers.remove(appIf); 347 final IBluetoothGattCallback callback = mClientCallbacks.get(appIf); 348 @SuppressWarnings("unused") // go/futurereturn-lsc 349 Future<?> possiblyIgnoredError = 350 DeviceShadowEnvironmentImpl.run( 351 mAddress, 352 () -> { 353 callback.onMultiAdvertiseCallback( 354 BluetoothConstants.ADVERTISE_SUCCESS, false /* isStart */, 355 null /* setting */); 356 return null; 357 }); 358 } 359 startScan(final int appIf, ScanSettings settings, List<ScanFilter> filters)360 public void startScan(final int appIf, ScanSettings settings, List<ScanFilter> filters) { 361 LOGGER.d(String.format("startScan(%d) on %s", appIf, mAddress)); 362 if (filters == null) { 363 filters = new ArrayList<>(); 364 } 365 final Scanner scanner = new Scanner(appIf, settings, filters); 366 mScanners.put(appIf, scanner); 367 @SuppressWarnings("unused") // go/futurereturn-lsc 368 Future<?> possiblyIgnoredError = 369 DeviceShadowEnvironmentImpl.run( 370 mAddress, 371 () -> { 372 try { 373 scan(scanner); 374 } catch (InterruptedException e) { 375 LOGGER.e( 376 String.format("Failed to scan on %s, clientIf: %d.", 377 mAddress, scanner.mClientIf), 378 e); 379 } 380 return null; 381 }); 382 } 383 384 // TODO(b/200231384): support periodic scan with interval and scan window. scan(Scanner scanner)385 private void scan(Scanner scanner) throws InterruptedException { 386 // fetch existing advertisements 387 List<DeviceletImpl> devicelets = DeviceShadowEnvironmentImpl.getDeviceletImpls(); 388 for (DeviceletImpl devicelet : devicelets) { 389 BlueletImpl bluelet = devicelet.blueletImpl(); 390 if (bluelet.address.equals(mAddress)) { 391 continue; 392 } 393 for (Advertiser advertiser : bluelet.getGattDelegate().mAdvertisers.values()) { 394 if (VERSION.SDK_INT < 21) { 395 throw new UnsupportedOperationException( 396 String.format("API %d is not supported.", VERSION.SDK_INT)); 397 } 398 399 byte[] advertiseData = 400 GattHelper.convertAdvertiseData( 401 advertiser.mAdvertiseData, 402 advertiser.mTxPowerLevel, 403 advertiser.mName, 404 advertiser.mSettings.isConnectable()); 405 byte[] scanResponse = 406 GattHelper.convertAdvertiseData( 407 advertiser.mScanResponse, 408 advertiser.mTxPowerLevel, 409 advertiser.mName, 410 advertiser.mSettings.isConnectable()); 411 412 ScanRecord scanRecord = 413 ReflectionHelpers.callStaticMethod( 414 ScanRecord.class, 415 "parseFromBytes", 416 ClassParameter.from(byte[].class, 417 Bytes.concat(advertiseData, scanResponse))); 418 ScanResult scanResult = 419 new ScanResult( 420 BluetoothAdapter.getDefaultAdapter() 421 .getRemoteDevice(advertiser.mAddress), 422 scanRecord, 423 DEFAULT_RSSI, 424 SystemClock.elapsedRealtimeNanos()); 425 426 if (!matchFilters(scanResult, scanner.mFilters)) { 427 continue; 428 } 429 430 IBluetoothGattCallback callback = mClientCallbacks.get(scanner.mClientIf); 431 if (callback == null) { 432 LOGGER.e( 433 String.format("Callback is null on %s, clientIf: %d", mAddress, 434 scanner.mClientIf)); 435 return; 436 } 437 callback.onScanResult(scanResult); 438 } 439 } 440 } 441 matchFilters(ScanResult scanResult, List<ScanFilter> filters)442 private boolean matchFilters(ScanResult scanResult, List<ScanFilter> filters) { 443 for (ScanFilter filter : filters) { 444 if (!filter.matches(scanResult)) { 445 return false; 446 } 447 } 448 return true; 449 } 450 stopScan(int appIf)451 public void stopScan(int appIf) { 452 LOGGER.d(String.format("stopScan(%d) on %s", appIf, mAddress)); 453 Scanner scanner = mScanners.get(appIf); 454 if (scanner == null) { 455 LOGGER.d( 456 String.format("Scanning already stopped on %s, clientIf: %d", mAddress, appIf)); 457 return; 458 } 459 mScanners.remove(appIf); 460 } 461 462 static class Service { 463 464 private Map<ParcelUuid, Characteristic> mCharacteristics = new HashMap<>(); 465 private ParcelUuid mUuid; 466 Service(ParcelUuid uuid)467 Service(ParcelUuid uuid) { 468 this.mUuid = uuid; 469 } 470 getCharacteristic(ParcelUuid uuid)471 Characteristic getCharacteristic(ParcelUuid uuid) { 472 return mCharacteristics.get(uuid); 473 } 474 addCharacteristic(ParcelUuid uuid, int properties, int permissions)475 Characteristic addCharacteristic(ParcelUuid uuid, int properties, int permissions) { 476 Characteristic ch = new Characteristic(uuid, properties, permissions); 477 mCharacteristics.put(uuid, ch); 478 return ch; 479 } 480 getCharacteristics()481 Collection<Characteristic> getCharacteristics() { 482 return mCharacteristics.values(); 483 } 484 getUuid()485 ParcelUuid getUuid() { 486 return this.mUuid; 487 } 488 } 489 490 static class Characteristic { 491 492 private int mProperties; 493 private ParcelUuid mUuid; 494 private Map<ParcelUuid, Descriptor> mDescriptors = new HashMap<>(); 495 private Set<String> mNotifyClients = new HashSet<>(); 496 private byte[] mValue; 497 Characteristic(ParcelUuid uuid, int properties, int permissions)498 Characteristic(ParcelUuid uuid, int properties, int permissions) { 499 this.mProperties = properties; 500 this.mUuid = uuid; 501 } 502 getDescriptor(ParcelUuid uuid)503 Descriptor getDescriptor(ParcelUuid uuid) { 504 return mDescriptors.get(uuid); 505 } 506 addDescriptor(ParcelUuid uuid, int permissions)507 Descriptor addDescriptor(ParcelUuid uuid, int permissions) { 508 Descriptor desc = new Descriptor(uuid, permissions); 509 mDescriptors.put(uuid, desc); 510 return desc; 511 } 512 getDescriptors()513 Collection<Descriptor> getDescriptors() { 514 return mDescriptors.values(); 515 } 516 setValue(byte[] value)517 void setValue(byte[] value) { 518 this.mValue = value; 519 } 520 getValue()521 byte[] getValue() { 522 return mValue; 523 } 524 getUuid()525 ParcelUuid getUuid() { 526 return mUuid; 527 } 528 getProperties()529 int getProperties() { 530 return mProperties; 531 } 532 registerNotification(String client, int clientIf)533 void registerNotification(String client, int clientIf) { 534 mNotifyClients.add(client); 535 } 536 getNotifyClients()537 Set<String> getNotifyClients() { 538 return mNotifyClients; 539 } 540 } 541 542 static class Descriptor { 543 544 int mPermissions; 545 ParcelUuid mUuid; 546 byte[] mValue; 547 Descriptor(ParcelUuid uuid, int permissions)548 Descriptor(ParcelUuid uuid, int permissions) { 549 this.mUuid = uuid; 550 this.mPermissions = permissions; 551 } 552 setValue(byte[] value)553 void setValue(byte[] value) { 554 this.mValue = value; 555 } 556 getValue()557 byte[] getValue() { 558 return mValue; 559 } 560 getUuid()561 ParcelUuid getUuid() { 562 return mUuid; 563 } 564 } 565 566 @VisibleForTesting 567 static class Advertiser { 568 569 final int mClientIf; 570 final String mAddress; 571 final String mName; 572 final int mTxPowerLevel; 573 final AdvertiseData mAdvertiseData; 574 @Nullable 575 final AdvertiseData mScanResponse; 576 final AdvertiseSettings mSettings; 577 Advertiser( int clientIf, String address, String name, int txPowerLevel, AdvertiseData advertiseData, AdvertiseData scanResponse, AdvertiseSettings settings)578 Advertiser( 579 int clientIf, 580 String address, 581 String name, 582 int txPowerLevel, 583 AdvertiseData advertiseData, 584 AdvertiseData scanResponse, 585 AdvertiseSettings settings) { 586 this.mClientIf = clientIf; 587 this.mAddress = Preconditions.checkNotNull(address); 588 this.mName = name; 589 this.mTxPowerLevel = txPowerLevel; 590 this.mAdvertiseData = Preconditions.checkNotNull(advertiseData); 591 this.mScanResponse = scanResponse; 592 this.mSettings = Preconditions.checkNotNull(settings); 593 } 594 } 595 596 @VisibleForTesting 597 static class Scanner { 598 599 final int mClientIf; 600 final ScanSettings mSettings; 601 final List<ScanFilter> mFilters; 602 Scanner(int clientIf, ScanSettings settings, List<ScanFilter> filters)603 Scanner(int clientIf, ScanSettings settings, List<ScanFilter> filters) { 604 this.mClientIf = clientIf; 605 this.mSettings = Preconditions.checkNotNull(settings); 606 this.mFilters = Preconditions.checkNotNull(filters); 607 } 608 } 609 } 610