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 static org.robolectric.util.ReflectionHelpers.callConstructor; 20 21 import android.Manifest.permission; 22 import android.bluetooth.BluetoothAdapter; 23 import android.bluetooth.BluetoothClass; 24 import android.bluetooth.BluetoothDevice; 25 import android.bluetooth.BluetoothProfile; 26 import android.bluetooth.IBluetoothManager; 27 import android.content.AttributionSource; 28 import android.content.Intent; 29 import android.os.Build.VERSION; 30 import android.os.ParcelUuid; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 import com.android.libraries.testing.deviceshadower.Bluelet; 34 import com.android.libraries.testing.deviceshadower.internal.bluetooth.AdapterDelegate.Event; 35 import com.android.libraries.testing.deviceshadower.internal.bluetooth.AdapterDelegate.State; 36 import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.RfcommDelegate; 37 import com.android.libraries.testing.deviceshadower.internal.common.BroadcastManager; 38 import com.android.libraries.testing.deviceshadower.internal.common.Interrupter; 39 import com.android.libraries.testing.deviceshadower.internal.utils.Logger; 40 41 import com.google.common.base.Preconditions; 42 import com.google.common.collect.ImmutableMap; 43 44 import org.robolectric.util.ReflectionHelpers.ClassParameter; 45 46 import java.util.Arrays; 47 import java.util.HashMap; 48 import java.util.HashSet; 49 import java.util.Map; 50 import java.util.Set; 51 52 /** 53 * A container class of a real-world Bluetooth device. 54 */ 55 public class BlueletImpl implements Bluelet { 56 57 enum PairingConfirmation { 58 UNKNOWN, 59 CONFIRMED, 60 DENIED 61 } 62 63 /** 64 * See hidden {@link #EXTRA_REASON} and reason values in {@link BluetoothDevice}. 65 */ 66 static final int REASON_SUCCESS = 0; 67 /** 68 * See hidden {@link #EXTRA_REASON} and reason values in {@link BluetoothDevice}. 69 */ 70 static final int UNBOND_REASON_AUTH_FAILED = 1; 71 /** 72 * See hidden {@link #EXTRA_REASON} and reason values in {@link BluetoothDevice}. 73 */ 74 static final int UNBOND_REASON_AUTH_CANCELED = 3; 75 76 /** 77 * Hidden in {@link BluetoothDevice}. 78 */ 79 private static final String EXTRA_REASON = "android.bluetooth.device.extra.REASON"; 80 81 private static final Logger LOGGER = Logger.create("BlueletImpl"); 82 83 private static final ImmutableMap<Integer, Integer> PROFILE_STATE_TO_ADAPTER_STATE = 84 ImmutableMap.<Integer, Integer>builder() 85 .put(BluetoothProfile.STATE_CONNECTED, BluetoothAdapter.STATE_CONNECTED) 86 .put(BluetoothProfile.STATE_CONNECTING, BluetoothAdapter.STATE_CONNECTING) 87 .put(BluetoothProfile.STATE_DISCONNECTING, BluetoothAdapter.STATE_DISCONNECTING) 88 .put(BluetoothProfile.STATE_DISCONNECTED, BluetoothAdapter.STATE_DISCONNECTED) 89 .build(); 90 reset()91 public static void reset() { 92 RfcommDelegate.reset(); 93 } 94 95 public final String address; 96 String mName; 97 ParcelUuid[] mProfileUuids = new ParcelUuid[0]; 98 int mPhonebookAccessPermission; 99 int mMessageAccessPermission; 100 int mSimAccessPermission; 101 final BluetoothAdapter mAdapter; 102 int mPassKey; 103 104 private CreateBondOutcome mCreateBondOutcome = CreateBondOutcome.SUCCESS; 105 private int mCreateBondFailureReason; 106 private IoCapabilities mIoCapabilities = IoCapabilities.NO_INPUT_NO_OUTPUT; 107 private boolean mRefuseConnections; 108 private FetchUuidsTiming mFetchUuidsTiming = FetchUuidsTiming.AFTER_BONDING; 109 private boolean mEnableCVE20192225; 110 111 private final Interrupter mInterrupter; 112 private final AdapterDelegate mAdapterDelegate; 113 private final RfcommDelegate mRfcommDelegate; 114 private final GattDelegate mGattDelegate; 115 private final BluetoothBroadcastHandler mBluetoothBroadcastHandler; 116 private final Map<String, Integer> mRemoteAddressToBondState = new HashMap<>(); 117 private final Map<String, PairingConfirmation> mRemoteAddressToPairingConfirmation = 118 new HashMap<>(); 119 private final Map<Integer, Integer> mProfileTypeToConnectionState = new HashMap<>(); 120 private final Set<BluetoothDevice> mBondedDevices = new HashSet<>(); 121 BlueletImpl(String address, BroadcastManager broadcastManager)122 public BlueletImpl(String address, BroadcastManager broadcastManager) { 123 this.address = address; 124 this.mName = address; 125 this.mAdapter = callConstructor(BluetoothAdapter.class, 126 ClassParameter.from(IBluetoothManager.class, new IBluetoothManagerImpl()), 127 ClassParameter.from(AttributionSource.class, 128 AttributionSource.myAttributionSource())); 129 mBluetoothBroadcastHandler = new BluetoothBroadcastHandler(broadcastManager); 130 mInterrupter = new Interrupter(); 131 mAdapterDelegate = new AdapterDelegate(address, mBluetoothBroadcastHandler); 132 mRfcommDelegate = new RfcommDelegate(address, mBluetoothBroadcastHandler, mInterrupter); 133 mGattDelegate = new GattDelegate(address); 134 } 135 136 @Override setAdapterInitialState(int state)137 public Bluelet setAdapterInitialState(int state) throws IllegalArgumentException { 138 LOGGER.d(String.format("Address: %s, setAdapterInitialState(%d)", address, state)); 139 Preconditions.checkArgument( 140 state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_ON, 141 "State must be BluetoothAdapter.STATE_ON or BluetoothAdapter.STATE_OFF."); 142 mAdapterDelegate.setState(State.lookup(state)); 143 return this; 144 } 145 146 @Override setBluetoothClass(int bluetoothClass)147 public Bluelet setBluetoothClass(int bluetoothClass) { 148 mAdapterDelegate.setBluetoothClass(bluetoothClass); 149 return this; 150 } 151 152 @Override setScanMode(int scanMode)153 public Bluelet setScanMode(int scanMode) { 154 mAdapterDelegate.setScanMode(scanMode); 155 return this; 156 } 157 158 @Override setProfileUuids(ParcelUuid... profileUuids)159 public Bluelet setProfileUuids(ParcelUuid... profileUuids) { 160 this.mProfileUuids = profileUuids; 161 return this; 162 } 163 164 @Override setIoCapabilities(IoCapabilities ioCapabilities)165 public Bluelet setIoCapabilities(IoCapabilities ioCapabilities) { 166 this.mIoCapabilities = ioCapabilities; 167 return this; 168 } 169 170 @Override setCreateBondOutcome(CreateBondOutcome outcome, int failureReason)171 public Bluelet setCreateBondOutcome(CreateBondOutcome outcome, int failureReason) { 172 mCreateBondOutcome = outcome; 173 mCreateBondFailureReason = failureReason; 174 return this; 175 } 176 177 @Override setRefuseConnections(boolean refuse)178 public Bluelet setRefuseConnections(boolean refuse) { 179 mRefuseConnections = refuse; 180 return this; 181 } 182 183 @Override setRefuseGattConnections(boolean refuse)184 public Bluelet setRefuseGattConnections(boolean refuse) { 185 getGattDelegate().setRefuseConnections(refuse); 186 return this; 187 } 188 189 @Override setFetchUuidsTiming(FetchUuidsTiming fetchUuidsTiming)190 public Bluelet setFetchUuidsTiming(FetchUuidsTiming fetchUuidsTiming) { 191 this.mFetchUuidsTiming = fetchUuidsTiming; 192 return this; 193 } 194 195 @Override addBondedDevice(String address)196 public Bluelet addBondedDevice(String address) { 197 this.mBondedDevices.add(mAdapter.getRemoteDevice(address)); 198 return this; 199 } 200 201 @Override enableCVE20192225(boolean value)202 public Bluelet enableCVE20192225(boolean value) { 203 this.mEnableCVE20192225 = value; 204 return this; 205 } 206 getIoCapabilities()207 IoCapabilities getIoCapabilities() { 208 return mIoCapabilities; 209 } 210 getCreateBondOutcome()211 CreateBondOutcome getCreateBondOutcome() { 212 return mCreateBondOutcome; 213 } 214 getCreateBondFailureReason()215 int getCreateBondFailureReason() { 216 return mCreateBondFailureReason; 217 } 218 getRefuseConnections()219 public boolean getRefuseConnections() { 220 return mRefuseConnections; 221 } 222 getFetchUuidsTiming()223 public FetchUuidsTiming getFetchUuidsTiming() { 224 return mFetchUuidsTiming; 225 } 226 getBondedDevices()227 BluetoothDevice[] getBondedDevices() { 228 return mBondedDevices.toArray(new BluetoothDevice[0]); 229 } 230 getEnableCVE20192225()231 public boolean getEnableCVE20192225() { 232 return mEnableCVE20192225; 233 } 234 enableAdapter()235 public void enableAdapter() { 236 LOGGER.d(String.format("Address: %s, enableAdapter()", address)); 237 // TODO(b/200231384): async enabling, configurable delay, failure path 238 if (VERSION.SDK_INT < 23) { 239 mAdapterDelegate.processEvent(Event.USER_TURN_ON); 240 mAdapterDelegate.processEvent(Event.BREDR_STARTED); 241 } else { 242 mAdapterDelegate.processEvent(Event.BLE_TURN_ON); 243 mAdapterDelegate.processEvent(Event.BLE_STARTED); 244 mAdapterDelegate.processEvent(Event.USER_TURN_ON); 245 mAdapterDelegate.processEvent(Event.BREDR_STARTED); 246 } 247 } 248 disableAdapter()249 public void disableAdapter() { 250 LOGGER.d(String.format("Address: %s, disableAdapter()", address)); 251 // TODO(b/200231384): async disabling, configurable delay, failure path 252 if (VERSION.SDK_INT < 23) { 253 mAdapterDelegate.processEvent(Event.USER_TURN_OFF); 254 mAdapterDelegate.processEvent(Event.BREDR_STOPPED); 255 } else { 256 mAdapterDelegate.processEvent(Event.BLE_TURN_OFF); 257 mAdapterDelegate.processEvent(Event.BREDR_STOPPED); 258 mAdapterDelegate.processEvent(Event.USER_TURN_OFF); 259 mAdapterDelegate.processEvent(Event.BLE_STOPPED); 260 } 261 } 262 getAdapterDelegate()263 public AdapterDelegate getAdapterDelegate() { 264 return mAdapterDelegate; 265 } 266 getRfcommDelegate()267 public RfcommDelegate getRfcommDelegate() { 268 return mRfcommDelegate; 269 } 270 getGattDelegate()271 public GattDelegate getGattDelegate() { 272 return mGattDelegate; 273 } 274 getAdapter()275 public BluetoothAdapter getAdapter() { 276 return mAdapter; 277 } 278 setInterruptible(int identifier)279 public void setInterruptible(int identifier) { 280 LOGGER.d(String.format("Address: %s, setInterruptible(%d)", address, identifier)); 281 mInterrupter.setInterruptible(identifier); 282 } 283 interrupt(int identifier)284 public void interrupt(int identifier) { 285 LOGGER.d(String.format("Address: %s, interrupt(%d)", address, identifier)); 286 mInterrupter.interrupt(identifier); 287 } 288 289 @VisibleForTesting setAdapterState(int state)290 public void setAdapterState(int state) throws IllegalArgumentException { 291 State s = State.lookup(state); 292 if (s == null) { 293 throw new IllegalArgumentException(); 294 } 295 mAdapterDelegate.setState(s); 296 } 297 getBondState(String remoteAddress)298 public int getBondState(String remoteAddress) { 299 return mRemoteAddressToBondState.containsKey(remoteAddress) 300 ? mRemoteAddressToBondState.get(remoteAddress) 301 : BluetoothDevice.BOND_NONE; 302 } 303 setBondState(String remoteAddress, int bondState, int failureReason)304 public void setBondState(String remoteAddress, int bondState, int failureReason) { 305 Intent intent = 306 newDeviceIntent(BluetoothDevice.ACTION_BOND_STATE_CHANGED, remoteAddress) 307 .putExtra(BluetoothDevice.EXTRA_BOND_STATE, bondState) 308 .putExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, 309 getBondState(remoteAddress)); 310 311 if (failureReason != REASON_SUCCESS) { 312 intent.putExtra(EXTRA_REASON, failureReason); 313 } 314 315 LOGGER.d( 316 String.format( 317 "Address: %s, Bluetooth Bond State Change Intent: remote=%s, %s -> %s " 318 + "(reason=%s)", 319 address, remoteAddress, getBondState(remoteAddress), bondState, 320 failureReason)); 321 mRemoteAddressToBondState.put(remoteAddress, bondState); 322 mBluetoothBroadcastHandler.mBroadcastManager.sendBroadcast( 323 intent, android.Manifest.permission.BLUETOOTH); 324 } 325 onPairingRequest(String remoteAddress, int variant, int key)326 public void onPairingRequest(String remoteAddress, int variant, int key) { 327 Intent intent = 328 newDeviceIntent(BluetoothDevice.ACTION_PAIRING_REQUEST, remoteAddress) 329 .putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, variant) 330 .putExtra(BluetoothDevice.EXTRA_PAIRING_KEY, key); 331 332 LOGGER.d( 333 String.format( 334 "Address: %s, Bluetooth Pairing Request Intent: remote=%s, variant=%s, " 335 + "key=%s", address, remoteAddress, variant, key)); 336 mBluetoothBroadcastHandler.mBroadcastManager.sendBroadcast(intent, permission.BLUETOOTH); 337 } 338 getPairingConfirmation(String remoteAddress)339 public PairingConfirmation getPairingConfirmation(String remoteAddress) { 340 PairingConfirmation confirmation = mRemoteAddressToPairingConfirmation.get(remoteAddress); 341 return confirmation == null ? PairingConfirmation.UNKNOWN : confirmation; 342 } 343 setPairingConfirmation(String remoteAddress, PairingConfirmation confirmation)344 public void setPairingConfirmation(String remoteAddress, PairingConfirmation confirmation) { 345 mRemoteAddressToPairingConfirmation.put(remoteAddress, confirmation); 346 } 347 onFetchedUuids(String remoteAddress, ParcelUuid[] profileUuids)348 public void onFetchedUuids(String remoteAddress, ParcelUuid[] profileUuids) { 349 Intent intent = 350 newDeviceIntent(BluetoothDevice.ACTION_UUID, remoteAddress) 351 .putExtra(BluetoothDevice.EXTRA_UUID, profileUuids); 352 353 LOGGER.d( 354 String.format( 355 "Address: %s, Bluetooth Found UUIDs Intent: remoteAddress=%s, uuids=%s", 356 address, remoteAddress, Arrays.toString(profileUuids))); 357 mBluetoothBroadcastHandler.mBroadcastManager.sendBroadcast( 358 intent, android.Manifest.permission.BLUETOOTH); 359 } 360 maxProfileState(int a, int b)361 private static int maxProfileState(int a, int b) { 362 // Prefer connected > connecting > disconnecting > disconnected. 363 switch (a) { 364 case BluetoothProfile.STATE_CONNECTED: 365 return a; 366 case BluetoothProfile.STATE_CONNECTING: 367 return b == BluetoothProfile.STATE_CONNECTED ? b : a; 368 case BluetoothProfile.STATE_DISCONNECTING: 369 return b == BluetoothProfile.STATE_CONNECTED 370 || b == BluetoothProfile.STATE_CONNECTING 371 ? b 372 : a; 373 case BluetoothProfile.STATE_DISCONNECTED: 374 default: 375 return b; 376 } 377 } 378 getAdapterConnectionState()379 public int getAdapterConnectionState() { 380 int maxState = BluetoothProfile.STATE_DISCONNECTED; 381 for (int state : mProfileTypeToConnectionState.values()) { 382 maxState = maxProfileState(maxState, state); 383 } 384 return PROFILE_STATE_TO_ADAPTER_STATE.get(maxState); 385 } 386 getProfileConnectionState(int profileType)387 public int getProfileConnectionState(int profileType) { 388 return mProfileTypeToConnectionState.containsKey(profileType) 389 ? mProfileTypeToConnectionState.get(profileType) 390 : BluetoothProfile.STATE_DISCONNECTED; 391 } 392 setProfileConnectionState(int profileType, int state, String remoteAddress)393 public void setProfileConnectionState(int profileType, int state, String remoteAddress) { 394 int previousAdapterState = getAdapterConnectionState(); 395 mProfileTypeToConnectionState.put(profileType, state); 396 int adapterState = getAdapterConnectionState(); 397 if (previousAdapterState != adapterState) { 398 Intent intent = 399 newDeviceIntent(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED, remoteAddress) 400 .putExtra(BluetoothAdapter.EXTRA_PREVIOUS_CONNECTION_STATE, 401 previousAdapterState) 402 .putExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, adapterState); 403 404 LOGGER.d( 405 "Adapter Connection State Changed Intent: " 406 + previousAdapterState 407 + " -> " 408 + adapterState); 409 mBluetoothBroadcastHandler.mBroadcastManager.sendBroadcast( 410 intent, android.Manifest.permission.BLUETOOTH); 411 } 412 } 413 414 static class BluetoothBroadcastHandler implements AdapterDelegate.Callback, 415 RfcommDelegate.Callback { 416 417 private final BroadcastManager mBroadcastManager; 418 BluetoothBroadcastHandler(BroadcastManager broadcastManager)419 BluetoothBroadcastHandler(BroadcastManager broadcastManager) { 420 this.mBroadcastManager = broadcastManager; 421 } 422 423 @Override onAdapterStateChange(State prevState, State newState)424 public void onAdapterStateChange(State prevState, State newState) { 425 int prev = prevState.getValue(); 426 int cur = newState.getValue(); 427 LOGGER.d("Bluetooth State Change Intent: " + State.lookup(prev) + " -> " + State.lookup( 428 cur)); 429 Intent intent = new Intent(BluetoothAdapter.ACTION_STATE_CHANGED); 430 intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, prev); 431 intent.putExtra(BluetoothAdapter.EXTRA_STATE, cur); 432 mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH); 433 } 434 435 @Override onBleStateChange(State prevState, State newState)436 public void onBleStateChange(State prevState, State newState) { 437 int prev = prevState.getValue(); 438 int cur = newState.getValue(); 439 LOGGER.d("BLE State Change Intent: " + State.lookup(prev) + " -> " + State.lookup(cur)); 440 Intent intent = new Intent(BluetoothConstants.ACTION_BLE_STATE_CHANGED); 441 intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, prev); 442 intent.putExtra(BluetoothAdapter.EXTRA_STATE, cur); 443 mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH); 444 } 445 446 @Override onConnectionStateChange(String remoteAddress, boolean isConnected)447 public void onConnectionStateChange(String remoteAddress, boolean isConnected) { 448 LOGGER.d("Bluetooth Connection State Change Intent, isConnected: " + isConnected); 449 Intent intent = 450 isConnected 451 ? newDeviceIntent(BluetoothDevice.ACTION_ACL_CONNECTED, remoteAddress) 452 : newDeviceIntent(BluetoothDevice.ACTION_ACL_DISCONNECTED, 453 remoteAddress); 454 mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH); 455 } 456 457 @Override onDiscoveryStarted()458 public void onDiscoveryStarted() { 459 LOGGER.d("Bluetooth discovery started."); 460 Intent intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_STARTED); 461 mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH); 462 } 463 464 @Override onDiscoveryFinished()465 public void onDiscoveryFinished() { 466 LOGGER.d("Bluetooth discovery finished."); 467 Intent intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); 468 mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH); 469 } 470 471 @Override onDeviceFound(String address, int bluetoothClass, String name)472 public void onDeviceFound(String address, int bluetoothClass, String name) { 473 LOGGER.d("Bluetooth device found, address: " + address); 474 Intent intent = 475 newDeviceIntent(BluetoothDevice.ACTION_FOUND, address) 476 .putExtra( 477 BluetoothDevice.EXTRA_CLASS, 478 callConstructor( 479 BluetoothClass.class, 480 ClassParameter.from(int.class, bluetoothClass))) 481 .putExtra(BluetoothDevice.EXTRA_NAME, name); 482 // TODO(b/200231384): support rssi 483 // TODO(b/200231384): send broadcast with additional ACCESS_COARSE_LOCATION permission 484 // once broadcast permission is implemented. 485 mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH); 486 } 487 } 488 newDeviceIntent(String action, String address)489 private static Intent newDeviceIntent(String action, String address) { 490 return new Intent(action) 491 .putExtra( 492 BluetoothDevice.EXTRA_DEVICE, 493 BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address)); 494 } 495 } 496