1 /*
2  * Copyright (C) 2019 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.tradefed.util;
18 
19 import com.android.tradefed.device.DeviceNotAvailableException;
20 import com.android.tradefed.device.ITestDevice;
21 import com.android.tradefed.log.LogUtil.CLog;
22 import com.android.tradefed.util.sl4a.Sl4aClient;
23 import com.android.tradefed.util.sl4a.Sl4aEventDispatcher.EventSl4aObject;
24 
25 import com.google.common.annotations.VisibleForTesting;
26 
27 import org.json.JSONArray;
28 import org.json.JSONException;
29 import org.json.JSONObject;
30 
31 import java.io.File;
32 import java.io.IOException;
33 import java.time.Duration;
34 import java.util.HashMap;
35 import java.util.HashSet;
36 import java.util.Map;
37 import java.util.Set;
38 import java.util.stream.Collectors;
39 import java.util.stream.Stream;
40 
41 /** A utility class provides Bluetooth operations on one or two devices using SL4A */
42 public class Sl4aBluetoothUtil {
43 
44     private static final long BT_STATE_CHANGE_TIMEOUT_MS = 10000;
45     private static final long BT_PAIRING_CHECK_INTERVAL_MS = 200;
46     private static final long BT_CHECK_CONNECTION_INTERVAL_MS = 100;
47 
48     @VisibleForTesting
49     static final String BT_SNOOP_LOG_CMD_LEGACY = "setprop persist.bluetooth.btsnoopenable %s";
50 
51     @VisibleForTesting
52     static final String BT_SNOOP_LOG_CMD = "setprop persist.bluetooth.btsnooplogmode %s";
53 
54     /** Holding mappings from device serial number to the {@link Sl4aClient} of the device */
55     private Map<String, Sl4aClient> mSl4aClients = new HashMap<>();
56 
57     /** Holding mappings from {@link ITestDevice} instance to device MAC address */
58     private Map<ITestDevice, String> mAddresses = new HashMap<>();
59 
60     private Duration mBtPairTimeout = Duration.ofSeconds(25);
61 
62     private Duration mBtConnectionTimeout = Duration.ofSeconds(15);
63 
64     /** SL4A RPC commands to be used for Bluetooth operations */
65     @VisibleForTesting
66     static class Commands {
67         static final String BLUETOOTH_CHECK_STATE = "bluetoothCheckState";
68         static final String BLUETOOTH_TOGGLE_STATE = "bluetoothToggleState";
69         static final String BLUETOOTH_GET_LOCAL_ADDRESS = "bluetoothGetLocalAddress";
70         static final String BLUETOOTH_GET_BONDED_DEVICES = "bluetoothGetBondedDevices";
71         static final String BLUETOOTH_MAKE_DISCOVERABLE = "bluetoothMakeDiscoverable";
72         static final String BLUETOOTH_GET_SCAN_MODE = "bluetoothGetScanMode";
73         static final String BLUETOOTH_START_PAIRING_HELPER = "bluetoothStartPairingHelper";
74         static final String BLUETOOTH_DISCOVER_AND_BOND = "bluetoothDiscoverAndBond";
75         static final String BLUETOOTH_UNBOND = "bluetoothUnbond";
76         static final String BLUETOOTH_START_CONNECTION_STATE_CHANGE_MONITOR =
77                 "bluetoothStartConnectionStateChangeMonitor";
78         static final String BLUETOOTH_CONNECT_BONDED = "bluetoothConnectBonded";
79         static final String BLUETOOTH_DISCONNECT_CONNECTED_PROFILE =
80                 "bluetoothDisconnectConnectedProfile";
81         static final String BLUETOOTH_HFP_CLIENT_GET_CONNECTED_DEVICES =
82                 "bluetoothHfpClientGetConnectedDevices";
83         static final String BLUETOOTH_A2DP_GET_CONNECTED_DEVICES =
84                 "bluetoothA2dpGetConnectedDevices";
85         static final String BLUETOOTH_A2DP_SINK_GET_CONNECTED_DEVICES =
86                 "bluetoothA2dpSinkGetConnectedDevices";
87         static final String BLUETOOTH_PBAP_CLIENT_GET_CONNECTED_DEVICES =
88                 "bluetoothPbapClientGetConnectedDevices";
89         static final String BLUETOOTH_PAN_GET_CONNECTED_DEVICES = "bluetoothPanGetConnectedDevices";
90         static final String BLUETOOTH_MAP_GET_CONNECTED_DEVICES = "bluetoothMapGetConnectedDevices";
91         static final String BLUETOOTH_MAP_CLIENT_GET_CONNECTED_DEVICES =
92                 "bluetoothMapClientGetConnectedDevices";
93         static final String BLUETOOTH_CHANGE_PROFILE_ACCESS_PERMISSION =
94                 "bluetoothChangeProfileAccessPermission";
95         static final String BLUETOOTH_A2DP_SINK_SET_PRIORITY = "bluetoothA2dpSinkSetPriority";
96         static final String BLUETOOTH_HFP_CLIENT_SET_PRIORITY = "bluetoothHfpClientSetPriority";
97         static final String BLUETOOTH_PBAP_CLIENT_SET_PRIORITY = "bluetoothPbapClientSetPriority";
98     }
99 
100     /** SL4A events to be used for Bluetooth */
101     @VisibleForTesting
102     static class Events {
103         static final String BLUETOOTH_STATE_CHANGED_ON = "BluetoothStateChangedOn";
104         static final String BLUETOOTH_STATE_CHANGED_OFF = "BluetoothStateChangedOff";
105         static final String BLUETOOTH_PROFILE_CONNECTION_STATE_CHANGED =
106                 "BluetoothProfileConnectionStateChanged";
107     }
108 
109     /** Enums for Bluetooth profiles which are based on {@code BluetoothProfile.java} */
110     public enum BluetoothProfile {
111         HEADSET(1),
112         A2DP(2),
113         HID_HOST(4),
114         PAN(5),
115         PBAP(6),
116         GATT(7),
117         GATT_SERVER(8),
118         MAP(9),
119         SAP(10),
120         A2DP_SINK(11),
121         AVRCP_CONTROLLER(12),
122         HEADSET_CLIENT(16),
123         PBAP_CLIENT(17),
124         MAP_CLIENT(18);
125 
126         private static final Map<Integer, BluetoothProfile> sProfileToValue =
127                 Stream.of(values())
128                         .collect(Collectors.toMap(BluetoothProfile::getProfile, value -> value));
129 
130         private final int mProfile;
131 
valueOfProfile(int profile)132         public static BluetoothProfile valueOfProfile(int profile) {
133             return sProfileToValue.get(profile);
134         }
135 
BluetoothProfile(int profile)136         BluetoothProfile(int profile) {
137             mProfile = profile;
138         }
139 
getProfile()140         public int getProfile() {
141             return mProfile;
142         }
143     }
144 
145     /** Enums for Bluetooth connection states which are based on {@code BluetoothProfile.java} */
146     public enum BluetoothConnectionState {
147         DISCONNECTED(0),
148         CONNECTING(1),
149         CONNECTED(2),
150         DISCONNECTING(3);
151 
152         private final int mState;
153 
BluetoothConnectionState(int state)154         BluetoothConnectionState(int state) {
155             mState = state;
156         }
157 
getState()158         public int getState() {
159             return mState;
160         }
161     }
162 
163     /** Enums for Bluetooth device access level which are based on {@code BluetoothDevice.java} */
164     public enum BluetoothAccessLevel {
165         ACCESS_UNKNOWN(0),
166         ACCESS_ALLOWED(1),
167         ACCESS_REJECTED(2);
168 
169         private final int mAccess;
170 
BluetoothAccessLevel(int access)171         BluetoothAccessLevel(int access) {
172             mAccess = access;
173         }
174 
getAccess()175         public int getAccess() {
176             return mAccess;
177         }
178     }
179 
180     /**
181      * Enums for Bluetooth profile priority level which are based on {@code BluetoothProfile.java}
182      */
183     public enum BluetoothPriorityLevel {
184         PRIORITY_AUTO_CONNECT(1000),
185         PRIORITY_ON(100),
186         PRIORITY_OFF(0),
187         PRIORITY_UNDEFINED(-1);
188 
189         private final int mPriority;
190 
BluetoothPriorityLevel(int priority)191         BluetoothPriorityLevel(int priority) {
192             mPriority = priority;
193         }
194 
getPriority()195         public int getPriority() {
196             return mPriority;
197         }
198     }
199 
setBtPairTimeout(Duration timeout)200     public void setBtPairTimeout(Duration timeout) {
201         mBtPairTimeout = timeout;
202     }
203 
setBtConnectionTimeout(Duration timeout)204     public void setBtConnectionTimeout(Duration timeout) {
205         mBtConnectionTimeout = timeout;
206     }
207 
208     /**
209      * Explicitly start SL4A client with the given device and SL4A apk file. Normally this method is
210      * not required, because SL4A connection will always be established before actual operations.
211      *
212      * @param device the device to be connected using SL4A
213      * @param sl4aApkFile the optional SL4A apk to install and use.
214      * @throws DeviceNotAvailableException
215      */
startSl4a(ITestDevice device, File sl4aApkFile)216     public void startSl4a(ITestDevice device, File sl4aApkFile) throws DeviceNotAvailableException {
217         Sl4aClient sl4aClient = Sl4aClient.startSL4A(device, sl4aApkFile);
218         mSl4aClients.put(device.getSerialNumber(), sl4aClient);
219     }
220 
221     /**
222      * Stop SL4A clients that already being opened. It basically provide a way to cleanup clients
223      * immediately after they are no longer used
224      */
stopSl4a()225     public void stopSl4a() {
226         for (Map.Entry<String, Sl4aClient> entry : mSl4aClients.entrySet()) {
227             entry.getValue().close();
228         }
229         mSl4aClients.clear();
230     }
231 
232     @VisibleForTesting
setSl4a(ITestDevice device, Sl4aClient client)233     void setSl4a(ITestDevice device, Sl4aClient client) {
234         mSl4aClients.put(device.getSerialNumber(), client);
235     }
236 
237     /** Clean up all SL4A connections */
238     @Override
finalize()239     protected void finalize() {
240         stopSl4a();
241     }
242 
243     /**
244      * Enable Bluetooth on target device
245      *
246      * @param device target device
247      * @return true if Bluetooth successfully enabled
248      * @throws DeviceNotAvailableException
249      */
enable(ITestDevice device)250     public boolean enable(ITestDevice device) throws DeviceNotAvailableException {
251         return toggleState(device, true);
252     }
253 
254     /**
255      * Disable Bluetooth on target device
256      *
257      * @param device target device
258      * @return true if Bluetooth successfully disabled
259      * @throws DeviceNotAvailableException
260      */
disable(ITestDevice device)261     public boolean disable(ITestDevice device) throws DeviceNotAvailableException {
262         return toggleState(device, false);
263     }
264 
265     /**
266      * Get Bluetooth MAC Address of target device
267      *
268      * @param device target device
269      * @return MAC Address string
270      * @throws DeviceNotAvailableException
271      */
getAddress(ITestDevice device)272     public String getAddress(ITestDevice device) throws DeviceNotAvailableException {
273         if (mAddresses.containsKey(device)) {
274             return mAddresses.get(device);
275         }
276         Sl4aClient client = getSl4aClient(device);
277         String address = null;
278         try {
279             address = (String) client.rpcCall(Commands.BLUETOOTH_GET_LOCAL_ADDRESS);
280             mAddresses.put(device, address);
281         } catch (IOException e) {
282             CLog.e(
283                     "Failed to get Bluetooth MAC address on device: %s, %s",
284                     device.getSerialNumber(), e);
285         }
286         return address;
287     }
288 
289     /**
290      * Get set of Bluetooth MAC addresses of the bonded (paired) devices on the target device
291      *
292      * @param device target device
293      * @return Set of Bluetooth MAC addresses
294      * @throws DeviceNotAvailableException
295      */
getBondedDevices(ITestDevice device)296     public Set<String> getBondedDevices(ITestDevice device) throws DeviceNotAvailableException {
297         Set<String> addresses = new HashSet<>();
298         Sl4aClient client = getSl4aClient(device);
299         try {
300             Object response = client.rpcCall(Commands.BLUETOOTH_GET_BONDED_DEVICES);
301             if (response != null) {
302                 JSONArray bondedDevices = (JSONArray) response;
303                 for (int i = 0; i < bondedDevices.length(); i++) {
304                     JSONObject bondedDevice = bondedDevices.getJSONObject(i);
305                     if (bondedDevice.has("address")) {
306                         addresses.add(bondedDevice.getString("address"));
307                     }
308                 }
309             }
310         } catch (IOException | JSONException e) {
311             CLog.e("Failed to get bonded devices for device: %s, %s", device.getSerialNumber(), e);
312         }
313         return addresses;
314     }
315 
316     /**
317      * Pair primary device to secondary device
318      *
319      * @param primary device to pair from
320      * @param secondary device to pair to
321      * @return true if pairing is successful
322      * @throws DeviceNotAvailableException
323      */
pair(ITestDevice primary, ITestDevice secondary)324     public boolean pair(ITestDevice primary, ITestDevice secondary)
325             throws DeviceNotAvailableException {
326         Sl4aClient primaryClient = getSl4aClient(primary);
327         Sl4aClient secondaryClient = getSl4aClient(secondary);
328         try {
329             if (isPaired(primary, secondary)) {
330                 CLog.i("The two devices are already paired.");
331                 return true;
332             }
333             CLog.d("Make secondary device discoverable");
334             secondaryClient.rpcCall(Commands.BLUETOOTH_MAKE_DISCOVERABLE);
335             Integer response = (Integer) secondaryClient.rpcCall(Commands.BLUETOOTH_GET_SCAN_MODE);
336             if (response != 3) {
337                 CLog.e("Scan mode is not CONNECTABLE_DISCOVERABLE");
338                 return false;
339             }
340             CLog.d("Secondary device is made discoverable");
341 
342             CLog.d("Start pairing helper on both devices");
343             primaryClient.rpcCall(Commands.BLUETOOTH_START_PAIRING_HELPER);
344             secondaryClient.rpcCall(Commands.BLUETOOTH_START_PAIRING_HELPER);
345 
346             // Discover and bond (pair) companion device
347             CLog.d("Start discover and bond to secondary device: %s", secondary.getSerialNumber());
348             primaryClient.getEventDispatcher().clearAllEvents();
349             primaryClient.rpcCall(Commands.BLUETOOTH_DISCOVER_AND_BOND, getAddress(secondary));
350 
351             if (!waitUntilPaired(primary, secondary)) {
352                 CLog.e("Bluetooth pairing timeout");
353                 return false;
354             }
355         } catch (IOException | InterruptedException e) {
356             CLog.e("Error when pair two devices, %s", e);
357             return false;
358         }
359         CLog.i("Secondary device successfully paired");
360         return true;
361     }
362 
363     /**
364      * Un-pair all paired devices for current device
365      *
366      * @param device Current device to perform the action
367      * @return true if un-pair successfully
368      * @throws DeviceNotAvailableException
369      */
unpairAll(ITestDevice device)370     public boolean unpairAll(ITestDevice device) throws DeviceNotAvailableException {
371         Set<String> bondedDevices = getBondedDevices(device);
372         Sl4aClient client = getSl4aClient(device);
373         for (String address : bondedDevices) {
374             try {
375                 Boolean res = (Boolean) client.rpcCall(Commands.BLUETOOTH_UNBOND, address);
376                 if (!res) {
377                     CLog.w(
378                             "Failed to unpair device %s. It may not be an actual failure, instead"
379                                 + " it may be due to trying to unpair an already unpaired device."
380                                 + " This usually happens when device was first connected using LE"
381                                 + " transport where both LE address and classic address are paired"
382                                 + " and unpaired at the same time.",
383                             address);
384                 }
385             } catch (IOException e) {
386                 CLog.e("Failed to unpair all Bluetooth devices, %s", e);
387             }
388         }
389         return getBondedDevices(device).isEmpty();
390     }
391 
392     /**
393      * Connect primary device to secondary device on given Bluetooth profiles
394      *
395      * @param primary device to connect from
396      * @param secondary device to connect to
397      * @param profiles A set of Bluetooth profiles are required to be connected
398      * @return true if connection are successful
399      * @throws DeviceNotAvailableException
400      */
connect( ITestDevice primary, ITestDevice secondary, Set<BluetoothProfile> profiles)401     public boolean connect(
402             ITestDevice primary, ITestDevice secondary, Set<BluetoothProfile> profiles)
403             throws DeviceNotAvailableException {
404         if (!isPaired(primary, secondary)) {
405             CLog.e("Primary device have not yet paired to secondary device");
406             return false;
407         }
408         CLog.d("Connecting to profiles: %s", profiles);
409         Sl4aClient primaryClient = getSl4aClient(primary);
410         String address = getAddress(secondary);
411         try {
412             primaryClient.rpcCall(
413                     Commands.BLUETOOTH_START_CONNECTION_STATE_CHANGE_MONITOR, address);
414             primaryClient.rpcCall(Commands.BLUETOOTH_CONNECT_BONDED, address);
415             Set<BluetoothProfile> connectedProfiles =
416                     waitForConnectedOrDisconnectedProfiles(
417                             primary, address, BluetoothConnectionState.CONNECTED, profiles);
418             return waitForRemainingProfilesConnected(primary, address, connectedProfiles, profiles);
419         } catch (IOException | InterruptedException | JSONException e) {
420             CLog.e("Failed to connect to secondary device, %s", e);
421         }
422         return false;
423     }
424 
425     /**
426      * Disconnect primary device from secondary device
427      *
428      * @param primary device to perform disconnect operation
429      * @param secondary device to be disconnected
430      * @param profiles Given set of Bluetooth profiles required to be disconnected
431      * @return true if disconnected successfully
432      * @throws DeviceNotAvailableException
433      */
disconnect( ITestDevice primary, ITestDevice secondary, Set<BluetoothProfile> profiles)434     public boolean disconnect(
435             ITestDevice primary, ITestDevice secondary, Set<BluetoothProfile> profiles)
436             throws DeviceNotAvailableException {
437         CLog.d("Disconnecting to profiles: %s", profiles);
438         Sl4aClient primaryClient = getSl4aClient(primary);
439         String address = getAddress(secondary);
440         try {
441             primaryClient.rpcCall(
442                     Commands.BLUETOOTH_START_CONNECTION_STATE_CHANGE_MONITOR, address);
443             primaryClient.rpcCall(
444                     Commands.BLUETOOTH_DISCONNECT_CONNECTED_PROFILE,
445                     address,
446                     new JSONArray(
447                             profiles.stream()
448                                     .map(profile -> profile.getProfile())
449                                     .collect(Collectors.toList())));
450             Set<BluetoothProfile> disconnectedProfiles =
451                     waitForConnectedOrDisconnectedProfiles(
452                             primary, address, BluetoothConnectionState.DISCONNECTED, profiles);
453             return waitForRemainingProfilesDisconnected(
454                     primary, address, disconnectedProfiles, profiles);
455         } catch (IOException | JSONException | InterruptedException e) {
456             CLog.e("Failed to disconnect from secondary device, %s", e);
457         }
458         return false;
459     }
460 
461     /**
462      * Enable Bluetooth snoop log
463      *
464      * @param device to enable snoop log
465      * @return true if enabled successfully
466      * @throws DeviceNotAvailableException
467      */
enableBluetoothSnoopLog(ITestDevice device)468     public boolean enableBluetoothSnoopLog(ITestDevice device) throws DeviceNotAvailableException {
469         if (isQAndAbove(device)) {
470             device.executeShellCommand(String.format(BT_SNOOP_LOG_CMD, "full"));
471         } else {
472             device.executeShellCommand(String.format(BT_SNOOP_LOG_CMD_LEGACY, "true"));
473         }
474         return disable(device) && enable(device);
475     }
476 
477     /**
478      * Disable Bluetooth snoop log
479      *
480      * @param device to disable snoop log
481      * @return true if disabled successfully
482      * @throws DeviceNotAvailableException
483      */
disableBluetoothSnoopLog(ITestDevice device)484     public boolean disableBluetoothSnoopLog(ITestDevice device) throws DeviceNotAvailableException {
485         if (isQAndAbove(device)) {
486             device.executeShellCommand(String.format(BT_SNOOP_LOG_CMD, "disabled"));
487         } else {
488             device.executeShellCommand(String.format(BT_SNOOP_LOG_CMD_LEGACY, "false"));
489         }
490         return disable(device) && enable(device);
491     }
492 
493     /**
494      * Change Bluetooth profile access permission of secondary device on primary device in order for
495      * secondary device to access primary device on the given profile
496      *
497      * @param primary device to change permission
498      * @param secondary device that accesses primary device on the given profile
499      * @param profile Bluetooth profile to access
500      * @param access level of access, see {@code BluetoothAccessLevel}
501      * @return true if permission changed successfully
502      * @throws DeviceNotAvailableException
503      */
changeProfileAccessPermission( ITestDevice primary, ITestDevice secondary, BluetoothProfile profile, BluetoothAccessLevel access)504     public boolean changeProfileAccessPermission(
505             ITestDevice primary,
506             ITestDevice secondary,
507             BluetoothProfile profile,
508             BluetoothAccessLevel access)
509             throws DeviceNotAvailableException {
510         Sl4aClient primaryClient = getSl4aClient(primary);
511         String secondaryAddress = getAddress(secondary);
512         try {
513             primaryClient.rpcCall(
514                     Commands.BLUETOOTH_CHANGE_PROFILE_ACCESS_PERMISSION,
515                     secondaryAddress,
516                     profile.mProfile,
517                     access.getAccess());
518         } catch (IOException e) {
519             CLog.e("Failed to set profile access level %s for profile %s, %s", access, profile, e);
520             return false;
521         }
522         return true;
523     }
524 
525     /**
526      * Change priority setting of given profiles on primary device towards secondary device
527      *
528      * @param primary device to set priority on
529      * @param secondary device to set priority for
530      * @param profiles Bluetooth profiles to change priority setting
531      * @param priority level of priority
532      * @return true if set priority successfully
533      * @throws DeviceNotAvailableException
534      */
setProfilePriority( ITestDevice primary, ITestDevice secondary, Set<BluetoothProfile> profiles, BluetoothPriorityLevel priority)535     public boolean setProfilePriority(
536             ITestDevice primary,
537             ITestDevice secondary,
538             Set<BluetoothProfile> profiles,
539             BluetoothPriorityLevel priority)
540             throws DeviceNotAvailableException {
541         Sl4aClient primaryClient = getSl4aClient(primary);
542         String secondaryAddress = getAddress(secondary);
543 
544         for (BluetoothProfile profile : profiles) {
545             try {
546                 switch (profile) {
547                     case A2DP_SINK:
548                         primaryClient.rpcCall(
549                                 Commands.BLUETOOTH_A2DP_SINK_SET_PRIORITY,
550                                 secondaryAddress,
551                                 priority.getPriority());
552                         break;
553                     case HEADSET_CLIENT:
554                         primaryClient.rpcCall(
555                                 Commands.BLUETOOTH_HFP_CLIENT_SET_PRIORITY,
556                                 secondaryAddress,
557                                 priority.getPriority());
558                         break;
559                     case PBAP_CLIENT:
560                         primaryClient.rpcCall(
561                                 Commands.BLUETOOTH_PBAP_CLIENT_SET_PRIORITY,
562                                 secondaryAddress,
563                                 priority.getPriority());
564                         break;
565                     default:
566                         CLog.e("Profile %s is not yet supported for priority settings", profile);
567                         return false;
568                 }
569             } catch (IOException e) {
570                 CLog.e("Failed to set profile %s with priority %s, %s", profile, priority, e);
571                 return false;
572             }
573         }
574         return true;
575     }
576 
toggleState(ITestDevice device, boolean targetState)577     private boolean toggleState(ITestDevice device, boolean targetState)
578             throws DeviceNotAvailableException {
579         Sl4aClient client = getSl4aClient(device);
580         try {
581             boolean currentState = (Boolean) client.rpcCall(Commands.BLUETOOTH_CHECK_STATE);
582             if (currentState == targetState) {
583                 return true;
584             }
585             client.getEventDispatcher().clearAllEvents();
586             Boolean result = (Boolean) client.rpcCall(Commands.BLUETOOTH_TOGGLE_STATE, targetState);
587             if (!result) {
588                 CLog.e(
589                         "Error in sl4a when toggling %s Bluetooth state.",
590                         targetState ? "ON" : "OFF");
591                 return false;
592             }
593             String event =
594                     targetState
595                             ? Events.BLUETOOTH_STATE_CHANGED_ON
596                             : Events.BLUETOOTH_STATE_CHANGED_OFF;
597             EventSl4aObject response =
598                     client.getEventDispatcher().popEvent(event, BT_STATE_CHANGE_TIMEOUT_MS);
599             if (response == null) {
600                 CLog.e(
601                         "Get null response after toggling %s Bluetooth state.",
602                         targetState ? "ON" : "OFF");
603                 return false;
604             }
605         } catch (IOException e) {
606             CLog.e("Error when toggling %s Bluetooth state, %s", targetState ? "ON" : "OFF", e);
607             return false;
608         }
609         return true;
610     }
611 
getSl4aClient(ITestDevice device)612     private Sl4aClient getSl4aClient(ITestDevice device) throws DeviceNotAvailableException {
613         String serial = device.getSerialNumber();
614         if (!mSl4aClients.containsKey(serial)) {
615             Sl4aClient client = Sl4aClient.startSL4A(device, null);
616             mSl4aClients.put(serial, client);
617         }
618         return mSl4aClients.get(serial);
619     }
620 
waitUntilPaired(ITestDevice primary, ITestDevice secondary)621     private boolean waitUntilPaired(ITestDevice primary, ITestDevice secondary)
622             throws InterruptedException, DeviceNotAvailableException {
623         long endTime = System.currentTimeMillis() + mBtPairTimeout.toMillis();
624         while (System.currentTimeMillis() < endTime) {
625             if (isPaired(primary, secondary)) {
626                 return true;
627             }
628             Thread.sleep(BT_PAIRING_CHECK_INTERVAL_MS);
629         }
630         return false;
631     }
632 
isPaired(ITestDevice primary, ITestDevice secondary)633     private boolean isPaired(ITestDevice primary, ITestDevice secondary)
634             throws DeviceNotAvailableException {
635         return getBondedDevices(primary).contains(getAddress(secondary));
636     }
637 
waitForConnectedOrDisconnectedProfiles( ITestDevice primary, String address, BluetoothConnectionState targetState, Set<BluetoothProfile> allProfiles)638     private Set<BluetoothProfile> waitForConnectedOrDisconnectedProfiles(
639             ITestDevice primary,
640             String address,
641             BluetoothConnectionState targetState,
642             Set<BluetoothProfile> allProfiles)
643             throws DeviceNotAvailableException, JSONException {
644 
645         Set<BluetoothProfile> targetProfiles = new HashSet<>();
646         Sl4aClient primaryClient = getSl4aClient(primary);
647         // Check connection broadcast receiver
648         while (!targetProfiles.containsAll(allProfiles)) {
649             EventSl4aObject event =
650                     primaryClient
651                             .getEventDispatcher()
652                             .popEvent(
653                                     Events.BLUETOOTH_PROFILE_CONNECTION_STATE_CHANGED,
654                                     mBtConnectionTimeout.toMillis());
655             if (event == null) {
656                 CLog.w("Timeout while waiting for connection state changes for all profiles");
657                 return targetProfiles;
658             }
659             JSONObject profileData = new JSONObject(event.getData());
660             int profile = profileData.getInt("profile");
661             int state = profileData.getInt("state");
662             String actualAddress = profileData.getString("addr");
663             if (state == targetState.getState() && address.equals(actualAddress)) {
664                 targetProfiles.add(BluetoothProfile.valueOfProfile(profile));
665             }
666         }
667         return targetProfiles;
668     }
669 
waitForRemainingProfilesConnected( ITestDevice primary, String address, Set<BluetoothProfile> connectedProfiles, Set<BluetoothProfile> allProfiles)670     private boolean waitForRemainingProfilesConnected(
671             ITestDevice primary,
672             String address,
673             Set<BluetoothProfile> connectedProfiles,
674             Set<BluetoothProfile> allProfiles)
675             throws InterruptedException, DeviceNotAvailableException, JSONException, IOException {
676         long endTime = System.currentTimeMillis() + mBtConnectionTimeout.toMillis();
677         while (System.currentTimeMillis() < endTime
678                 && !connectedProfiles.containsAll(allProfiles)) {
679             for (BluetoothProfile profile : allProfiles) {
680                 if (connectedProfiles.contains(profile)) {
681                     continue;
682                 }
683                 if (isProfileConnected(primary, address, profile)) {
684                     connectedProfiles.add(profile);
685                 }
686                 CLog.d("Connected profiles for now: %s", connectedProfiles);
687             }
688             Thread.sleep(BT_CHECK_CONNECTION_INTERVAL_MS);
689         }
690         return connectedProfiles.containsAll(allProfiles);
691     }
692 
waitForRemainingProfilesDisconnected( ITestDevice primary, String address, Set<BluetoothProfile> disConnectedProfiles, Set<BluetoothProfile> allProfiles)693     private boolean waitForRemainingProfilesDisconnected(
694             ITestDevice primary,
695             String address,
696             Set<BluetoothProfile> disConnectedProfiles,
697             Set<BluetoothProfile> allProfiles)
698             throws InterruptedException, DeviceNotAvailableException, JSONException, IOException {
699         long endTime = System.currentTimeMillis() + mBtConnectionTimeout.toMillis();
700         while (System.currentTimeMillis() < endTime
701                 && !disConnectedProfiles.containsAll(allProfiles)) {
702             for (BluetoothProfile profile : allProfiles) {
703                 if (disConnectedProfiles.contains(profile)) {
704                     continue;
705                 }
706                 if (!isProfileConnected(primary, address, profile)) {
707                     disConnectedProfiles.add(profile);
708                 }
709                 CLog.d("Disconnected profiles for now: %s", disConnectedProfiles);
710             }
711             Thread.sleep(BT_CHECK_CONNECTION_INTERVAL_MS);
712         }
713         return disConnectedProfiles.containsAll(allProfiles);
714     }
715 
isProfileConnected( ITestDevice primary, String address, BluetoothProfile profile)716     private boolean isProfileConnected(
717             ITestDevice primary, String address, BluetoothProfile profile)
718             throws DeviceNotAvailableException, IOException, JSONException {
719         switch (profile) {
720             case HEADSET_CLIENT:
721                 return checkConnectedDevice(
722                         primary, address, Commands.BLUETOOTH_HFP_CLIENT_GET_CONNECTED_DEVICES);
723             case A2DP:
724                 return checkConnectedDevice(
725                         primary, address, Commands.BLUETOOTH_A2DP_GET_CONNECTED_DEVICES);
726             case A2DP_SINK:
727                 return checkConnectedDevice(
728                         primary, address, Commands.BLUETOOTH_A2DP_SINK_GET_CONNECTED_DEVICES);
729             case PAN:
730                 return checkConnectedDevice(
731                         primary, address, Commands.BLUETOOTH_PAN_GET_CONNECTED_DEVICES);
732             case PBAP_CLIENT:
733                 return checkConnectedDevice(
734                         primary, address, Commands.BLUETOOTH_PBAP_CLIENT_GET_CONNECTED_DEVICES);
735             case MAP:
736                 return checkConnectedDevice(
737                         primary, address, Commands.BLUETOOTH_MAP_GET_CONNECTED_DEVICES);
738             case MAP_CLIENT:
739                 return checkConnectedDevice(
740                         primary, address, Commands.BLUETOOTH_MAP_CLIENT_GET_CONNECTED_DEVICES);
741             default:
742                 CLog.e("Unsupported profile %s to check connection state", profile);
743         }
744         return false;
745     }
746 
checkConnectedDevice(ITestDevice primary, String address, String sl4aCommand)747     private boolean checkConnectedDevice(ITestDevice primary, String address, String sl4aCommand)
748             throws DeviceNotAvailableException, IOException, JSONException {
749         Sl4aClient primaryClient = getSl4aClient(primary);
750         JSONArray devices = (JSONArray) primaryClient.rpcCall(sl4aCommand);
751         if (devices == null) {
752             CLog.e("Empty response");
753             return false;
754         }
755         for (int i = 0; i < devices.length(); i++) {
756             JSONObject device = devices.getJSONObject(i);
757             if (device.has("address") && device.getString("address").equals(address)) {
758                 return true;
759             }
760         }
761         return false;
762     }
763 
isQAndAbove(ITestDevice device)764     private boolean isQAndAbove(ITestDevice device) throws DeviceNotAvailableException {
765         return device.getApiLevel() > 28;
766     }
767 }
768