• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""AndroidBluetoothDecorator class.
2
3This decorator is used for giving an AndroidDevice Bluetooth-specific
4functionality.
5"""
6
7import datetime
8import logging
9import os
10import queue
11import random
12import re
13import string
14import time
15from typing import Dict, Any, Text, Optional, Tuple, Sequence, Union, List
16
17from mobly import logger as mobly_logger
18from mobly import signals
19from mobly import utils
20from mobly.controllers import android_device
21from mobly.controllers.android_device_lib import adb
22from mobly.controllers.android_device_lib import jsonrpc_client_base
23from mobly.controllers.android_device_lib.services import sl4a_service
24
25from blueberry.controllers import derived_bt_device
26from blueberry.utils import bt_constants
27from blueberry.utils import ble_scan_adv_constants
28from blueberry.utils import bt_test_utils
29
30# Map for media passthrough commands and the corresponding events.
31MEDIA_CMD_MAP = {
32    bt_constants.CMD_MEDIA_PAUSE: bt_constants.EVENT_PAUSE_RECEIVED,
33    bt_constants.CMD_MEDIA_PLAY: bt_constants.EVENT_PLAY_RECEIVED,
34    bt_constants.CMD_MEDIA_SKIP_PREV: bt_constants.EVENT_SKIP_PREV_RECEIVED,
35    bt_constants.CMD_MEDIA_SKIP_NEXT: bt_constants.EVENT_SKIP_NEXT_RECEIVED
36}
37
38# Timeout for track change and playback state update in second.
39MEDIA_UPDATE_TIMEOUT_SEC = 3
40
41# Timeout for the event of Media passthrough commands in second.
42MEDIA_EVENT_TIMEOUT_SEC = 1
43
44BT_CONNECTION_WAITING_TIME_SECONDS = 10
45
46ADB_WAITING_TIME_SECONDS = 1
47
48# Common timeout for toggle status in seconds.
49COMMON_TIMEOUT_SECONDS = 5
50
51# Local constant
52_DATETIME_FMT = '%m-%d %H:%M:%S.%f'
53
54# Interval time between ping requests in second.
55PING_INTERVAL_TIME_SEC = 2
56
57# Timeout to wait for ping success in second.
58PING_TIMEOUT_SEC = 60
59
60# A URL is used to verify internet by ping request.
61TEST_URL = 'http://www.google.com'
62
63# Timeout to wait for device boot success in second.
64WAIT_FOR_DEVICE_TIMEOUT_SEC = 180
65
66
67class DeviceBootError(signals.ControllerError):
68    """Exception raised for Android device boot failures."""
69    pass
70
71
72class Error(Exception):
73    """Raised when an operation in this module fails."""
74    pass
75
76
77class DiscoveryError(signals.ControllerError):
78    """Exception raised for Bluetooth device discovery failures."""
79    pass
80
81
82class AndroidBluetoothDecorator(android_device.AndroidDevice):
83    """Decorates an AndroidDevice with Bluetooth-specific functionality."""
84
85    def __init__(self, ad: android_device.AndroidDevice):
86        self._ad = ad
87        self._user_params = None
88        if not self._ad or not isinstance(self._ad, android_device.AndroidDevice):
89            raise TypeError('Must apply AndroidBluetoothDecorator to an ' 'AndroidDevice')
90        self.ble_advertise_callback = None
91        self.regex_logcat_time = re.compile(r'(?P<datetime>[\d]{2}-[\d]{2} [\d]{2}:[\d]{2}:[\d]{2}.[\d]{3})'
92                                            r'[ ]+\d+.*')
93        self._regex_bt_crash = re.compile(r'Bluetooth crashed (?P<num_bt_crashes>\d+) times')
94
95    def __getattr__(self, name: Any) -> Any:
96        return getattr(self._ad, name)
97
98    def _is_device_connected(self, mac_address):
99        """Wrapper method to help with unit testability of this class."""
100        return self._ad.sl4a.bluetoothIsDeviceConnected(mac_address)
101
102    def _is_profile_connected(self, mac_address, profile):
103        """Checks if the profile is connected."""
104        status = None
105        pri_ad = self._ad
106        if profile == bt_constants.BluetoothProfile.HEADSET_CLIENT:
107            status = pri_ad.sl4a.bluetoothHfpClientGetConnectionStatus(mac_address)
108        elif profile == bt_constants.BluetoothProfile.A2DP_SINK:
109            status = pri_ad.sl4a.bluetoothA2dpSinkGetConnectionStatus(mac_address)
110        elif profile == bt_constants.BluetoothProfile.PBAP_CLIENT:
111            status = pri_ad.sl4a.bluetoothPbapClientGetConnectionStatus(mac_address)
112        elif profile == bt_constants.BluetoothProfile.MAP_MCE:
113            connected_devices = self._ad.sl4a.bluetoothMapClientGetConnectedDevices()
114            return any(mac_address in device['address'] for device in connected_devices)
115        else:
116            pri_ad.log.warning('The connection check for profile %s is not supported ' 'yet', profile)
117            return False
118        return status == bt_constants.BluetoothConnectionStatus.STATE_CONNECTED
119
120    def _get_bluetooth_le_state(self):
121        """Wrapper method to help with unit testability of this class."""
122        return self._ad.sl4a.bluetoothGetLeState
123
124    def _generate_id_by_size(self, size):
125        """Generate string of random ascii letters and digits.
126
127    Args:
128      size: required size of string.
129
130    Returns:
131      String of random chars.
132    """
133        return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(size))
134
135    def _wait_for_bluetooth_manager_state(self, state=None, timeout=10, threshold=5):
136        """Waits for Bluetooth normalized state or normalized explicit state.
137
138    Args:
139      state: expected Bluetooth state
140      timeout: max timeout threshold
141      threshold: list len of bt state
142    Returns:
143      True if successful, false if unsuccessful.
144    """
145        all_states = []
146        start_time = time.time()
147        while time.time() < start_time + timeout:
148            all_states.append(self._get_bluetooth_le_state())
149            if len(all_states) >= threshold:
150                # for any normalized state
151                if state is None:
152                    if len(all_states[-threshold:]) == 1:
153                        logging.info('State normalized %s', all_states[-threshold:])
154                        return True
155                else:
156                    # explicit check against normalized state
157                    if state in all_states[-threshold:]:
158                        return True
159            time.sleep(0.5)
160        logging.error('Bluetooth state fails to normalize'
161                      if state is None else 'Failed to match bluetooth state, current state {} expected state {}'
162                      .format(self._get_bluetooth_le_state(), state))
163        return False
164
165    def init_setup(self) -> None:
166        """Sets up android device for bluetooth tests."""
167        self._ad.services.register('sl4a', sl4a_service.Sl4aService)
168        self._ad.load_snippet('mbs', 'com.google.android.mobly.snippet.bundled')
169        self._ad.adb.shell('setenforce 0')
170
171        # Adds 2 seconds waiting time to see it can fix the NullPointerException
172        # when executing the following sl4a.bluetoothStartPairingHelper method.
173        time.sleep(2)
174        self._ad.sl4a.bluetoothStartPairingHelper()
175        self.factory_reset_bluetooth()
176
177    def sl4a_setup(self) -> None:
178        """A common setup routine for android device sl4a function.
179
180    Things this method setup:
181    1. Set Bluetooth local name to random string of size 4
182    2. Disable BLE background scanning.
183    """
184
185        sl4a = self._ad.sl4a
186        sl4a.bluetoothStartConnectionStateChangeMonitor('')
187        setup_result = sl4a.bluetoothSetLocalName(self._generate_id_by_size(4))
188        if not setup_result:
189            self.log.error('Failed to set device name.')
190            return
191        sl4a.bluetoothDisableBLE()
192        bonded_devices = sl4a.bluetoothGetBondedDevices()
193        for b in bonded_devices:
194            self.log.info('Removing bond for device {}'.format(b['address']))
195            sl4a.bluetoothUnbond(b['address'])
196
197    def set_user_params(self, params: Dict[str, Any]) -> None:
198        self._user_params = params
199
200    def get_user_params(self) -> Dict[str, Any]:
201        return self._user_params
202
203    def is_sim_state_loaded(self) -> bool:
204        """Checks if SIM state is loaded.
205
206    Returns:
207      True if SIM state is loaded else False.
208    """
209        state = self._ad.adb.shell('getprop gsm.sim.state').decode().strip()
210        return state == 'LOADED'
211
212    def is_package_installed(self, package_name: str) -> bool:
213        """Checks if a package is installed.
214
215    Args:
216      package_name: string, a package to be checked.
217
218    Returns:
219      True if the package is installed else False.
220    """
221        # The package is installed if result is 1, not installed if result is 0.
222        result = int(self._ad.adb.shell('pm list packages | grep -i %s$ | wc -l' % package_name))
223        return bool(result)
224
225    def connect_with_rfcomm(self, other_ad: android_device.AndroidDevice) -> bool:
226        """Establishes an RFCOMM connection with other android device.
227
228    Connects this android device (as a client) to the other android device
229    (as a server).
230
231    Args:
232      other_ad: the Android device accepting the connection from this device.
233
234    Returns:
235        True if connection was successful, False if unsuccessful.
236    """
237        server_address = other_ad.sl4a.bluetoothGetLocalAddress()
238        logging.info('Pairing and connecting devices')
239        if not self._ad.sl4a.bluetoothDiscoverAndBond(server_address):
240            logging.info('Failed to pair and connect devices')
241            return False
242
243        # Create RFCOMM connection
244        logging.info('establishing RFCOMM connection')
245        return self.orchestrate_rfcomm_connection(other_ad)
246
247    def orchestrate_rfcomm_connection(self,
248                                      other_ad: android_device.AndroidDevice,
249                                      accept_timeout_ms: int = bt_constants.DEFAULT_RFCOMM_TIMEOUT_MS,
250                                      uuid: Optional[Text] = None) -> bool:
251        """Sets up the RFCOMM connection to another android device.
252
253    It sets up the connection with a Bluetooth Socket connection with other
254    device.
255
256    Args:
257        other_ad: the Android device accepting the connection from this device.
258        accept_timeout_ms: the timeout in ms for the connection.
259        uuid: universally unique identifier.
260
261    Returns:
262        True if connection was successful, False if unsuccessful.
263    """
264        if uuid is None:
265            uuid = bt_constants.BT_RFCOMM_UUIDS['default_uuid']
266        other_ad.sl4a.bluetoothStartPairingHelper()
267        self._ad.sl4a.bluetoothStartPairingHelper()
268        other_ad.sl4a.bluetoothSocketConnBeginAcceptThreadUuid(uuid, accept_timeout_ms)
269        self._ad.sl4a.bluetoothSocketConnBeginConnectThreadUuid(other_ad.sl4a.bluetoothGetLocalAddress(), uuid)
270
271        end_time = time.time() + bt_constants.BT_DEFAULT_TIMEOUT_SECONDS
272        test_result = True
273
274        while time.time() < end_time:
275            number_socket_connections = len(other_ad.sl4a.bluetoothSocketConnActiveConnections())
276            connected = number_socket_connections > 0
277            if connected:
278                test_result = True
279                other_ad.log.info('Bluetooth socket Client Connection Active')
280                break
281            else:
282                test_result = False
283            time.sleep(1)
284        if not test_result:
285            other_ad.log.error('Failed to establish a Bluetooth socket connection')
286            return False
287        return True
288
289    def wait_for_discovery_success(self, mac_address: str, timeout: float = 30) -> float:
290        """Waits for a device to be discovered by AndroidDevice.
291
292    Args:
293      mac_address: The Bluetooth mac address of the peripheral device.
294      timeout: Number of seconds to wait for device discovery.
295
296    Returns:
297      discovery_time: The time it takes to pair in seconds.
298
299    Raises:
300      DiscoveryError
301    """
302        device_start_time = self.get_device_time()
303        start_time = time.time()
304        event_name = f'Discovery{mac_address}'
305        try:
306            self._ad.ed.wait_for_event(event_name, lambda x: x['data']['Status'], timeout)
307            discovery_time = time.time() - start_time
308            return discovery_time
309
310        except queue.Empty:
311            # TODO(user): Remove this check when this bug is fixed.
312            if self.logcat_filter(device_start_time, event_name):
313                self._ad.log.info('Actually the event "%s" was posted within %d seconds.', event_name, timeout)
314                return timeout
315            raise DiscoveryError('Failed to discover device %s after %d seconds' % (mac_address, timeout))
316
317    def wait_for_pairing_success(self, mac_address: str, timeout: float = 30) -> float:
318        """Waits for a device to pair with the AndroidDevice.
319
320    Args:
321      mac_address: The Bluetooth mac address of the peripheral device.
322      timeout: Number of seconds to wait for the devices to pair.
323
324    Returns:
325      pairing_time: The time it takes to pair in seconds.
326
327    Raises:
328      ControllerError
329    """
330        start_time = time.time()
331        try:
332            self._ad.ed.wait_for_event('Bond%s' % mac_address, lambda x: x['data']['Status'], timeout)
333            pairing_time = time.time() - start_time
334            return pairing_time
335
336        except queue.Empty:
337            raise signals.ControllerError('Failed to bond with device %s after %d seconds' % (mac_address, timeout))
338
339    def wait_for_connection_success(self, mac_address: str, timeout: int = 30) -> float:
340        """Waits for a device to connect with the AndroidDevice.
341
342    Args:
343      mac_address: The Bluetooth mac address of the peripheral device.
344      timeout: Number of seconds to wait for the devices to connect.
345
346    Returns:
347      connection_time: The time it takes to connect in seconds.
348
349    Raises:
350      ControllerError
351    """
352        start_time = time.time()
353        end_time = start_time + timeout
354        while time.time() < end_time:
355            if self._is_device_connected(mac_address):
356                connection_time = (time.time() - start_time)
357                logging.info('Connected device %s in %d seconds', mac_address, connection_time)
358                return connection_time
359
360        raise signals.ControllerError('Failed to connect device within %d seconds.' % timeout)
361
362    def factory_reset_bluetooth(self) -> None:
363        """Factory resets Bluetooth on an AndroidDevice."""
364
365        logging.info('Factory resetting Bluetooth for AndroidDevice.')
366        self._ad.sl4a.bluetoothToggleState(True)
367        paired_devices = self._ad.mbs.btGetPairedDevices()
368        for device in paired_devices:
369            self._ad.sl4a.bluetoothUnbond(device['Address'])
370        self._ad.sl4a.bluetoothFactoryReset()
371        self._wait_for_bluetooth_manager_state()
372        self.wait_for_bluetooth_toggle_state(True)
373
374    def get_device_info(self) -> Dict[str, Any]:
375        """Gets the configuration info of an AndroidDevice.
376
377    Returns:
378      dict, A dictionary mapping metric keys to their respective values.
379    """
380
381        device_info = {
382            'device_class': 'AndroidDevice',
383            'device_model': self._ad.device_info['model'],
384            'hardware_version': self._ad.adb.getprop('ro.boot.hardware.revision'),
385            'software_version': self._ad.build_info['build_id'],
386            'android_build_type': self._ad.build_info['build_type'],
387            'android_build_number': self._ad.adb.getprop('ro.build.version.incremental'),
388            'android_release_id': self._ad.build_info['build_id']
389        }
390
391        return device_info
392
393    def pair_and_connect_bluetooth(self, mac_address: str, attempts: int = 3,
394                                   enable_pairing_retry: bool = True) -> Tuple[float, float]:
395        """Pairs and connects an AndroidDevice with a peripheral Bluetooth device.
396
397    Ensures that an AndroidDevice is paired and connected to a peripheral
398    device. If the devices are already connected, does nothing. If
399    the devices are paired but not connected, connects the devices. If the
400    devices are neither paired nor connected, this method pairs and connects the
401    devices.
402
403    Suggests to use the retry mechanism on Discovery because it sometimes fail
404    even if the devices are testing in shielding. In order to avoid the remote
405    device may not respond a incoming pairing request causing to bonding failure
406    , it suggests to retry pairing too.
407
408    Args:
409      mac_address: The Bluetooth mac address of the peripheral device.
410      attempts: Number of attempts to discover and pair the peripheral device.
411      enable_pairing_retry: Bool to control whether the retry mechanism is used
412          on bonding failure, it's enabled if True.
413
414    Returns:
415      pairing_time: The time, in seconds, it takes to pair the devices.
416      connection_time: The time, in seconds, it takes to connect the
417      devices after pairing is completed.
418
419    Raises:
420      DiscoveryError: Raised if failed to discover the peripheral device.
421      ControllerError: Raised if failed to bond the peripheral device.
422    """
423
424        connected = self._is_device_connected(mac_address)
425        pairing_time = 0
426        connection_time = 0
427        if connected:
428            logging.info('Device %s already paired and connected', mac_address)
429            return pairing_time, connection_time
430
431        paired_devices = [device['address'] for device in self._ad.sl4a.bluetoothGetBondedDevices()]
432        if mac_address in paired_devices:
433            self._ad.sl4a.bluetoothConnectBonded(mac_address)
434            return pairing_time, self.wait_for_connection_success(mac_address)
435
436        logging.info('Initiate pairing to the device "%s".', mac_address)
437        for i in range(attempts):
438            self._ad.sl4a.bluetoothDiscoverAndBond(mac_address)
439            try:
440                self.wait_for_discovery_success(mac_address)
441                pairing_time = self.wait_for_pairing_success(mac_address)
442                break
443            except DiscoveryError:
444                if i + 1 < attempts:
445                    logging.error('Failed to find the device "%s" on Attempt %d. '
446                                  'Retrying discovery...', mac_address, i + 1)
447                    continue
448                raise DiscoveryError('Failed to find the device "%s".' % mac_address)
449            except signals.ControllerError:
450                if i + 1 < attempts and enable_pairing_retry:
451                    logging.error('Failed to bond the device "%s" on Attempt %d. '
452                                  'Retrying pairing...', mac_address, i + 1)
453                    continue
454                raise signals.ControllerError('Failed to bond the device "%s".' % mac_address)
455
456        connection_time = self.wait_for_connection_success(mac_address)
457        return pairing_time, connection_time
458
459    def disconnect_bluetooth(self, mac_address: str, timeout: float = 30) -> float:
460        """Disconnects Bluetooth between an AndroidDevice and peripheral device.
461
462    Args:
463      mac_address: The Bluetooth mac address of the peripheral device.
464      timeout: Number of seconds to wait for the devices to disconnect the
465      peripheral device.
466
467    Returns:
468      disconnection_time: The time, in seconds, it takes to disconnect the
469      peripheral device.
470
471    Raises:
472      ControllerError: Raised if failed to disconnect the peripheral device.
473    """
474        if not self._is_device_connected(mac_address):
475            logging.info('Device %s already disconnected', mac_address)
476            return 0
477
478        self._ad.sl4a.bluetoothDisconnectConnected(mac_address)
479        start_time = time.time()
480        end_time = time.time() + timeout
481        while time.time() < end_time:
482            connected = self._is_device_connected(mac_address)
483            if not connected:
484                logging.info('Device %s disconnected successfully.', mac_address)
485                return time.time() - start_time
486
487        raise signals.ControllerError('Failed to disconnect device within %d seconds.' % timeout)
488
489    def connect_bluetooth(self, mac_address: str, timeout: float = 30) -> float:
490        """Connects Bluetooth between an AndroidDevice and peripheral device.
491
492    Args:
493      mac_address: The Bluetooth mac address of the peripheral device.
494      timeout: Number of seconds to wait for the devices to connect the
495      peripheral device.
496
497    Returns:
498      connection_time: The time, in seconds, it takes to connect the
499      peripheral device.
500
501    Raises:
502      ControllerError: Raised if failed to connect the peripheral device.
503    """
504        if self._is_device_connected(mac_address):
505            logging.info('Device %s already connected', mac_address)
506            return 0
507
508        self._ad.sl4a.bluetoothConnectBonded(mac_address)
509        connect_time = self.wait_for_connection_success(mac_address)
510
511        return connect_time
512
513    def activate_pairing_mode(self) -> None:
514        """Activates pairing mode on an AndroidDevice."""
515        logging.info('Activating pairing mode on AndroidDevice.')
516        self._ad.sl4a.bluetoothMakeDiscoverable()
517        self._ad.sl4a.bluetoothStartPairingHelper()
518
519    def activate_ble_pairing_mode(self) -> None:
520        """Activates BLE pairing mode on an AndroidDevice."""
521        self.ble_advertise_callback = self._ad.sl4a.bleGenBleAdvertiseCallback()
522        self._ad.sl4a.bleSetAdvertiseDataIncludeDeviceName(True)
523        # Sets advertise mode to low latency.
524        self._ad.sl4a.bleSetAdvertiseSettingsAdvertiseMode(ble_scan_adv_constants.BleAdvertiseSettingsMode.LOW_LATENCY)
525        self._ad.sl4a.bleSetAdvertiseSettingsIsConnectable(True)
526        # Sets TX power level to High.
527        self._ad.sl4a.bleSetAdvertiseSettingsTxPowerLevel(ble_scan_adv_constants.BleAdvertiseSettingsTxPower.HIGH)
528        advertise_data = self._ad.sl4a.bleBuildAdvertiseData()
529        advertise_settings = self._ad.sl4a.bleBuildAdvertiseSettings()
530        logging.info('Activating BLE pairing mode on AndroidDevice.')
531        self._ad.sl4a.bleStartBleAdvertising(self.ble_advertise_callback, advertise_data, advertise_settings)
532
533    def deactivate_ble_pairing_mode(self) -> None:
534        """Deactivates BLE pairing mode on an AndroidDevice."""
535        if not self.ble_advertise_callback:
536            self._ad.log.debug('BLE pairing mode is not activated.')
537            return
538        logging.info('Deactivating BLE pairing mode on AndroidDevice.')
539        self._ad.sl4a.bleStopBleAdvertising(self.ble_advertise_callback)
540        self.ble_advertise_callback = None
541
542    def get_bluetooth_mac_address(self) -> str:
543        """Gets Bluetooth mac address of an AndroidDevice."""
544        logging.info('Getting Bluetooth mac address for AndroidDevice.')
545        mac_address = self._ad.sl4a.bluetoothGetLocalAddress()
546        logging.info('Bluetooth mac address of AndroidDevice: %s', mac_address)
547        return mac_address
548
549    def scan_and_get_ble_device_address(self, device_name: str, timeout_sec: float = 30) -> str:
550        """Searchs a BLE device by BLE scanner and returns it's BLE mac address.
551
552    Args:
553      device_name: string, the name of BLE device.
554      timeout_sec: int, number of seconds to wait for finding the advertisement.
555
556    Returns:
557      String of the BLE mac address.
558
559    Raises:
560      ControllerError: Raised if failed to get the BLE device address
561    """
562        filter_list = self._ad.sl4a.bleGenFilterList()
563        scan_settings = self._ad.sl4a.bleBuildScanSetting()
564        scan_callback = self._ad.sl4a.bleGenScanCallback()
565        self._ad.sl4a.bleSetScanFilterDeviceName(device_name)
566        self._ad.sl4a.bleBuildScanFilter(filter_list)
567        self._ad.sl4a.bleStartBleScan(filter_list, scan_settings, scan_callback)
568        try:
569            event = self._ad.ed.pop_event('BleScan%sonScanResults' % scan_callback, timeout_sec)
570        except queue.Empty:
571            raise signals.ControllerError(
572                'Timed out %ds after waiting for phone finding BLE device: %s.' % (timeout_sec, device_name))
573        finally:
574            self._ad.sl4a.bleStopBleScan(scan_callback)
575        return event['data']['Result']['deviceInfo']['address']
576
577    def get_device_name(self) -> str:
578        """Gets Bluetooth device name of an AndroidDevice."""
579        logging.info('Getting Bluetooth device name for AndroidDevice.')
580        device_name = self._ad.sl4a.bluetoothGetLocalName()
581        logging.info('Bluetooth device name of AndroidDevice: %s', device_name)
582        return device_name
583
584    def is_bluetooth_sco_on(self) -> bool:
585        """Checks whether communications use Bluetooth SCO."""
586        cmd = 'dumpsys bluetooth_manager | grep "isBluetoothScoOn"'
587        get_status = self._ad.adb.shell(cmd)
588        if isinstance(get_status, bytes):
589            get_status = get_status.decode()
590        return 'true' in get_status
591
592    def connect_with_profile(self, snd_ad_mac_address: str, profile: bt_constants.BluetoothProfile) -> bool:
593        """Connects with the profile.
594
595    The connection can only be completed after the bluetooth devices are paired.
596    To connected with the profile, the bluetooth connection policy is set to
597    forbidden first and then set to allowed. The paired bluetooth devices will
598    start to make connection. The connection time could be long. The waitting
599    time is set to BT_CONNECTION_WAITING_TIME_SECONDS (currently 10 seconds).
600
601    Args:
602      snd_ad_mac_address: the mac address of the device accepting connection.
603      profile: the profiles to be set
604
605    Returns:
606      The profile connection succeed/fail
607    """
608        if profile == bt_constants.BluetoothProfile.MAP_MCE:
609            self._ad.sl4a.bluetoothMapClientConnect(snd_ad_mac_address)
610        elif profile == bt_constants.BluetoothProfile.PBAP_CLIENT:
611            self.set_profile_policy(snd_ad_mac_address, profile,
612                                    bt_constants.BluetoothConnectionPolicy.CONNECTION_POLICY_ALLOWED)
613            self._ad.sl4a.bluetoothPbapClientConnect(snd_ad_mac_address)
614        else:
615            self.set_profile_policy(snd_ad_mac_address, profile,
616                                    bt_constants.BluetoothConnectionPolicy.CONNECTION_POLICY_FORBIDDEN)
617            self.set_profile_policy(snd_ad_mac_address, profile,
618                                    bt_constants.BluetoothConnectionPolicy.CONNECTION_POLICY_ALLOWED)
619            self._ad.sl4a.bluetoothConnectBonded(snd_ad_mac_address)
620        time.sleep(BT_CONNECTION_WAITING_TIME_SECONDS)
621        is_connected = self._is_profile_connected(snd_ad_mac_address, profile)
622        self.log.info('The connection between %s and %s for profile %s succeed: %s', self.serial, snd_ad_mac_address,
623                      profile, is_connected)
624        return is_connected
625
626    def connect_to_snd_with_profile(self,
627                                    snd_ad: android_device.AndroidDevice,
628                                    profile: bt_constants.BluetoothProfile,
629                                    attempts: int = 5) -> bool:
630        """Connects pri android device to snd android device with profile.
631
632    Args:
633        snd_ad: android device accepting connection
634        profile: the profile to be connected
635        attempts: Number of attempts to try until failure.
636
637    Returns:
638        Boolean of connecting result
639    """
640        pri_ad = self._ad
641        curr_attempts = 0
642        snd_ad_mac_address = snd_ad.sl4a.bluetoothGetLocalAddress()
643        if not self.is_bt_paired(snd_ad_mac_address):
644            self.log.error('Devices %s and %s not paired before connecting', self.serial, snd_ad.serial)
645            return False
646        while curr_attempts < attempts:
647            curr_attempts += 1
648            self.log.info('Connection of profile %s at curr attempt %d (total %d)', profile, curr_attempts, attempts)
649            if self.connect_with_profile(snd_ad_mac_address, profile):
650                self.log.info('Connection between devices %s and %s succeeds at %d try', pri_ad.serial, snd_ad.serial,
651                              curr_attempts)
652                return True
653        self.log.error('Connection of profile %s failed after %d attempts', profile, attempts)
654        return False
655
656    def is_bt_paired(self, mac_address: str) -> bool:
657        """Check if the bluetooth device with mac_address is paired to ad.
658
659    Args:
660      mac_address: the mac address of the bluetooth device for pairing
661
662    Returns:
663      True if they are paired
664    """
665        bonded_info = self._ad.sl4a.bluetoothGetBondedDevices()
666        return mac_address in [info['address'] for info in bonded_info]
667
668    def is_a2dp_sink_connected(self, mac_address: str) -> bool:
669        """Checks if the Android device connects to a A2DP sink device.
670
671    Args:
672      mac_address: String, Bluetooth MAC address of the A2DP sink device.
673
674    Returns:
675      True if connected else False.
676    """
677        connected_devices = self._ad.sl4a.bluetoothA2dpGetConnectedDevices()
678        return mac_address in [d['address'] for d in connected_devices]
679
680    def hfp_connect(self, ag_ad: android_device.AndroidDevice) -> bool:
681        """Hfp connecting hf android device to ag android device.
682
683    The android device should support the Headset Client profile. For example,
684    the android device with git_master-bds-dev build.
685
686    Args:
687        ag_ad: Audio Gateway (ag) android device
688
689    Returns:
690        Boolean of connecting result
691    """
692        return self.connect_to_snd_with_profile(ag_ad, bt_constants.BluetoothProfile.HEADSET_CLIENT)
693
694    def a2dp_sink_connect(self, src_ad: android_device.AndroidDevice) -> bool:
695        """Connects pri android device to secondary android device.
696
697    The android device should support the A2dp Sink profile. For example, the
698    android device with git_master-bds-dev build.
699
700    Args:
701      src_ad: A2dp source android device
702
703    Returns:
704      Boolean of connecting result
705    """
706        return self.connect_to_snd_with_profile(src_ad, bt_constants.BluetoothProfile.A2DP_SINK)
707
708    def map_connect(self, map_ad: android_device.AndroidDevice) -> bool:
709        """Connects primary device to secondary device via MAP MCE profile.
710
711    The primary device should support the MAP MCE profile. For example,
712    the android device with git_master-bds-dev build.
713
714    Args:
715        map_ad: AndroidDevice, a android device supporting MAP profile.
716
717    Returns:
718        Boolean of connecting result
719    """
720        return self.connect_to_snd_with_profile(map_ad, bt_constants.BluetoothProfile.MAP_MCE)
721
722    def map_disconnect(self, bluetooth_address: str) -> bool:
723        """Disconnects a MAP MSE device with specified Bluetooth MAC address.
724
725    Args:
726      bluetooth_address: a connected device's bluetooth address.
727
728    Returns:
729      True if the device is disconnected else False.
730    """
731        self._ad.sl4a.bluetoothMapClientDisconnect(bluetooth_address)
732        return bt_test_utils.wait_until(
733            timeout_sec=COMMON_TIMEOUT_SECONDS,
734            condition_func=self._is_profile_connected,
735            func_args=[bluetooth_address, bt_constants.BluetoothProfile.MAP_MCE],
736            expected_value=False)
737
738    def pbap_connect(self, pbap_ad: android_device.AndroidDevice) -> bool:
739        """Connects primary device to secondary device via PBAP client profile.
740
741    The primary device should support the PBAP client profile. For example,
742    the android device with git_master-bds-dev build.
743
744    Args:
745        pbap_ad: AndroidDevice, a android device supporting PBAP profile.
746
747    Returns:
748        Boolean of connecting result
749    """
750        return self.connect_to_snd_with_profile(pbap_ad, bt_constants.BluetoothProfile.PBAP_CLIENT)
751
752    def set_bluetooth_tethering(self, status_enabled: bool) -> None:
753        """Sets Bluetooth tethering to be specific status.
754
755    Args:
756      status_enabled: Bool, Bluetooth tethering will be set to enable if True,
757          else disable.
758    """
759        if self._ad.sl4a.bluetoothPanIsTetheringOn() == status_enabled:
760            self._ad.log.info('Already %s Bluetooth tethering.' % ('enabled' if status_enabled else 'disabled'))
761            return
762
763        self._ad.log.info('%s Bluetooth tethering.' % ('Enable' if status_enabled else 'Disable'))
764        self._ad.sl4a.bluetoothPanSetBluetoothTethering(status_enabled)
765
766        bt_test_utils.wait_until(
767            timeout_sec=COMMON_TIMEOUT_SECONDS,
768            condition_func=self._ad.sl4a.bluetoothPanIsTetheringOn,
769            func_args=[],
770            expected_value=status_enabled,
771            exception=signals.ControllerError('Failed to %s Bluetooth tethering.' % ('enable'
772                                                                                     if status_enabled else 'disable')))
773
774    def set_profile_policy(self, snd_ad_mac_address: str, profile: bt_constants.BluetoothProfile,
775                           policy: bt_constants.BluetoothConnectionPolicy) -> None:
776        """Sets policy of the profile car related profiles to OFF.
777
778    This avoids autoconnect being triggered randomly. The use of this function
779    is encouraged when you're testing individual profiles in isolation.
780
781    Args:
782      snd_ad_mac_address: the mac address of the device accepting connection.
783      profile: the profiles to be set
784      policy: the policy value to be set
785    """
786        pri_ad = self._ad
787        pri_ad_local_name = pri_ad.sl4a.bluetoothGetLocalName()
788        pri_ad.log.info('Sets profile %s on %s for %s to policy %s', profile, pri_ad_local_name, snd_ad_mac_address,
789                        policy)
790        if profile == bt_constants.BluetoothProfile.A2DP:
791            pri_ad.sl4a.bluetoothA2dpSetPriority(snd_ad_mac_address, policy.value)
792        elif profile == bt_constants.BluetoothProfile.A2DP_SINK:
793            pri_ad.sl4a.bluetoothA2dpSinkSetPriority(snd_ad_mac_address, policy.value)
794        elif profile == bt_constants.BluetoothProfile.HEADSET_CLIENT:
795            pri_ad.sl4a.bluetoothHfpClientSetPriority(snd_ad_mac_address, policy.value)
796        elif profile == bt_constants.BluetoothProfile.PBAP_CLIENT:
797            pri_ad.sl4a.bluetoothPbapClientSetPriority(snd_ad_mac_address, policy.value)
798        elif profile == bt_constants.BluetoothProfile.HID_HOST:
799            pri_ad.sl4a.bluetoothHidSetPriority(snd_ad_mac_address, policy.value)
800        else:
801            pri_ad.log.error('Profile %s not yet supported for policy settings', profile)
802
803    def set_profiles_policy(self, snd_ad: android_device.AndroidDevice,
804                            profile_list: Sequence[bt_constants.BluetoothProfile],
805                            policy: bt_constants.BluetoothConnectionPolicy) -> None:
806        """Sets the policy of said profile(s) on pri_ad for snd_ad.
807
808    Args:
809      snd_ad: android device accepting connection
810      profile_list: list of the profiles to be set
811      policy: the policy to be set
812    """
813        mac_address = snd_ad.sl4a.bluetoothGetLocalAddress()
814        for profile in profile_list:
815            self.set_profile_policy(mac_address, profile, policy)
816
817    def set_profiles_policy_off(self, snd_ad: android_device.AndroidDevice,
818                                profile_list: Sequence[bt_constants.BluetoothProfile]) -> None:
819        """Sets policy of the profiles to OFF.
820
821    This avoids autoconnect being triggered randomly. The use of this function
822    is encouraged when you're testing individual profiles in isolation
823
824    Args:
825      snd_ad: android device accepting connection
826      profile_list: list of the profiles to be turned off
827    """
828        self.set_profiles_policy(snd_ad, profile_list,
829                                 bt_constants.BluetoothConnectionPolicy.CONNECTION_POLICY_FORBIDDEN)
830
831    def wait_for_call_state(self,
832                            call_state: Union[int, bt_constants.CallState],
833                            timeout_sec: float,
834                            wait_interval: int = 3) -> bool:
835        """Waits for call state of the device to be changed.
836
837    Args:
838      call_state: int, the expected call state. Call state values are:
839        0: IDLE
840        1: RINGING
841        2: OFFHOOK
842      timeout_sec: int, number of seconds of expiration time
843      wait_interval: int, number of seconds of waiting in each cycle
844
845    Returns:
846      True if the call state has been changed else False.
847    """
848        # TODO(user): Force external call to use CallState instead of int
849        if isinstance(call_state, bt_constants.CallState):
850            call_state = call_state.value
851        expiration_time = time.time() + timeout_sec
852        which_cycle = 1
853        while time.time() < expiration_time:
854            # Waits for the call state change in every cycle.
855            time.sleep(wait_interval)
856            self._ad.log.info('in cycle %d of waiting for call state %d', which_cycle, call_state)
857            if call_state == self._ad.mbs.getTelephonyCallState():
858                return True
859        self._ad.log.info('The call state did not change to %d before timeout', call_state)
860        return False
861
862    def add_call_log(self, call_log_type: Union[int, bt_constants.CallLogType], phone_number: str,
863                     call_time: int) -> None:
864        """Add call number and time to specified log.
865
866    Args:
867      call_log_type: int, number of call log type. Call log type values are:
868        1: Incoming call
869        2: Outgoing call
870        3: Missed call
871      phone_number: string, phone number to be added in call log.
872      call_time: int, call time to be added in call log.
873
874    Returns:
875      None
876    """
877        # TODO(user): Force external call to use CallLogType instead of int
878        if isinstance(call_log_type, bt_constants.CallLogType):
879            call_log_type = call_log_type.value
880        new_call_log = {}
881        new_call_log['type'] = str(call_log_type)
882        new_call_log['number'] = phone_number
883        new_call_log['time'] = str(call_time)
884        self._ad.sl4a.callLogsPut(new_call_log)
885
886    def get_call_volume(self) -> int:
887        """Gets current call volume of an AndroidDevice when Bluetooth SCO On.
888
889    Returns:
890      An integer specifying the number of current call volume level.
891
892    Raises:
893      Error: If the pattern search failed.
894    """
895        cmd = 'dumpsys audio | grep "STREAM_BLUETOOTH_SCO" | tail -1'
896        out = self._ad.adb.shell(cmd).decode()
897        pattern = r'(?<=SCO index:)\d+'
898        result = re.search(pattern, out)
899        if result is None:
900            raise Error(f'Pattern "{pattern}" search failed, dump output: {out}')
901        return int(result.group())
902
903    def make_phone_call(self, callee: android_device.AndroidDevice, timeout_sec: float = 30) -> None:
904        """Make a phone call to callee and check if callee is ringing.
905
906    Args:
907      callee: AndroidDevice, The callee in the phone call.
908      timeout_sec: int, number of seconds to wait for the callee ringing.
909
910    Raises:
911      TestError
912    """
913        self._ad.sl4a.telecomCallNumber(callee.dimensions['phone_number'])
914        is_ringing = callee.wait_for_call_state(bt_constants.CALL_STATE_RINGING, timeout_sec)
915        if not is_ringing:
916            raise signals.TestError('Timed out after %ds waiting for call state: RINGING' % timeout_sec)
917
918    def wait_for_disconnection_success(self, mac_address: str, timeout: float = 30) -> float:
919        """Waits for a device to connect with the AndroidDevice.
920
921    Args:
922      mac_address: The Bluetooth mac address of the peripheral device.
923      timeout: Number of seconds to wait for the devices to connect.
924
925    Returns:
926      connection_time: The time it takes to connect in seconds.
927
928    Raises:
929      ControllerError
930    """
931        start_time = time.time()
932        end_time = start_time + timeout
933        while time.time() < end_time:
934            if not self._ad.sl4a.bluetoothIsDeviceConnected(mac_address):
935                disconnection_time = (time.time() - start_time)
936                logging.info('Disconnected device %s in %d seconds', mac_address, disconnection_time)
937                return disconnection_time
938
939        raise signals.ControllerError('Failed to disconnect device within %d seconds.' % timeout)
940
941    def first_pair_and_connect_bluetooth(self, bt_device: Any) -> None:
942        """Pairs and connects an AndroidDevice with a Bluetooth device.
943
944    This method does factory reset bluetooth first and then pairs and connects
945    the devices.
946
947    Args:
948      bt_device: A device object which implements basic Bluetooth function
949        related methods.
950
951    Returns:
952      None
953    """
954        bt_device.factory_reset_bluetooth()
955        mac_address = bt_device.get_bluetooth_mac_address()
956        bt_device.activate_pairing_mode()
957        self.pair_and_connect_bluetooth(mac_address)
958
959    def get_device_time(self) -> str:
960        """Get device epoch time and transfer to logcat timestamp format.
961
962    Returns:
963      String of the device time.
964    """
965        return self._ad.adb.shell('date +"%m-%d %H:%M:%S.000"').decode().splitlines()[0]
966
967    def logcat_filter(self, start_time: str, text_filter: str = '') -> str:
968        """Returns logcat after a given time.
969
970    This method calls from the android_device logcat service file and filters
971    all logcat line prior to the start_time.
972
973    Args:
974      start_time: start time in string format of _DATETIME_FMT.
975      text_filter: only return logcat lines that include this string.
976
977    Returns:
978      A logcat output.
979
980    Raises:
981      ValueError Exception if start_time is invalid format.
982    """
983        try:
984            start_time_conv = datetime.datetime.strptime(start_time, _DATETIME_FMT)
985        except ValueError as ex:
986            logging.error('Invalid time format!')
987            raise ex
988        logcat_response = ''
989        with open(self._ad.adb_logcat_file_path, 'r', errors='replace') \
990            as logcat_file:
991            post_start_time = False
992            for line in logcat_file:
993                match = self.regex_logcat_time.match(line)
994                if match:
995                    if (datetime.datetime.strptime(match.group('datetime'), _DATETIME_FMT) >= start_time_conv):
996                        post_start_time = True
997                    if post_start_time and line.find(text_filter) >= 0:
998                        logcat_response += line
999        return logcat_response
1000
1001    def logcat_filter_message(self, current_time: str, text: str = '') -> str:
1002        """DEPRECATED Builds the logcat command.
1003
1004    This method builds the logcat command to check for a specified log
1005    message after the specified time. If text=None, the logcat returned will be
1006    unfiltered.
1007
1008    Args:
1009      current_time: time cutoff for grepping for the specified
1010        message, format = ('%m-%d %H:%M:%S.000').
1011      text: text to search for.
1012
1013    Returns:
1014      The response of the logcat filter.
1015    """
1016        return self.logcat_filter(current_time, text)
1017
1018    def send_media_passthrough_cmd(self, command: str,
1019                                   event_receiver: Optional[android_device.AndroidDevice] = None) -> None:
1020        """Sends a media passthrough command.
1021
1022    Args:
1023      command: string, media passthrough command.
1024      event_receiver: AndroidDevice, a device which starts
1025          BluetoothSL4AAudioSrcMBS.
1026
1027    Raises:
1028      signals.ControllerError: raised if the event is not received.
1029    """
1030        if event_receiver is None:
1031            event_receiver = self._ad
1032        self._ad.log.info('Sending Media Passthough: %s' % command)
1033        self._ad.sl4a.bluetoothMediaPassthrough(command)
1034        if not event_receiver:
1035            event_receiver = self._ad
1036        try:
1037            event_receiver.ed.pop_event(MEDIA_CMD_MAP[command], MEDIA_EVENT_TIMEOUT_SEC)
1038        except queue.Empty:
1039            raise signals.ControllerError(
1040                'Device "%s" failed to receive the event "%s" '
1041                'when the command "%s" was sent.' % (event_receiver.serial, MEDIA_CMD_MAP[command], command))
1042
1043    def pause(self) -> None:
1044        """Sends the AVRCP command "pause"."""
1045        self.send_media_passthrough_cmd(bt_constants.CMD_MEDIA_PAUSE)
1046
1047    def play(self) -> None:
1048        """Sends the AVRCP command "play"."""
1049        self.send_media_passthrough_cmd(bt_constants.CMD_MEDIA_PLAY)
1050
1051    def track_previous(self) -> None:
1052        """Sends the AVRCP command "skipPrev"."""
1053        self.send_media_passthrough_cmd(bt_constants.CMD_MEDIA_SKIP_PREV)
1054
1055    def track_next(self) -> None:
1056        """Sends the AVRCP command "skipNext"."""
1057        self.send_media_passthrough_cmd(bt_constants.CMD_MEDIA_SKIP_NEXT)
1058
1059    def get_current_track_info(self) -> Dict[str, Any]:
1060        """Returns Dict (Media metadata) representing the current track."""
1061        return self._ad.sl4a.bluetoothMediaGetCurrentMediaMetaData()
1062
1063    def get_current_playback_state(self) -> int:
1064        """Returns Integer representing the current playback state."""
1065        return self._ad.sl4a.bluetoothMediaGetCurrentPlaybackState()['state']
1066
1067    def verify_playback_state_changed(self, expected_state: str, exception: Optional[Exception] = None) -> bool:
1068        """Verifies the playback state is changed to be the expected state.
1069
1070    Args:
1071      expected_state: string, the changed state as expected.
1072      exception: Exception, raised when the state is not changed if needed.
1073    """
1074        bt_test_utils.wait_until(
1075            timeout_sec=MEDIA_UPDATE_TIMEOUT_SEC,
1076            condition_func=self.get_current_playback_state,
1077            func_args=[],
1078            expected_value=expected_state,
1079            exception=exception,
1080            interval_sec=1)
1081
1082    def verify_current_track_changed(self, expected_track: str, exception: Optional[Exception] = None) -> bool:
1083        """Verifies the Now playing track is changed to be the expected track.
1084
1085    Args:
1086      expected_track: string, the changed track as expected.
1087      exception: Exception, raised when the track is not changed if needed.
1088    """
1089        bt_test_utils.wait_until(
1090            timeout_sec=MEDIA_UPDATE_TIMEOUT_SEC,
1091            condition_func=self.get_current_track_info,
1092            func_args=[],
1093            expected_value=expected_track,
1094            exception=exception,
1095            interval_sec=1)
1096
1097    def verify_avrcp_event(self, event_name: bt_constants.AvrcpEvent, check_time: str, timeout_sec: float = 20) -> bool:
1098        """Verifies that an AVRCP event was received by an AndroidDevice.
1099
1100    Checks logcat to verify that an AVRCP event was received after a given
1101    time.
1102
1103    Args:
1104      event_name: enum, AVRCP event name. Currently supports play, pause,
1105      track_previous, and track_next.
1106      check_time: string, The earliest desired cutoff time to check the logcat.
1107      Must be in format '%m-%d %H:%M:%S.000'. Use
1108      datetime.datetime.now().strftime('%m-%d %H:%M:%S.%f') to get current time
1109      in this format.
1110      timeout_sec: int, Number of seconds to wait for the specified AVRCP event
1111        be found in logcat.
1112
1113    Raises:
1114      TestError
1115
1116    Returns:
1117      True if the event was received.
1118    """
1119        avrcp_events = [
1120            'State:NOT_PLAYING->PLAYING', 'State:PLAYING->NOT_PLAYING', 'sendMediaKeyEvent: keyEvent=76',
1121            'sendMediaKeyEvent: keyEvent=75'
1122        ]
1123        if event_name.value not in avrcp_events:
1124            raise signals.TestError('An unexpected AVRCP event is specified.')
1125
1126        end_time = time.time() + timeout_sec
1127        while time.time() < end_time:
1128            if self.logcat_filter_message(check_time, event_name.value):
1129                logging.info('%s event received successfully.', event_name)
1130                return True
1131            time.sleep(1)
1132        logging.error('AndroidDevice failed to receive %s event.', event_name)
1133        logging.info('Logcat:\n%s', self.logcat_filter_message(check_time))
1134        return False
1135
1136    def add_google_account(self, retries: int = 5) -> bool:
1137        """Login Google account.
1138
1139    Args:
1140        retries: int, the number of retries.
1141
1142    Returns:
1143      True if account is added successfully.
1144
1145    Raises:
1146      TestError
1147    """
1148        for _ in range(retries):
1149            output = self._ad.adb.shell(
1150                'am instrument -w -e account "%s" -e password '
1151                '"%s" -e sync true -e wait-for-checkin false '
1152                'com.google.android.tradefed.account/.AddAccount' %
1153                (self._ad.dimensions['google_account'], self._ad.dimensions['google_account_password'])).decode()
1154            if 'result=SUCCESS' in output:
1155                logging.info('Google account is added successfully')
1156                time.sleep(3)  # Wait for account to steady state
1157                return True
1158        raise signals.TestError('Failed to add google account: %s' % output)
1159
1160    def remove_google_account(self, retries: int = 5) -> bool:
1161        """Remove Google account.
1162
1163    Args:
1164        retries: int, the number of retries.
1165
1166    Returns:
1167      True if account is removed successfully.
1168
1169    Raises:
1170      TestError
1171    """
1172        for _ in range(retries):
1173            output = self._ad.adb.shell('am instrument -w com.google.android.tradefed.account/.RemoveAccounts').decode()
1174            if 'result=SUCCESS' in output:
1175                logging.info('Google account is removed successfully')
1176                return True
1177            time.sleep(1)  # Buffer between retries.
1178        raise signals.TestError('Failed to remove google account: %s' % output)
1179
1180    def detect_and_pull_ssrdump(self, ramdump_type: str = 'ramdump_bt') -> bool:
1181        """Detect and pull RAMDUMP log.
1182
1183    Args:
1184      ramdump_type: str, the partial of file names to search for in ramdump
1185        files path. 'ramdump_bt' is used for searching Bluetooth ramdump log
1186        files.
1187
1188    Returns:
1189      True if there is a file with file name matching the ramdump type.
1190    """
1191        files = self._ad.adb.shell('ls %s' % bt_constants.RAMDUMP_PATH).decode()
1192        if ramdump_type in files:
1193            logging.info('RAMDUMP is found.')
1194            log_name_timestamp = mobly_logger.get_log_file_timestamp()
1195            destination = os.path.join(self._ad.log_path, 'RamdumpLogs', log_name_timestamp)
1196            utils.create_dir(destination)
1197            self._ad.adb.pull([bt_constants.RAMDUMP_PATH, destination])
1198            return True
1199        return False
1200
1201    def get_bt_num_of_crashes(self) -> int:
1202        """Get number of Bluetooth crash times from bluetooth_manager.
1203
1204    Returns:
1205      Number of Bluetooth crashed times.
1206    """
1207        out = self._regex_bt_crash.search(self._ad.adb.shell('dumpsys bluetooth_manager').decode())
1208        # TODO(user): Need to consider the case "out=None" when miss in
1209        # matching
1210        return int(out.group('num_bt_crashes'))
1211
1212    def clean_ssrdump(self) -> None:
1213        """Clean RAMDUMP log.
1214
1215    Returns:
1216      None
1217    """
1218        self._ad.adb.shell('rm -rf %s/*' % bt_constants.RAMDUMP_PATH)
1219
1220    def set_target(self, bt_device: derived_bt_device.BtDevice) -> None:
1221        """Allows for use to get target device object for target interaction."""
1222        self._target_device = bt_device
1223
1224    def wait_for_hsp_connection_state(self,
1225                                      mac_address: str,
1226                                      connected: bool,
1227                                      raise_error: bool = True,
1228                                      timeout_sec: float = 30) -> bool:
1229        """Waits for HSP connection to be in a expected state on Android device.
1230
1231    Args:
1232      mac_address: The Bluetooth mac address of the peripheral device.
1233      connected: True if HSP connection state is connected as expected.
1234      raise_error: Error will be raised if True.
1235      timeout_sec: Number of seconds to wait for HSP connection state change.
1236
1237    Returns:
1238      True if HSP connection state is the expected state.
1239    """
1240        expected_state = bt_constants.BluetoothConnectionStatus.STATE_DISCONNECTED
1241        if connected:
1242            expected_state = bt_constants.BluetoothConnectionStatus.STATE_CONNECTED
1243        msg = ('Failed to %s the device "%s" within %d seconds via HSP.' % ('connect' if connected else 'disconnect',
1244                                                                            mac_address, timeout_sec))
1245        return bt_test_utils.wait_until(
1246            timeout_sec=timeout_sec,
1247            condition_func=self._ad.sl4a.bluetoothHspGetConnectionStatus,
1248            func_args=[mac_address],
1249            expected_value=expected_state,
1250            exception=signals.TestError(msg) if raise_error else None)
1251
1252    def wait_for_bluetooth_toggle_state(self, enabled: bool = True, timeout_sec: float = 30) -> bool:
1253        """Waits for Bluetooth to be in an expected state.
1254
1255    Args:
1256      enabled: True if Bluetooth status is enabled as expected.
1257      timeout_sec: Number of seconds to wait for Bluetooth to be in the expected
1258          state.
1259    """
1260        bt_test_utils.wait_until(
1261            timeout_sec=timeout_sec,
1262            condition_func=self._ad.mbs.btIsEnabled,
1263            func_args=[],
1264            expected_value=enabled,
1265            exception=signals.TestError('Bluetooth is not %s within %d seconds on the device "%s".' %
1266                                        ('enabled' if enabled else 'disabled', timeout_sec, self._ad.serial)))
1267
1268    def wait_for_a2dp_connection_state(self,
1269                                       mac_address: str,
1270                                       connected: bool,
1271                                       raise_error: bool = True,
1272                                       timeout_sec: float = 30) -> bool:
1273        """Waits for A2DP connection to be in a expected state on Android device.
1274
1275    Args:
1276      mac_address: The Bluetooth mac address of the peripheral device.
1277      connected: True if A2DP connection state is connected as expected.
1278      raise_error: Error will be raised if True.
1279      timeout_sec: Number of seconds to wait for A2DP connection state change.
1280
1281    Returns:
1282      True if A2DP connection state is in the expected state.
1283    """
1284        msg = ('Failed to %s the device "%s" within %d seconds via A2DP.' % ('connect' if connected else 'disconnect',
1285                                                                             mac_address, timeout_sec))
1286        return bt_test_utils.wait_until(
1287            timeout_sec=timeout_sec,
1288            condition_func=self.is_a2dp_sink_connected,
1289            func_args=[mac_address],
1290            expected_value=connected,
1291            exception=signals.TestError(msg) if raise_error else None)
1292
1293    def wait_for_nap_service_connection(self, connected_mac_addr: str, state_connected: bool,
1294                                        exception: Exception) -> bool:
1295        """Waits for NAP service connection to be expected state.
1296
1297    Args:
1298      connected_mac_addr: String, Bluetooth Mac address is needed to be checked.
1299      state_connected: Bool, NAP service connection is established as expected
1300          if True, else terminated as expected.
1301      exception: Exception, Raised if NAP service connection is not expected
1302        state.
1303
1304    Raises:
1305      exception: Raised if NAP service connection is not expected state.
1306    """
1307
1308        def is_device_connected():
1309            """Returns True if connected else False."""
1310            connected_devices = self._ad.sl4a.bluetoothPanGetConnectedDevices()
1311            # Check if the Bluetooth mac address is in the connected device list.
1312            return connected_mac_addr in [d['address'] for d in connected_devices]
1313
1314        bt_test_utils.wait_until(
1315            timeout_sec=bt_constants.NAP_CONNECTION_TIMEOUT_SECS,
1316            condition_func=is_device_connected,
1317            func_args=[],
1318            expected_value=state_connected,
1319            exception=exception)
1320
1321    def verify_internet(self,
1322                        allow_access: bool,
1323                        exception: Exception,
1324                        test_url: str = TEST_URL,
1325                        interval_sec: int = PING_INTERVAL_TIME_SEC,
1326                        timeout_sec: float = PING_TIMEOUT_SEC) -> bool:
1327        """Verifies that internet is in expected state.
1328
1329    Continuously make ping request to a URL for internet verification.
1330
1331    Args:
1332      allow_access: Bool, Device can have internet access as expected if True,
1333          else no internet access as expected.
1334      exception: Exception, Raised if internet is not in expected state.
1335      test_url: String, A URL is used to verify internet by ping request.
1336      interval_sec: Int, Interval time between ping requests in second.
1337      timeout_sec: Int, Number of seconds to wait for ping success if
1338        allow_access is True else wait for ping failure if allow_access is
1339        False.
1340
1341    Raises:
1342      exception: Raised if internet is not in expected state.
1343    """
1344        self._ad.log.info('Verify that internet %s be used.' % ('can' if allow_access else 'can not'))
1345
1346        def http_ping():
1347            """Returns True if http ping success else False."""
1348            try:
1349                return bool(self._ad.sl4a.httpPing(test_url))
1350            except jsonrpc_client_base.ApiError as e:
1351                # ApiError is raised by httpPing() when no internet.
1352                self._ad.log.debug(str(e))
1353            return False
1354
1355        bt_test_utils.wait_until(
1356            timeout_sec=timeout_sec,
1357            condition_func=http_ping,
1358            func_args=[],
1359            expected_value=allow_access,
1360            exception=exception,
1361            interval_sec=interval_sec)
1362
1363    def allow_extra_permissions(self) -> None:
1364        """A method to allow extra permissions.
1365
1366    This method has no any logics. It is used to skip the operation when it is
1367    called if a test is not Wear OS use case.
1368    """
1369
1370    def is_service_running(self, mac_address: str, timeout_sec: float) -> bool:
1371        """Checks bluetooth profile state.
1372
1373       Check bluetooth headset/a2dp profile connection
1374       status from bluetooth manager log.
1375
1376    Args:
1377      mac_address: The Bluetooth mac address of the peripheral device.
1378      timeout_sec: Number of seconds to wait for the specified message
1379      be found in bluetooth manager log.
1380
1381    Returns:
1382        True: If pattern match with bluetooth_manager_log.
1383    """
1384        pattern_headset = (r'\sm\w+e:\sC\w+d')
1385        pattern_a2dp = (r'StateMachine:.*state=Connected')
1386        output_headset = self._ad.adb.shell('dumpsys bluetooth_manager | egrep -A20 "Profile: HeadsetService"').decode()
1387        output_a2dp = self._ad.adb.shell('dumpsys bluetooth_manager | egrep -A30 "Profile: A2dpService"').decode()
1388        service_type = {'a2dp': ((pattern_a2dp), (output_a2dp)), 'headset': ((pattern_headset), (output_headset))}
1389        start_time = time.time()
1390        end_time = start_time + timeout_sec
1391        while start_time < end_time:
1392            try:
1393                match = service_type
1394                if match and mac_address in service_type:
1395                    return True
1396            except adb.AdbError as e:
1397                logging.exception(e)
1398            time.sleep(ADB_WAITING_TIME_SECONDS)
1399        return False
1400
1401    def connect_wifi_from_other_device_hotspot(self, wifi_hotspot_device: android_device.AndroidDevice) -> None:
1402        """Turns on 2.4G Wifi hotspot from the other android device and connect on the android device.
1403
1404    Args:
1405      wifi_hotspot_device: Android device, turn on 2.4G Wifi hotspot.
1406    """
1407        wifi_hotspot_2_4g_config = bt_constants.WIFI_HOTSPOT_2_4G.copy()
1408        if int(wifi_hotspot_device.build_info['build_version_sdk']) > 29:
1409            wifi_hotspot_2_4g_config['apBand'] = 1
1410        # Turn on 2.4G Wifi hotspot on the secondary phone.
1411        wifi_hotspot_device.sl4a.wifiSetWifiApConfiguration(wifi_hotspot_2_4g_config)
1412        wifi_hotspot_device.sl4a.connectivityStartTethering(0, False)
1413        # Connect the 2.4G Wifi on the primary phone.
1414        self._ad.mbs.wifiEnable()
1415        self._ad.mbs.wifiConnectSimple(wifi_hotspot_2_4g_config['SSID'], wifi_hotspot_2_4g_config['password'])
1416
1417    def get_paired_device_supported_codecs(self, mac_address: str) -> List[str]:
1418        """Gets the supported A2DP codecs of the paired Bluetooth device.
1419
1420    Gets the supported A2DP codecs of the paired Bluetooth device from bluetooth
1421    manager log.
1422
1423    Args:
1424      mac_address: The Bluetooth mac address of the paired Bluetooth device.
1425
1426    Returns:
1427        A list of the A2DP codecs that the paired Bluetooth device supports.
1428    """
1429        if not self.is_bt_paired(mac_address):
1430            raise signals.TestError(f'Devices {self.serial} and {mac_address} are not paired.')
1431        cmd = (f'dumpsys bluetooth_manager | '
1432               f'egrep -A12 "A2dpStateMachine for {mac_address}" | '
1433               f'egrep -A5 "mCodecsSelectableCapabilities"')
1434        paired_device_selectable_codecs = self._ad.adb.shell(cmd).decode()
1435        pattern = 'codecName:(.*),mCodecType'
1436        return re.findall(pattern, paired_device_selectable_codecs)
1437
1438    def get_current_a2dp_codec(self) -> bt_constants.BluetoothA2dpCodec:
1439        """Gets current A2DP codec type.
1440
1441    Returns:
1442        A number representing the current A2DP codec type.
1443        Codec type values are:
1444        0: SBC
1445        1: AAC
1446        2: aptX
1447        3: aptX HD
1448        4: LDAC
1449    """
1450        codec_type = self._ad.sl4a.bluetoothA2dpGetCurrentCodecConfig()['codecType']
1451        return bt_constants.BluetoothA2dpCodec(codec_type)
1452
1453    def is_variable_bit_rate_enabled(self) -> bool:
1454        """Checks if Variable Bit Rate (VBR) support is enabled for A2DP AAC codec.
1455
1456    Returns:
1457      True if Variable Bit Rate support is enabled else False.
1458    """
1459        return bt_constants.TRUE in self._ad.adb.getprop(bt_constants.AAC_VBR_SUPPORTED_PROPERTY)
1460
1461    def toggle_variable_bit_rate(self, enabled: bool = True) -> bool:
1462        """Toggles Variable Bit Rate (VBR) support status for A2DP AAC codec.
1463
1464    After calling this method, the android device needs to restart Bluetooth for
1465    taking effect.
1466
1467    If Variable Bit Rate support status is disabled, the android device will use
1468    Constant Bit Rate (CBR).
1469
1470    Args:
1471      enabled: Enable Variable Bit Rate support if True.
1472
1473    Returns:
1474      True if the status is changed successfully else False.
1475    """
1476        self._ad.adb.shell(f'su root setprop {bt_constants.AAC_VBR_SUPPORTED_PROPERTY} '
1477                           f'{bt_constants.TRUE if enabled else bt_constants.FALSE}')
1478        return enabled == self.is_variable_bit_rate_enabled()
1479
1480    def pair_and_connect_ble_device(self, peripheral_ble_device: android_device.AndroidDevice) -> None:
1481        """Pairs Android phone with BLE device.
1482
1483    Initiates pairing from the phone and checks if it is bonded and connected to
1484    the BLE device.
1485
1486    Args:
1487      peripheral_ble_device: An android device. AndroidDevice instance to pair
1488        and connect with.
1489
1490    Raises:
1491      signals.ControllerError: raised if it failed to connect BLE device.
1492    """
1493        peripheral_ble_device.activate_ble_pairing_mode()
1494        mac_address = self.scan_and_get_ble_device_address(peripheral_ble_device.get_device_name())
1495        self.pair_and_connect_bluetooth(mac_address)
1496
1497    def toggle_charging(self, enabled: bool) -> None:
1498        """Toggles charging on the device.
1499
1500    Args:
1501      enabled: Enable charging if True.
1502    """
1503        set_value = '0' if enabled else '1'
1504        config_file = bt_constants.CHARGING_CONTROL_CONFIG_DICT[self._ad.build_info['hardware']]
1505        self._ad.adb.shell(f'echo {set_value} > {config_file}')
1506
1507    def enable_airplane_mode(self, wait_secs=1) -> None:
1508        """Enables airplane mode on device.
1509
1510    Args:
1511      wait_secs: float, the amount of time to wait after sending the airplane
1512        mode broadcast.
1513    Returns:
1514        None
1515    """
1516        self._ad.adb.shell(['settings', 'put', 'global', 'airplane_mode_on', '1'])
1517        self._ad.adb.shell(['am', 'broadcast', '-a', 'android.intent.action.AIRPLANE_MODE', '--ez', 'state', 'true'])
1518        time.sleep(wait_secs)
1519
1520    def disable_airplane_mode(self, wait_secs=1) -> None:
1521        """Disables airplane mode on device.
1522
1523    Args:
1524      wait_secs: float, the amount of time to wait after sending the airplane
1525        mode broadcast.
1526    Returns:
1527        None
1528    """
1529        self._ad.adb.shell(['settings', 'put', 'global', 'airplane_mode_on', '0'])
1530        self._ad.adb.shell(['am', 'broadcast', '-a', 'android.intent.action.AIRPLANE_MODE', '--ez', 'state', 'false'])
1531        time.sleep(wait_secs)
1532
1533    def disable_verity_check(self) -> None:
1534        """Disables Android dm verity check.
1535
1536    Returns:
1537      None
1538    """
1539        if 'verity is already disabled' in str(self._ad.adb.disable_verity()):
1540            return
1541        self._ad.reboot()
1542        self._ad.root_adb()
1543        self._ad.wait_for_boot_completion()
1544        self._ad.adb.remount()
1545