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