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