| /* |
| * Copyright (C) 2019 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.tradefed.util; |
| |
| import com.android.tradefed.device.DeviceNotAvailableException; |
| import com.android.tradefed.device.ITestDevice; |
| import com.android.tradefed.log.LogUtil.CLog; |
| import com.android.tradefed.util.sl4a.Sl4aClient; |
| import com.android.tradefed.util.sl4a.Sl4aEventDispatcher.EventSl4aObject; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| |
| import org.json.JSONArray; |
| import org.json.JSONException; |
| import org.json.JSONObject; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.time.Duration; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| |
| /** A utility class provides Bluetooth operations on one or two devices using SL4A */ |
| public class Sl4aBluetoothUtil { |
| |
| private static final long BT_STATE_CHANGE_TIMEOUT_MS = 10000; |
| private static final long BT_PAIRING_CHECK_INTERVAL_MS = 200; |
| private static final long BT_CHECK_CONNECTION_INTERVAL_MS = 100; |
| |
| @VisibleForTesting |
| static final String BT_SNOOP_LOG_CMD_LEGACY = "setprop persist.bluetooth.btsnoopenable %s"; |
| |
| @VisibleForTesting |
| static final String BT_SNOOP_LOG_CMD = "setprop persist.bluetooth.btsnooplogmode %s"; |
| |
| /** Holding mappings from device serial number to the {@link Sl4aClient} of the device */ |
| private Map<String, Sl4aClient> mSl4aClients = new HashMap<>(); |
| |
| /** Holding mappings from {@link ITestDevice} instance to device MAC address */ |
| private Map<ITestDevice, String> mAddresses = new HashMap<>(); |
| |
| private Duration mBtPairTimeout = Duration.ofSeconds(25); |
| |
| private Duration mBtConnectionTimeout = Duration.ofSeconds(15); |
| |
| /** SL4A RPC commands to be used for Bluetooth operations */ |
| @VisibleForTesting |
| static class Commands { |
| static final String BLUETOOTH_CHECK_STATE = "bluetoothCheckState"; |
| static final String BLUETOOTH_TOGGLE_STATE = "bluetoothToggleState"; |
| static final String BLUETOOTH_GET_LOCAL_ADDRESS = "bluetoothGetLocalAddress"; |
| static final String BLUETOOTH_GET_BONDED_DEVICES = "bluetoothGetBondedDevices"; |
| static final String BLUETOOTH_MAKE_DISCOVERABLE = "bluetoothMakeDiscoverable"; |
| static final String BLUETOOTH_GET_SCAN_MODE = "bluetoothGetScanMode"; |
| static final String BLUETOOTH_START_PAIRING_HELPER = "bluetoothStartPairingHelper"; |
| static final String BLUETOOTH_DISCOVER_AND_BOND = "bluetoothDiscoverAndBond"; |
| static final String BLUETOOTH_UNBOND = "bluetoothUnbond"; |
| static final String BLUETOOTH_START_CONNECTION_STATE_CHANGE_MONITOR = |
| "bluetoothStartConnectionStateChangeMonitor"; |
| static final String BLUETOOTH_CONNECT_BONDED = "bluetoothConnectBonded"; |
| static final String BLUETOOTH_DISCONNECT_CONNECTED_PROFILE = |
| "bluetoothDisconnectConnectedProfile"; |
| static final String BLUETOOTH_HFP_CLIENT_GET_CONNECTED_DEVICES = |
| "bluetoothHfpClientGetConnectedDevices"; |
| static final String BLUETOOTH_A2DP_GET_CONNECTED_DEVICES = |
| "bluetoothA2dpGetConnectedDevices"; |
| static final String BLUETOOTH_A2DP_SINK_GET_CONNECTED_DEVICES = |
| "bluetoothA2dpSinkGetConnectedDevices"; |
| static final String BLUETOOTH_PBAP_CLIENT_GET_CONNECTED_DEVICES = |
| "bluetoothPbapClientGetConnectedDevices"; |
| static final String BLUETOOTH_PAN_GET_CONNECTED_DEVICES = "bluetoothPanGetConnectedDevices"; |
| static final String BLUETOOTH_MAP_GET_CONNECTED_DEVICES = "bluetoothMapGetConnectedDevices"; |
| static final String BLUETOOTH_MAP_CLIENT_GET_CONNECTED_DEVICES = |
| "bluetoothMapClientGetConnectedDevices"; |
| static final String BLUETOOTH_CHANGE_PROFILE_ACCESS_PERMISSION = |
| "bluetoothChangeProfileAccessPermission"; |
| static final String BLUETOOTH_A2DP_SINK_SET_PRIORITY = "bluetoothA2dpSinkSetPriority"; |
| static final String BLUETOOTH_HFP_CLIENT_SET_PRIORITY = "bluetoothHfpClientSetPriority"; |
| static final String BLUETOOTH_PBAP_CLIENT_SET_PRIORITY = "bluetoothPbapClientSetPriority"; |
| } |
| |
| /** SL4A events to be used for Bluetooth */ |
| @VisibleForTesting |
| static class Events { |
| static final String BLUETOOTH_STATE_CHANGED_ON = "BluetoothStateChangedOn"; |
| static final String BLUETOOTH_STATE_CHANGED_OFF = "BluetoothStateChangedOff"; |
| static final String BLUETOOTH_PROFILE_CONNECTION_STATE_CHANGED = |
| "BluetoothProfileConnectionStateChanged"; |
| } |
| |
| /** Enums for Bluetooth profiles which are based on {@code BluetoothProfile.java} */ |
| public enum BluetoothProfile { |
| HEADSET(1), |
| A2DP(2), |
| HID_HOST(4), |
| PAN(5), |
| PBAP(6), |
| GATT(7), |
| GATT_SERVER(8), |
| MAP(9), |
| SAP(10), |
| A2DP_SINK(11), |
| AVRCP_CONTROLLER(12), |
| HEADSET_CLIENT(16), |
| PBAP_CLIENT(17), |
| MAP_CLIENT(18); |
| |
| private static final Map<Integer, BluetoothProfile> sProfileToValue = |
| Stream.of(values()) |
| .collect(Collectors.toMap(BluetoothProfile::getProfile, value -> value)); |
| |
| private final int mProfile; |
| |
| public static BluetoothProfile valueOfProfile(int profile) { |
| return sProfileToValue.get(profile); |
| } |
| |
| BluetoothProfile(int profile) { |
| mProfile = profile; |
| } |
| |
| public int getProfile() { |
| return mProfile; |
| } |
| } |
| |
| /** Enums for Bluetooth connection states which are based on {@code BluetoothProfile.java} */ |
| public enum BluetoothConnectionState { |
| DISCONNECTED(0), |
| CONNECTING(1), |
| CONNECTED(2), |
| DISCONNECTING(3); |
| |
| private final int mState; |
| |
| BluetoothConnectionState(int state) { |
| mState = state; |
| } |
| |
| public int getState() { |
| return mState; |
| } |
| } |
| |
| /** Enums for Bluetooth device access level which are based on {@code BluetoothDevice.java} */ |
| public enum BluetoothAccessLevel { |
| ACCESS_UNKNOWN(0), |
| ACCESS_ALLOWED(1), |
| ACCESS_REJECTED(2); |
| |
| private final int mAccess; |
| |
| BluetoothAccessLevel(int access) { |
| mAccess = access; |
| } |
| |
| public int getAccess() { |
| return mAccess; |
| } |
| } |
| |
| /** |
| * Enums for Bluetooth profile priority level which are based on {@code BluetoothProfile.java} |
| */ |
| public enum BluetoothPriorityLevel { |
| PRIORITY_AUTO_CONNECT(1000), |
| PRIORITY_ON(100), |
| PRIORITY_OFF(0), |
| PRIORITY_UNDEFINED(-1); |
| |
| private final int mPriority; |
| |
| BluetoothPriorityLevel(int priority) { |
| mPriority = priority; |
| } |
| |
| public int getPriority() { |
| return mPriority; |
| } |
| } |
| |
| public void setBtPairTimeout(Duration timeout) { |
| mBtPairTimeout = timeout; |
| } |
| |
| public void setBtConnectionTimeout(Duration timeout) { |
| mBtConnectionTimeout = timeout; |
| } |
| |
| /** |
| * Explicitly start SL4A client with the given device and SL4A apk file. Normally this method is |
| * not required, because SL4A connection will always be established before actual operations. |
| * |
| * @param device the device to be connected using SL4A |
| * @param sl4aApkFile the optional SL4A apk to install and use. |
| * @throws DeviceNotAvailableException |
| */ |
| public void startSl4a(ITestDevice device, File sl4aApkFile) throws DeviceNotAvailableException { |
| Sl4aClient sl4aClient = Sl4aClient.startSL4A(device, sl4aApkFile); |
| mSl4aClients.put(device.getSerialNumber(), sl4aClient); |
| } |
| |
| /** |
| * Stop SL4A clients that already being opened. It basically provide a way to cleanup clients |
| * immediately after they are no longer used |
| */ |
| public void stopSl4a() { |
| for (Map.Entry<String, Sl4aClient> entry : mSl4aClients.entrySet()) { |
| entry.getValue().close(); |
| } |
| mSl4aClients.clear(); |
| } |
| |
| @VisibleForTesting |
| void setSl4a(ITestDevice device, Sl4aClient client) { |
| mSl4aClients.put(device.getSerialNumber(), client); |
| } |
| |
| /** Clean up all SL4A connections */ |
| @Override |
| protected void finalize() { |
| stopSl4a(); |
| } |
| |
| /** |
| * Enable Bluetooth on target device |
| * |
| * @param device target device |
| * @return true if Bluetooth successfully enabled |
| * @throws DeviceNotAvailableException |
| */ |
| public boolean enable(ITestDevice device) throws DeviceNotAvailableException { |
| return toggleState(device, true); |
| } |
| |
| /** |
| * Disable Bluetooth on target device |
| * |
| * @param device target device |
| * @return true if Bluetooth successfully disabled |
| * @throws DeviceNotAvailableException |
| */ |
| public boolean disable(ITestDevice device) throws DeviceNotAvailableException { |
| return toggleState(device, false); |
| } |
| |
| /** |
| * Get Bluetooth MAC Address of target device |
| * |
| * @param device target device |
| * @return MAC Address string |
| * @throws DeviceNotAvailableException |
| */ |
| public String getAddress(ITestDevice device) throws DeviceNotAvailableException { |
| if (mAddresses.containsKey(device)) { |
| return mAddresses.get(device); |
| } |
| Sl4aClient client = getSl4aClient(device); |
| String address = null; |
| try { |
| address = (String) client.rpcCall(Commands.BLUETOOTH_GET_LOCAL_ADDRESS); |
| mAddresses.put(device, address); |
| } catch (IOException e) { |
| CLog.e( |
| "Failed to get Bluetooth MAC address on device: %s, %s", |
| device.getSerialNumber(), e); |
| } |
| return address; |
| } |
| |
| /** |
| * Get set of Bluetooth MAC addresses of the bonded (paired) devices on the target device |
| * |
| * @param device target device |
| * @return Set of Bluetooth MAC addresses |
| * @throws DeviceNotAvailableException |
| */ |
| public Set<String> getBondedDevices(ITestDevice device) throws DeviceNotAvailableException { |
| Set<String> addresses = new HashSet<>(); |
| Sl4aClient client = getSl4aClient(device); |
| try { |
| Object response = client.rpcCall(Commands.BLUETOOTH_GET_BONDED_DEVICES); |
| if (response != null) { |
| JSONArray bondedDevices = (JSONArray) response; |
| for (int i = 0; i < bondedDevices.length(); i++) { |
| JSONObject bondedDevice = bondedDevices.getJSONObject(i); |
| if (bondedDevice.has("address")) { |
| addresses.add(bondedDevice.getString("address")); |
| } |
| } |
| } |
| } catch (IOException | JSONException e) { |
| CLog.e("Failed to get bonded devices for device: %s, %s", device.getSerialNumber(), e); |
| } |
| return addresses; |
| } |
| |
| /** |
| * Pair primary device to secondary device |
| * |
| * @param primary device to pair from |
| * @param secondary device to pair to |
| * @return true if pairing is successful |
| * @throws DeviceNotAvailableException |
| */ |
| public boolean pair(ITestDevice primary, ITestDevice secondary) |
| throws DeviceNotAvailableException { |
| Sl4aClient primaryClient = getSl4aClient(primary); |
| Sl4aClient secondaryClient = getSl4aClient(secondary); |
| try { |
| if (isPaired(primary, secondary)) { |
| CLog.i("The two devices are already paired."); |
| return true; |
| } |
| CLog.d("Make secondary device discoverable"); |
| secondaryClient.rpcCall(Commands.BLUETOOTH_MAKE_DISCOVERABLE); |
| Integer response = (Integer) secondaryClient.rpcCall(Commands.BLUETOOTH_GET_SCAN_MODE); |
| if (response != 3) { |
| CLog.e("Scan mode is not CONNECTABLE_DISCOVERABLE"); |
| return false; |
| } |
| CLog.d("Secondary device is made discoverable"); |
| |
| CLog.d("Start pairing helper on both devices"); |
| primaryClient.rpcCall(Commands.BLUETOOTH_START_PAIRING_HELPER); |
| secondaryClient.rpcCall(Commands.BLUETOOTH_START_PAIRING_HELPER); |
| |
| // Discover and bond (pair) companion device |
| CLog.d("Start discover and bond to secondary device: %s", secondary.getSerialNumber()); |
| primaryClient.getEventDispatcher().clearAllEvents(); |
| primaryClient.rpcCall(Commands.BLUETOOTH_DISCOVER_AND_BOND, getAddress(secondary)); |
| |
| if (!waitUntilPaired(primary, secondary)) { |
| CLog.e("Bluetooth pairing timeout"); |
| return false; |
| } |
| } catch (IOException | InterruptedException e) { |
| CLog.e("Error when pair two devices, %s", e); |
| return false; |
| } |
| CLog.i("Secondary device successfully paired"); |
| return true; |
| } |
| |
| /** |
| * Un-pair all paired devices for current device |
| * |
| * @param device Current device to perform the action |
| * @return true if un-pair successfully |
| * @throws DeviceNotAvailableException |
| */ |
| public boolean unpairAll(ITestDevice device) throws DeviceNotAvailableException { |
| Set<String> bondedDevices = getBondedDevices(device); |
| Sl4aClient client = getSl4aClient(device); |
| for (String address : bondedDevices) { |
| try { |
| Boolean res = (Boolean) client.rpcCall(Commands.BLUETOOTH_UNBOND, address); |
| if (!res) { |
| CLog.w( |
| "Failed to unpair device %s. It may not be an actual failure, instead" |
| + " it may be due to trying to unpair an already unpaired device." |
| + " This usually happens when device was first connected using LE" |
| + " transport where both LE address and classic address are paired" |
| + " and unpaired at the same time.", |
| address); |
| } |
| } catch (IOException e) { |
| CLog.e("Failed to unpair all Bluetooth devices, %s", e); |
| } |
| } |
| return getBondedDevices(device).isEmpty(); |
| } |
| |
| /** |
| * Connect primary device to secondary device on given Bluetooth profiles |
| * |
| * @param primary device to connect from |
| * @param secondary device to connect to |
| * @param profiles A set of Bluetooth profiles are required to be connected |
| * @return true if connection are successful |
| * @throws DeviceNotAvailableException |
| */ |
| public boolean connect( |
| ITestDevice primary, ITestDevice secondary, Set<BluetoothProfile> profiles) |
| throws DeviceNotAvailableException { |
| if (!isPaired(primary, secondary)) { |
| CLog.e("Primary device have not yet paired to secondary device"); |
| return false; |
| } |
| CLog.d("Connecting to profiles: %s", profiles); |
| Sl4aClient primaryClient = getSl4aClient(primary); |
| String address = getAddress(secondary); |
| try { |
| primaryClient.rpcCall( |
| Commands.BLUETOOTH_START_CONNECTION_STATE_CHANGE_MONITOR, address); |
| primaryClient.rpcCall(Commands.BLUETOOTH_CONNECT_BONDED, address); |
| Set<BluetoothProfile> connectedProfiles = |
| waitForConnectedOrDisconnectedProfiles( |
| primary, address, BluetoothConnectionState.CONNECTED, profiles); |
| return waitForRemainingProfilesConnected(primary, address, connectedProfiles, profiles); |
| } catch (IOException | InterruptedException | JSONException e) { |
| CLog.e("Failed to connect to secondary device, %s", e); |
| } |
| return false; |
| } |
| |
| /** |
| * Disconnect primary device from secondary device |
| * |
| * @param primary device to perform disconnect operation |
| * @param secondary device to be disconnected |
| * @param profiles Given set of Bluetooth profiles required to be disconnected |
| * @return true if disconnected successfully |
| * @throws DeviceNotAvailableException |
| */ |
| public boolean disconnect( |
| ITestDevice primary, ITestDevice secondary, Set<BluetoothProfile> profiles) |
| throws DeviceNotAvailableException { |
| CLog.d("Disconnecting to profiles: %s", profiles); |
| Sl4aClient primaryClient = getSl4aClient(primary); |
| String address = getAddress(secondary); |
| try { |
| primaryClient.rpcCall( |
| Commands.BLUETOOTH_START_CONNECTION_STATE_CHANGE_MONITOR, address); |
| primaryClient.rpcCall( |
| Commands.BLUETOOTH_DISCONNECT_CONNECTED_PROFILE, |
| address, |
| new JSONArray( |
| profiles.stream() |
| .map(profile -> profile.getProfile()) |
| .collect(Collectors.toList()))); |
| Set<BluetoothProfile> disconnectedProfiles = |
| waitForConnectedOrDisconnectedProfiles( |
| primary, address, BluetoothConnectionState.DISCONNECTED, profiles); |
| return waitForRemainingProfilesDisconnected( |
| primary, address, disconnectedProfiles, profiles); |
| } catch (IOException | JSONException | InterruptedException e) { |
| CLog.e("Failed to disconnect from secondary device, %s", e); |
| } |
| return false; |
| } |
| |
| /** |
| * Enable Bluetooth snoop log |
| * |
| * @param device to enable snoop log |
| * @return true if enabled successfully |
| * @throws DeviceNotAvailableException |
| */ |
| public boolean enableBluetoothSnoopLog(ITestDevice device) throws DeviceNotAvailableException { |
| if (isQAndAbove(device)) { |
| device.executeShellCommand(String.format(BT_SNOOP_LOG_CMD, "full")); |
| } else { |
| device.executeShellCommand(String.format(BT_SNOOP_LOG_CMD_LEGACY, "true")); |
| } |
| return disable(device) && enable(device); |
| } |
| |
| /** |
| * Disable Bluetooth snoop log |
| * |
| * @param device to disable snoop log |
| * @return true if disabled successfully |
| * @throws DeviceNotAvailableException |
| */ |
| public boolean disableBluetoothSnoopLog(ITestDevice device) throws DeviceNotAvailableException { |
| if (isQAndAbove(device)) { |
| device.executeShellCommand(String.format(BT_SNOOP_LOG_CMD, "disabled")); |
| } else { |
| device.executeShellCommand(String.format(BT_SNOOP_LOG_CMD_LEGACY, "false")); |
| } |
| return disable(device) && enable(device); |
| } |
| |
| /** |
| * Change Bluetooth profile access permission of secondary device on primary device in order for |
| * secondary device to access primary device on the given profile |
| * |
| * @param primary device to change permission |
| * @param secondary device that accesses primary device on the given profile |
| * @param profile Bluetooth profile to access |
| * @param access level of access, see {@code BluetoothAccessLevel} |
| * @return true if permission changed successfully |
| * @throws DeviceNotAvailableException |
| */ |
| public boolean changeProfileAccessPermission( |
| ITestDevice primary, |
| ITestDevice secondary, |
| BluetoothProfile profile, |
| BluetoothAccessLevel access) |
| throws DeviceNotAvailableException { |
| Sl4aClient primaryClient = getSl4aClient(primary); |
| String secondaryAddress = getAddress(secondary); |
| try { |
| primaryClient.rpcCall( |
| Commands.BLUETOOTH_CHANGE_PROFILE_ACCESS_PERMISSION, |
| secondaryAddress, |
| profile.mProfile, |
| access.getAccess()); |
| } catch (IOException e) { |
| CLog.e("Failed to set profile access level %s for profile %s, %s", access, profile, e); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Change priority setting of given profiles on primary device towards secondary device |
| * |
| * @param primary device to set priority on |
| * @param secondary device to set priority for |
| * @param profiles Bluetooth profiles to change priority setting |
| * @param priority level of priority |
| * @return true if set priority successfully |
| * @throws DeviceNotAvailableException |
| */ |
| public boolean setProfilePriority( |
| ITestDevice primary, |
| ITestDevice secondary, |
| Set<BluetoothProfile> profiles, |
| BluetoothPriorityLevel priority) |
| throws DeviceNotAvailableException { |
| Sl4aClient primaryClient = getSl4aClient(primary); |
| String secondaryAddress = getAddress(secondary); |
| |
| for (BluetoothProfile profile : profiles) { |
| try { |
| switch (profile) { |
| case A2DP_SINK: |
| primaryClient.rpcCall( |
| Commands.BLUETOOTH_A2DP_SINK_SET_PRIORITY, |
| secondaryAddress, |
| priority.getPriority()); |
| break; |
| case HEADSET_CLIENT: |
| primaryClient.rpcCall( |
| Commands.BLUETOOTH_HFP_CLIENT_SET_PRIORITY, |
| secondaryAddress, |
| priority.getPriority()); |
| break; |
| case PBAP_CLIENT: |
| primaryClient.rpcCall( |
| Commands.BLUETOOTH_PBAP_CLIENT_SET_PRIORITY, |
| secondaryAddress, |
| priority.getPriority()); |
| break; |
| default: |
| CLog.e("Profile %s is not yet supported for priority settings", profile); |
| return false; |
| } |
| } catch (IOException e) { |
| CLog.e("Failed to set profile %s with priority %s, %s", profile, priority, e); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private boolean toggleState(ITestDevice device, boolean targetState) |
| throws DeviceNotAvailableException { |
| Sl4aClient client = getSl4aClient(device); |
| try { |
| boolean currentState = (Boolean) client.rpcCall(Commands.BLUETOOTH_CHECK_STATE); |
| if (currentState == targetState) { |
| return true; |
| } |
| client.getEventDispatcher().clearAllEvents(); |
| Boolean result = (Boolean) client.rpcCall(Commands.BLUETOOTH_TOGGLE_STATE, targetState); |
| if (!result) { |
| CLog.e( |
| "Error in sl4a when toggling %s Bluetooth state.", |
| targetState ? "ON" : "OFF"); |
| return false; |
| } |
| String event = |
| targetState |
| ? Events.BLUETOOTH_STATE_CHANGED_ON |
| : Events.BLUETOOTH_STATE_CHANGED_OFF; |
| EventSl4aObject response = |
| client.getEventDispatcher().popEvent(event, BT_STATE_CHANGE_TIMEOUT_MS); |
| if (response == null) { |
| CLog.e( |
| "Get null response after toggling %s Bluetooth state.", |
| targetState ? "ON" : "OFF"); |
| return false; |
| } |
| } catch (IOException e) { |
| CLog.e("Error when toggling %s Bluetooth state, %s", targetState ? "ON" : "OFF", e); |
| return false; |
| } |
| return true; |
| } |
| |
| private Sl4aClient getSl4aClient(ITestDevice device) throws DeviceNotAvailableException { |
| String serial = device.getSerialNumber(); |
| if (!mSl4aClients.containsKey(serial)) { |
| Sl4aClient client = Sl4aClient.startSL4A(device, null); |
| mSl4aClients.put(serial, client); |
| } |
| return mSl4aClients.get(serial); |
| } |
| |
| private boolean waitUntilPaired(ITestDevice primary, ITestDevice secondary) |
| throws InterruptedException, DeviceNotAvailableException { |
| long endTime = System.currentTimeMillis() + mBtPairTimeout.toMillis(); |
| while (System.currentTimeMillis() < endTime) { |
| if (isPaired(primary, secondary)) { |
| return true; |
| } |
| Thread.sleep(BT_PAIRING_CHECK_INTERVAL_MS); |
| } |
| return false; |
| } |
| |
| private boolean isPaired(ITestDevice primary, ITestDevice secondary) |
| throws DeviceNotAvailableException { |
| return getBondedDevices(primary).contains(getAddress(secondary)); |
| } |
| |
| private Set<BluetoothProfile> waitForConnectedOrDisconnectedProfiles( |
| ITestDevice primary, |
| String address, |
| BluetoothConnectionState targetState, |
| Set<BluetoothProfile> allProfiles) |
| throws DeviceNotAvailableException, JSONException { |
| |
| Set<BluetoothProfile> targetProfiles = new HashSet<>(); |
| Sl4aClient primaryClient = getSl4aClient(primary); |
| // Check connection broadcast receiver |
| while (!targetProfiles.containsAll(allProfiles)) { |
| EventSl4aObject event = |
| primaryClient |
| .getEventDispatcher() |
| .popEvent( |
| Events.BLUETOOTH_PROFILE_CONNECTION_STATE_CHANGED, |
| mBtConnectionTimeout.toMillis()); |
| if (event == null) { |
| CLog.w("Timeout while waiting for connection state changes for all profiles"); |
| return targetProfiles; |
| } |
| JSONObject profileData = new JSONObject(event.getData()); |
| int profile = profileData.getInt("profile"); |
| int state = profileData.getInt("state"); |
| String actualAddress = profileData.getString("addr"); |
| if (state == targetState.getState() && address.equals(actualAddress)) { |
| targetProfiles.add(BluetoothProfile.valueOfProfile(profile)); |
| } |
| } |
| return targetProfiles; |
| } |
| |
| private boolean waitForRemainingProfilesConnected( |
| ITestDevice primary, |
| String address, |
| Set<BluetoothProfile> connectedProfiles, |
| Set<BluetoothProfile> allProfiles) |
| throws InterruptedException, DeviceNotAvailableException, JSONException, IOException { |
| long endTime = System.currentTimeMillis() + mBtConnectionTimeout.toMillis(); |
| while (System.currentTimeMillis() < endTime |
| && !connectedProfiles.containsAll(allProfiles)) { |
| for (BluetoothProfile profile : allProfiles) { |
| if (connectedProfiles.contains(profile)) { |
| continue; |
| } |
| if (isProfileConnected(primary, address, profile)) { |
| connectedProfiles.add(profile); |
| } |
| CLog.d("Connected profiles for now: %s", connectedProfiles); |
| } |
| Thread.sleep(BT_CHECK_CONNECTION_INTERVAL_MS); |
| } |
| return connectedProfiles.containsAll(allProfiles); |
| } |
| |
| private boolean waitForRemainingProfilesDisconnected( |
| ITestDevice primary, |
| String address, |
| Set<BluetoothProfile> disConnectedProfiles, |
| Set<BluetoothProfile> allProfiles) |
| throws InterruptedException, DeviceNotAvailableException, JSONException, IOException { |
| long endTime = System.currentTimeMillis() + mBtConnectionTimeout.toMillis(); |
| while (System.currentTimeMillis() < endTime |
| && !disConnectedProfiles.containsAll(allProfiles)) { |
| for (BluetoothProfile profile : allProfiles) { |
| if (disConnectedProfiles.contains(profile)) { |
| continue; |
| } |
| if (!isProfileConnected(primary, address, profile)) { |
| disConnectedProfiles.add(profile); |
| } |
| CLog.d("Disconnected profiles for now: %s", disConnectedProfiles); |
| } |
| Thread.sleep(BT_CHECK_CONNECTION_INTERVAL_MS); |
| } |
| return disConnectedProfiles.containsAll(allProfiles); |
| } |
| |
| private boolean isProfileConnected( |
| ITestDevice primary, String address, BluetoothProfile profile) |
| throws DeviceNotAvailableException, IOException, JSONException { |
| switch (profile) { |
| case HEADSET_CLIENT: |
| return checkConnectedDevice( |
| primary, address, Commands.BLUETOOTH_HFP_CLIENT_GET_CONNECTED_DEVICES); |
| case A2DP: |
| return checkConnectedDevice( |
| primary, address, Commands.BLUETOOTH_A2DP_GET_CONNECTED_DEVICES); |
| case A2DP_SINK: |
| return checkConnectedDevice( |
| primary, address, Commands.BLUETOOTH_A2DP_SINK_GET_CONNECTED_DEVICES); |
| case PAN: |
| return checkConnectedDevice( |
| primary, address, Commands.BLUETOOTH_PAN_GET_CONNECTED_DEVICES); |
| case PBAP_CLIENT: |
| return checkConnectedDevice( |
| primary, address, Commands.BLUETOOTH_PBAP_CLIENT_GET_CONNECTED_DEVICES); |
| case MAP: |
| return checkConnectedDevice( |
| primary, address, Commands.BLUETOOTH_MAP_GET_CONNECTED_DEVICES); |
| case MAP_CLIENT: |
| return checkConnectedDevice( |
| primary, address, Commands.BLUETOOTH_MAP_CLIENT_GET_CONNECTED_DEVICES); |
| default: |
| CLog.e("Unsupported profile %s to check connection state", profile); |
| } |
| return false; |
| } |
| |
| private boolean checkConnectedDevice(ITestDevice primary, String address, String sl4aCommand) |
| throws DeviceNotAvailableException, IOException, JSONException { |
| Sl4aClient primaryClient = getSl4aClient(primary); |
| JSONArray devices = (JSONArray) primaryClient.rpcCall(sl4aCommand); |
| if (devices == null) { |
| CLog.e("Empty response"); |
| return false; |
| } |
| for (int i = 0; i < devices.length(); i++) { |
| JSONObject device = devices.getJSONObject(i); |
| if (device.has("address") && device.getString("address").equals(address)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean isQAndAbove(ITestDevice device) throws DeviceNotAvailableException { |
| return device.getApiLevel() > 28; |
| } |
| } |