blob: ccf0ac3aa667389ad6d48b25d7630e039b8fa8e5 [file] [log] [blame]
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.libraries.testing.deviceshadower.internal.bluetooth;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.IBluetooth;
import android.bluetooth.OobData;
import android.content.AttributionSource;
import android.os.ParcelFileDescriptor;
import android.os.ParcelUuid;
import com.android.libraries.testing.deviceshadower.Bluelet.CreateBondOutcome;
import com.android.libraries.testing.deviceshadower.Bluelet.FetchUuidsTiming;
import com.android.libraries.testing.deviceshadower.Bluelet.IoCapabilities;
import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
import com.android.libraries.testing.deviceshadower.internal.bluetooth.AdapterDelegate.State;
import com.android.libraries.testing.deviceshadower.internal.bluetooth.BlueletImpl.PairingConfirmation;
import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
import com.google.common.base.Preconditions;
import java.util.Random;
/**
* Implementation of IBluetooth interface.
*/
public class IBluetoothImpl implements IBluetooth {
private static final Logger LOGGER = Logger.create("BlueletImpl");
private enum PairingVariant {
JUST_WORKS,
/**
* AKA Passkey Confirmation.
*/
NUMERIC_COMPARISON,
PASSKEY_INPUT,
CONSENT
}
/**
* User will be prompted to accept or deny the incoming pairing request.
*/
private static final int PAIRING_VARIANT_CONSENT = 3;
/**
* User will be prompted to enter the passkey displayed on remote device. This is used for
* Bluetooth 2.1 pairing.
*/
private static final int PAIRING_VARIANT_DISPLAY_PASSKEY = 4;
public IBluetoothImpl() {
}
@Override
public String getAddress() {
return localBlueletImpl().address;
}
@Override
public String getName() {
return localBlueletImpl().mName;
}
@Override
public boolean setName(String name) {
localBlueletImpl().mName = name;
return true;
}
@Override
public int getRemoteClass(BluetoothDevice device) {
return remoteBlueletImpl(device.getAddress()).getAdapterDelegate().getBluetoothClass();
}
@Override
public String getRemoteName(BluetoothDevice device) {
return remoteBlueletImpl(device.getAddress()).mName;
}
@Override
public int getRemoteType(BluetoothDevice device, AttributionSource attributionSource) {
return BluetoothDevice.DEVICE_TYPE_LE;
}
@Override
public ParcelUuid[] getRemoteUuids(BluetoothDevice device) {
return remoteBlueletImpl(device.getAddress()).mProfileUuids;
}
@Override
public boolean fetchRemoteUuids(BluetoothDevice device) {
localBlueletImpl().onFetchedUuids(device.getAddress(), getRemoteUuids(device));
return true;
}
@Override
public int getBondState(BluetoothDevice device, AttributionSource attributionSource) {
return localBlueletImpl().getBondState(device.getAddress());
}
@Override
public boolean createBond(BluetoothDevice device, int transport, OobData remoteP192Data,
OobData remoteP256Data, AttributionSource attributionSource) {
setBondState(device.getAddress(), BluetoothDevice.BOND_BONDING, BlueletImpl.REASON_SUCCESS);
BlueletImpl remoteBluelet = remoteBlueletImpl(device.getAddress());
BlueletImpl localBluelet = localBlueletImpl();
// Like the real Bluetooth stack, choose a pairing variant based on IO Capabilities.
// https://blog.bluetooth.com/bluetooth-pairing-part-2-key-generation-methods
PairingVariant variant = PairingVariant.JUST_WORKS;
if (localBluelet.getIoCapabilities() == IoCapabilities.DISPLAY_YES_NO) {
if (remoteBluelet.getIoCapabilities() == IoCapabilities.DISPLAY_YES_NO) {
variant = PairingVariant.NUMERIC_COMPARISON;
} else if (remoteBluelet.getIoCapabilities() == IoCapabilities.KEYBOARD_ONLY) {
variant = PairingVariant.PASSKEY_INPUT;
} else if (remoteBluelet.getIoCapabilities() == IoCapabilities.NO_INPUT_NO_OUTPUT
&& localBluelet.getEnableCVE20192225()) {
// After CVE-2019-2225, Bluetooth decides to ask consent instead of JustWorks.
variant = PairingVariant.CONSENT;
}
}
// Bonding doesn't complete until the passkey is confirmed on both devices. The passkey is a
// positive 6-digit integer, generated by the Bluetooth stack.
int passkey = new Random().nextInt(999999) + 1;
switch (variant) {
case NUMERIC_COMPARISON:
localBluelet.onPairingRequest(
remoteBluelet.address, BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION,
passkey);
remoteBluelet.onPairingRequest(
localBluelet.address, BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION,
passkey);
break;
case JUST_WORKS:
// Bonding completes immediately, with no PAIRING_REQUEST broadcast.
finishBonding(device);
break;
case PASSKEY_INPUT:
localBluelet.onPairingRequest(
remoteBluelet.address, PAIRING_VARIANT_DISPLAY_PASSKEY, passkey);
localBluelet.mPassKey = passkey;
remoteBluelet.onPairingRequest(
localBluelet.address, PAIRING_VARIANT_DISPLAY_PASSKEY, passkey);
break;
case CONSENT:
localBluelet.onPairingRequest(remoteBluelet.address,
PAIRING_VARIANT_CONSENT, /* key= */ 0);
if (remoteBluelet.getIoCapabilities() == IoCapabilities.NO_INPUT_NO_OUTPUT) {
remoteBluelet.setPairingConfirmation(localBluelet.address,
PairingConfirmation.CONFIRMED);
} else {
remoteBluelet.onPairingRequest(
localBluelet.address, PAIRING_VARIANT_CONSENT, /* key= */ 0);
}
break;
}
return true;
}
private void finishBonding(BluetoothDevice device) {
BlueletImpl remoteBluelet = remoteBlueletImpl(device.getAddress());
finishBonding(
device, remoteBluelet.getCreateBondOutcome(),
remoteBluelet.getCreateBondFailureReason());
}
private void finishBonding(BluetoothDevice device, CreateBondOutcome outcome,
int failureReason) {
switch (outcome) {
case SUCCESS:
setBondState(device.getAddress(), BluetoothDevice.BOND_BONDED,
BlueletImpl.REASON_SUCCESS);
break;
case FAILURE:
setBondState(device.getAddress(), BluetoothDevice.BOND_NONE, failureReason);
break;
case TIMEOUT:
// Send nothing.
break;
}
}
@Override
public boolean setPairingConfirmation(BluetoothDevice device, boolean confirmed,
AttributionSource attributionSource) {
localBlueletImpl()
.setPairingConfirmation(
device.getAddress(),
confirmed ? PairingConfirmation.CONFIRMED : PairingConfirmation.DENIED);
PairingConfirmation remoteConfirmation =
remoteBlueletImpl(device.getAddress()).getPairingConfirmation(
localBlueletImpl().address);
if (confirmed && remoteConfirmation == PairingConfirmation.CONFIRMED) {
LOGGER.d(String.format("CONFIRMED"));
finishBonding(device);
} else if (!confirmed || remoteConfirmation == PairingConfirmation.DENIED) {
LOGGER.d(String.format("NOT CONFIRMED"));
finishBonding(device, CreateBondOutcome.FAILURE, BlueletImpl.UNBOND_REASON_AUTH_FAILED);
}
return true;
}
@Override
public boolean setPasskey(BluetoothDevice device, int passkey) {
BlueletImpl remoteBluelet = remoteBlueletImpl(device.getAddress());
if (passkey == remoteBluelet.mPassKey) {
finishBonding(device);
} else {
finishBonding(device, CreateBondOutcome.FAILURE, BlueletImpl.UNBOND_REASON_AUTH_FAILED);
}
return true;
}
@Override
public boolean cancelBondProcess(BluetoothDevice device) {
finishBonding(device, CreateBondOutcome.FAILURE, BlueletImpl.UNBOND_REASON_AUTH_CANCELED);
return true;
}
@Override
public boolean removeBond(BluetoothDevice device) {
setBondState(device.getAddress(), BluetoothDevice.BOND_NONE, BlueletImpl.REASON_SUCCESS);
return true;
}
@Override
public BluetoothDevice[] getBondedDevices() {
return localBlueletImpl().getBondedDevices();
}
@Override
public int getAdapterConnectionState() {
return localBlueletImpl().getAdapterConnectionState();
}
@Override
public int getProfileConnectionState(int profile) {
return localBlueletImpl().getProfileConnectionState(profile);
}
@Override
public int getPhonebookAccessPermission(BluetoothDevice device) {
return remoteBlueletImpl(device.getAddress()).mPhonebookAccessPermission;
}
@Override
public boolean setPhonebookAccessPermission(BluetoothDevice device, int value) {
remoteBlueletImpl(device.getAddress()).mPhonebookAccessPermission = value;
return true;
}
@Override
public int getMessageAccessPermission(BluetoothDevice device) {
return remoteBlueletImpl(device.getAddress()).mMessageAccessPermission;
}
@Override
public boolean setMessageAccessPermission(BluetoothDevice device, int value) {
remoteBlueletImpl(device.getAddress()).mMessageAccessPermission = value;
return true;
}
@Override
public int getSimAccessPermission(BluetoothDevice device) {
return remoteBlueletImpl(device.getAddress()).mSimAccessPermission;
}
@Override
public boolean setSimAccessPermission(BluetoothDevice device, int value) {
remoteBlueletImpl(device.getAddress()).mSimAccessPermission = value;
return true;
}
private static void setBondState(String remoteAddress, int state, int failureReason) {
BlueletImpl remoteBluelet = remoteBlueletImpl(remoteAddress);
if (remoteBluelet.getFetchUuidsTiming() == FetchUuidsTiming.BEFORE_BONDING) {
fetchUuidsOnBondedState(remoteAddress, state);
}
remoteBluelet.setBondState(localBlueletImpl().address, state, failureReason);
localBlueletImpl().setBondState(remoteAddress, state, failureReason);
if (remoteBluelet.getFetchUuidsTiming() == FetchUuidsTiming.AFTER_BONDING) {
fetchUuidsOnBondedState(remoteAddress, state);
}
}
private static void fetchUuidsOnBondedState(String remoteAddress, int state) {
if (state == BluetoothDevice.BOND_BONDED) {
remoteBlueletImpl(remoteAddress)
.onFetchedUuids(localBlueletImpl().address, localBlueletImpl().mProfileUuids);
localBlueletImpl()
.onFetchedUuids(remoteAddress, remoteBlueletImpl(remoteAddress).mProfileUuids);
}
}
@Override
public int getScanMode() {
return localBlueletImpl().getAdapterDelegate().getScanMode();
}
@Override
public boolean setScanMode(int mode, int duration) {
localBlueletImpl().getAdapterDelegate().setScanMode(mode);
return true;
}
@Override
public int getDiscoverableTimeout() {
return -1;
}
@Override
public boolean setDiscoverableTimeout(int timeout) {
return true;
}
@Override
public boolean startDiscovery() {
localBlueletImpl().getAdapterDelegate().startDiscovery();
return true;
}
@Override
public boolean cancelDiscovery() {
localBlueletImpl().getAdapterDelegate().cancelDiscovery();
return true;
}
@Override
public boolean isDiscovering() {
return localBlueletImpl().getAdapterDelegate().isDiscovering();
}
@Override
public boolean isEnabled() {
return localBlueletImpl().getAdapterDelegate().getState().equals(State.ON);
}
@Override
public int getState() {
return localBlueletImpl().getAdapterDelegate().getState().getValue();
}
@Override
public boolean enable() {
localBlueletImpl().enableAdapter();
return true;
}
@Override
public boolean disable() {
localBlueletImpl().disableAdapter();
return true;
}
@Override
public ParcelFileDescriptor connectSocket(BluetoothDevice device, int type, ParcelUuid uuid,
int port, int flag) {
Preconditions.checkArgument(
port == BluetoothConstants.SOCKET_CHANNEL_CONNECT_WITH_UUID,
"Connect to port is not supported.");
Preconditions.checkArgument(
type == BluetoothConstants.TYPE_RFCOMM,
"Only Rfcomm socket is supported.");
return localBlueletImpl().getRfcommDelegate()
.connectSocket(device.getAddress(), uuid.getUuid());
}
@Override
public ParcelFileDescriptor createSocketChannel(int type, String serviceName, ParcelUuid uuid,
int port, int flag) {
Preconditions.checkArgument(
port == BluetoothConstants.SERVER_SOCKET_CHANNEL_AUTO_ASSIGN,
"Listen on port is not supported.");
Preconditions.checkArgument(
type == BluetoothConstants.TYPE_RFCOMM,
"Only Rfcomm socket is supported.");
return localBlueletImpl().getRfcommDelegate().createSocketChannel(serviceName, uuid);
}
@Override
public boolean isMultiAdvertisementSupported() {
return maxAdvertiseInstances() > 1;
}
@Override
public boolean isPeripheralModeSupported() {
return maxAdvertiseInstances() > 0;
}
private int maxAdvertiseInstances() {
return localBlueletImpl().getGattDelegate().getMaxAdvertiseInstances();
}
@Override
public boolean isOffloadedFilteringSupported() {
return localBlueletImpl().getGattDelegate().isOffloadedFilteringSupported();
}
private static BlueletImpl localBlueletImpl() {
return DeviceShadowEnvironmentImpl.getLocalBlueletImpl();
}
private static BlueletImpl remoteBlueletImpl(String address) {
return DeviceShadowEnvironmentImpl.getBlueletImpl(address);
}
}