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