android.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. """
  2. Copyright (c) Contributors to the Open 3D Engine Project.
  3. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. SPDX-License-Identifier: Apache-2.0 OR MIT
  5. Utilities for interacting with Android devices.
  6. """
  7. import datetime
  8. import logging
  9. import os
  10. import psutil
  11. import subprocess
  12. import ly_test_tools.environment.process_utils as process_utils
  13. import ly_test_tools.environment.waiter as waiter
  14. import ly_test_tools._internal.exceptions as exceptions
  15. logger = logging.getLogger(__name__)
  16. SINGLE_DEVICE = 0
  17. MULTIPLE_DEVICES = 1
  18. NO_DEVICES = 2
  19. def can_run_android():
  20. """
  21. Determine if android can be run by trying to use adb.
  22. :return: True if the adb command returns success and False otherwise.
  23. """
  24. try:
  25. with open(os.devnull, 'wb') as DEVNULL:
  26. return_code = process_utils.safe_check_call(["adb", "version"], stdout=DEVNULL, stderr=subprocess.STDOUT)
  27. if return_code == 0:
  28. return True
  29. except Exception: # purposefully broad
  30. logger.info("Android not enabled")
  31. logger.debug("Attempt to verify adb installation failed", exc_info=True)
  32. return False
  33. def check_adb_connection_state():
  34. """
  35. Wrapper for gathering output of adb get-state command.
  36. :return: The output of the adb command as an int, raises RunTimeError otherwise.
  37. """
  38. with psutil.Popen('adb get-state', stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as proc:
  39. output = proc.communicate()[0].decode('utf-8')
  40. if 'more than one device' in output:
  41. return MULTIPLE_DEVICES
  42. elif 'no devices/emulators found' in output:
  43. return NO_DEVICES
  44. elif 'device' == output.strip():
  45. return SINGLE_DEVICE
  46. else:
  47. raise exceptions.LyTestToolsFrameworkException("Detected unhandled output from adb get-state: {}".format(output.strip()))
  48. def reverse_tcp(device, host_port, device_port):
  49. """
  50. Tunnels a TCP port over USB from the device to the local host.
  51. :param device: Device id of a connected device
  52. :param host_port: Port to reverse to
  53. :param device_port: Port to reverse from
  54. :return: None
  55. """
  56. logger.debug('Running ADB reverse command')
  57. cmd = ['adb', '-s', device, 'reverse', f'tcp:{host_port}', f'tcp:{device_port}']
  58. logger.debug(f'Executing command: "{cmd}"')
  59. process_utils.check_output(cmd)
  60. def forward_tcp(device, host_port, device_port):
  61. """
  62. Tunnels a TCP port over USB from the local host to the device.
  63. :param device: Device id of a connected device
  64. :param host_port: Port to forward from
  65. :param device_port: Port to forward to
  66. :return: None
  67. """
  68. logger.debug('Running ADB forward command')
  69. cmd = ['adb', '-s', device, 'forward', 'tcp:{}'.format(host_port), 'tcp:{}'.format(device_port)]
  70. process_utils.check_output(cmd)
  71. logger.debug('Executing command: %s' % cmd)
  72. def undo_tcp_port_changes(device):
  73. """
  74. Undoes all 'adb forward' and 'adb reverse' commands for forwarding and reversing TCP ports.
  75. :param device: Device id of a connected device
  76. :return: None
  77. """
  78. logger.debug('Reverting "adb forward" and "adb reverse" commands.')
  79. undo_tcp_forward = ['adb', '-s', device, 'forward', '--remove-all']
  80. undo_tcp_reverse = ['adb', '-s', device, 'reverse', '--remove-all']
  81. process_utils.check_output(undo_tcp_forward)
  82. process_utils.check_output(undo_tcp_reverse)
  83. logger.debug('Reverted forwarded/reversed TCP ports using commands: {} && {}'.format(
  84. undo_tcp_forward, undo_tcp_reverse))
  85. def get_screenshots(device, package_name, project):
  86. """
  87. Captures a Screenshot for the game and stores it in the project folder on the Devices.
  88. :param device: Device id of a connected device
  89. :param package_name: Name of the Android package
  90. :param project: Name of the lumberyard project
  91. :return: None
  92. """
  93. screenshot_cmd = ['adb',
  94. '-s',
  95. device,
  96. 'shell',
  97. 'screencap',
  98. '-p',
  99. '/sdcard/Android/data/{}/files/log/{}-{}.png'.format(package_name, project, device)]
  100. process_utils.check_output(screenshot_cmd)
  101. logger.debug('Screenshot Command Ran: {}'.format(screenshot_cmd))
  102. def pull_files_to_pc(package_name, logs_path, device=None):
  103. """
  104. Pulls a file from the package installed on the device to the PC.
  105. :param device: ID of a connected Android device
  106. :param package_name: Name of the Android package
  107. :param logs_path: Path to the logs location on the local machine
  108. :return: None
  109. """
  110. directory = os.path.join(logs_path, device)
  111. if not os.path.exists(directory):
  112. os.makedirs(directory, exist_ok=True)
  113. pull_cmd = ['adb']
  114. if device is not None:
  115. pull_cmd.extend(['-s', device])
  116. pull_cmd.extend(['pull', '/sdcard/Android/data/{}/files/log/'.format(package_name), directory])
  117. try:
  118. process_utils.check_output(pull_cmd, stderr=subprocess.STDOUT)
  119. except subprocess.CalledProcessError as err:
  120. if 'does not exist' in err.output:
  121. logger.info('Could not pull logs since none exist on device {}'.format(device))
  122. else:
  123. raise exceptions.LyTestToolsFrameworkException from err
  124. logger.debug('Pull File Command Ran successfully: {}'.format(str(pull_cmd)))
  125. def push_files_to_device(source, destination, device=None):
  126. """
  127. Pushes a file to a specific location. Source being from the PC and the destination is the Android destination
  128. Params.
  129. :param source: The file location on the host machine
  130. :param destination: The destination on the Android device we want to push the files
  131. :param device: The device ID of the device to push files to
  132. :return: None
  133. """
  134. logger.debug('Pushing files from windows location {} to device {} location {}'.format(source, device, destination))
  135. cmd = ['adb']
  136. if device is not None:
  137. cmd.extend(["-s", device])
  138. cmd.extend(["push", source, destination])
  139. push_result = process_utils.check_output(cmd)
  140. logger.debug('Push File Command Ran: {}'.format(str(cmd)))
  141. if 'pushed' not in push_result:
  142. raise exceptions.LyTestToolsFrameworkException('[AndroidLauncher] Failed to push file to device: {}!'.format(device))
  143. def start_adb_server():
  144. """
  145. Starts the ADB server.
  146. :return: None
  147. """
  148. logger.debug('Starting adb server')
  149. cmd = 'adb start-server'
  150. process_utils.check_call(cmd)
  151. def kill_adb_server():
  152. """
  153. Kills the ADB server.
  154. :return: None
  155. """
  156. logger.debug('Killing adb server')
  157. cmd = 'adb kill-server'
  158. process_utils.check_call(cmd)
  159. def wait_for_android_device_load(android_device_id, timeout=60):
  160. """
  161. Utilizes adb logcat commands to make sure the device is fully loaded before connecting the RemoteConsole()
  162. Helps deal with race conditions that may occur when calls are made to the LY client before loading is complete.
  163. :param android_device_id: string ID for the Android device to target.
  164. :param timeout: int seconds to wait until raising an exception.
  165. :return: output from the command if it succeeds, raises an exception otherwise.
  166. """
  167. adb_prefix = ['adb', '-s', android_device_id]
  168. current_time = datetime.datetime.now().strftime('%m-%d %H:%M:%S.%f') # Example output: '01-28 16:56:28.271000'
  169. wait_command = []
  170. wait_command.extend(adb_prefix)
  171. wait_command.extend(['logcat',
  172. '-e',
  173. '\\bFinished loading textures\\b', # exact regex match for 'Finished loading textures'
  174. '-t',
  175. current_time])
  176. try:
  177. waiter.wait_for(
  178. lambda: process_utils.check_output(wait_command),
  179. timeout=timeout,
  180. exc=subprocess.CalledProcessError)
  181. except subprocess.CalledProcessError:
  182. logger.exception("Android device with ID: {} never finished loading".format(android_device_id))
  183. def get_devices():
  184. """
  185. Utilizes the 'adb devices' command to check that a device is connected to the host machine.
  186. :return: A list of connected device IDs or an empty list if none are found.
  187. """
  188. devices_list = []
  189. cmd = 'adb devices'
  190. # Example cmd_output: 'List of devices attached\r\nemulator-5554\tdevice\r\nA1B2C3D4E5\tdevice\r\n\r\n'
  191. cmd_output = process_utils.check_output(cmd)
  192. # Example raw_devices_output: ['List of devices attached', 'emulator-5554\tdevice', 'A1B2C3D4E5\tdevice']
  193. raw_devices_output = cmd_output.strip().splitlines()
  194. for raw_output in raw_devices_output:
  195. updated_raw_output = raw_output.split('\t') # ['emulator-5554', 'device'] or ['List of devices attached']
  196. if len(updated_raw_output) > 1:
  197. devices_list.append(updated_raw_output[0])
  198. return devices_list # Example devices_list: ['emulator-5554', 'A1B2C3D4E5']