android_deployment.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  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. #
  5. # SPDX-License-Identifier: Apache-2.0 OR MIT
  6. #
  7. #
  8. import datetime
  9. import logging
  10. import os
  11. import json
  12. import platform
  13. import subprocess
  14. import sys
  15. import time
  16. import pathlib
  17. import shutil
  18. # Resolve the common python module
  19. ROOT_DEV_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..'))
  20. if ROOT_DEV_PATH not in sys.path:
  21. sys.path.append(ROOT_DEV_PATH)
  22. from cmake.Tools import common
  23. from cmake.Tools.Platform.Android import android_support
  24. # The following is the list of known android external storage paths that we will attempt to verify on a device and
  25. # return the first one that is detected
  26. KNOWN_ANDROID_EXTERNAL_STORAGE_PATHS = [
  27. '/sdcard/',
  28. '/storage/emulated/0/',
  29. '/storage/emulated/legacy/',
  30. '/storage/sdcard0/',
  31. '/storage/self/primary/',
  32. ]
  33. ANDROID_TARGET_TIMESTAMP_FILENAME = 'deploy.timestamp'
  34. class AndroidDeployment(object):
  35. """
  36. Class to manage the deployment of game assets to an android device (Separately from the APK)
  37. """
  38. DEPLOY_APK_ONLY = 'APK'
  39. DEPLOY_ASSETS_ONLY = 'ASSETS'
  40. DEPLOY_BOTH = 'BOTH'
  41. def __init__(self, dev_root, build_dir, configuration, android_device_filter, clean_deploy, android_sdk_path, deployment_type, game_name=None, asset_mode=None, asset_type=None, embedded_assets=True, is_unit_test=False, kill_adb_server=False):
  42. """
  43. Initialize the Android Deployment Worker
  44. :param dev_root: The dev-root of the engine
  45. :param android_device_filter: An optional list of devices to filter on the connected devices to deploy to. If not supplied, deploy to all devices
  46. :param clean_deploy: Option to clean the target device's assets before deploying the game's assets from the host
  47. :param android_sdk_path: Path to the android SDK (to use the adb tool)
  48. :param deployment_type: The type of deployment (DEPLOY_APK_ONLY, DEPLOY_ASSETS_ONLY, or DEPLOY_BOTH)
  49. :param game_name: The name of the game whose assets are being deployed. None if is_test_project is True
  50. :param asset_mode: The asset mode of deployment (LOOSE, PAK, VFS). None if is_test_project is True
  51. :param asset_type: The asset type. None if is_test_project is True
  52. :param embedded_assets: Boolean to indicate if the assets are embedded in the APK or not
  53. :param is_unit_test: Boolean to indicate if this is a unit test deployment
  54. :param kill_adb_server: Boolean to indicate if it should kill adb server at the end of deployment
  55. """
  56. self.dev_root = pathlib.Path(dev_root)
  57. self.build_dir = self.dev_root / build_dir
  58. self.configuration = configuration
  59. self.game_name = game_name
  60. self.asset_mode = asset_mode
  61. self.asset_type = asset_type
  62. self.clean_deploy = clean_deploy
  63. self.embedded_assets = embedded_assets
  64. self.deployment_type = deployment_type
  65. self.is_test_project = is_unit_test
  66. self.kill_adb_server = kill_adb_server
  67. if self.embedded_assets:
  68. if self.deployment_type == AndroidDeployment.DEPLOY_ASSETS_ONLY:
  69. raise common.LmbrCmdError(f"Deployment type {deployment_type} set but the assets are embedded in the APK. To deploy assets, build the APK again.")
  70. # If assets are embedded in the APK then deploying both (apk and assets) just means deploy the APK.
  71. if self.deployment_type == AndroidDeployment.DEPLOY_BOTH:
  72. self.deployment_type = AndroidDeployment.DEPLOY_APK_ONLY
  73. if not self.is_test_project:
  74. if asset_mode == 'PAK':
  75. self.local_asset_path = self.dev_root / 'Pak' / f'{game_name.lower()}_{asset_type}_paks'
  76. else:
  77. # Assets layout folder when assets are not included into APK is 'app/src/assets'
  78. self.local_asset_path = self.build_dir / 'app/src/assets'
  79. assert game_name is not None, f"'game_name' is required"
  80. self.game_name = game_name
  81. assert asset_mode is not None, f"'asset_mode' is required"
  82. self.asset_mode = asset_mode
  83. assert asset_type is not None, f"'asset_type' is required"
  84. self.asset_type = asset_type
  85. self.files_in_asset_path = list(self.local_asset_path.glob('**/*'))
  86. self.android_settings = AndroidDeployment.read_android_settings(self.dev_root, game_name)
  87. else:
  88. self.local_asset_path = None
  89. if asset_mode:
  90. logging.warning(f"'asset_mode' argument '{asset_mode}' ignored for unit test deployment.")
  91. if asset_type:
  92. logging.warning(f"'asset_type' argument '{asset_type}' ignored for unit test deployment.")
  93. self.files_in_asset_path = []
  94. self.apk_path = self.build_dir / 'app' / 'build' / 'outputs' / 'apk' / configuration / f'app-{configuration}.apk'
  95. self.android_device_filter = [android_device.strip() for android_device in android_device_filter.split(',')] if android_device_filter else []
  96. self.adb_path = AndroidDeployment.resolve_adb_tool(pathlib.Path(android_sdk_path))
  97. self.adb_started = False
  98. @staticmethod
  99. def read_android_settings(dev_root, game_name):
  100. """
  101. Read and parse the android_project.json file into a dictionary to process the specific attributes needed for the manifest template
  102. :param dev_root: The dev root we are working from
  103. :param game_name: Name of the game under the dev root
  104. :return: The android settings for the game project if any
  105. """
  106. game_folder = dev_root / game_name
  107. game_folder_project_properties_path = game_folder / 'Platform' / 'Android' / 'android_project.json'
  108. game_project_properties_content = game_folder_project_properties_path.resolve(strict=True)\
  109. .read_text(encoding=common.DEFAULT_TEXT_READ_ENCODING,
  110. errors=common.ENCODING_ERROR_HANDLINGS)
  111. # Extract the key attributes we need to process and build up our environment table
  112. game_project_json = json.loads(game_project_properties_content)
  113. android_settings = game_project_json.get('android_settings', {})
  114. return android_settings
  115. @staticmethod
  116. def resolve_adb_tool(android_sdk_path):
  117. """
  118. Resolve the location of the adb tool, first check if the system can
  119. find one, otherwise look for it based on the input Android SDK Path.
  120. :param android_sdk_path: The android SDK path to search for the adb tool
  121. :return: The absolute path to the adb tool
  122. """
  123. adb_target = shutil.which('adb')
  124. if adb_target:
  125. return pathlib.Path(adb_target)
  126. else:
  127. adb_target = 'adb.exe' if platform.system() == 'Windows' else 'adb'
  128. check_adb_target = android_sdk_path / 'platform-tools' / adb_target
  129. if not check_adb_target.exists():
  130. raise common.LmbrCmdError(f"Invalid Android SDK path '{str(android_sdk_path)}': Unable to locate '{adb_target}'.")
  131. return check_adb_target
  132. def get_android_project_settings(self, key_name, default_value):
  133. return self.android_settings.get(key_name, default_value)
  134. def adb_call(self, arg_list, device_id=None):
  135. """
  136. Wrapper to execute the adb command-line tool
  137. :param arg_list: Argument list to send to the tool
  138. :param device_id: Optional device id (from the 'get_target_android_devices' call) to invoke the call to.
  139. :return: The stdout result of the call
  140. """
  141. if isinstance(arg_list, str):
  142. arg_list = [arg_list]
  143. call_arguments = [str(self.adb_path.resolve())]
  144. if device_id:
  145. call_arguments.extend(['-s', device_id])
  146. call_arguments.extend(arg_list)
  147. logging.debug(f"adb command: {subprocess.list2cmdline(call_arguments)}")
  148. try:
  149. output = subprocess.check_output(subprocess.list2cmdline(call_arguments),
  150. shell=True,
  151. stderr=subprocess.PIPE).decode(common.DEFAULT_TEXT_READ_ENCODING,
  152. common.ENCODING_ERROR_HANDLINGS)
  153. logging.debug(f"adb output:\n{output}")
  154. return output
  155. except subprocess.CalledProcessError as err:
  156. std_out = err.stdout.decode(common.DEFAULT_TEXT_READ_ENCODING, common.ENCODING_ERROR_HANDLINGS)
  157. std_err = err.stderr.decode(common.DEFAULT_TEXT_READ_ENCODING, common.ENCODING_ERROR_HANDLINGS)
  158. logging.debug(f"adb returned non-zero.\noutput:\n{std_out}\nerror:\n{std_err}\n")
  159. raise common.LmbrCmdError(std_err)
  160. def adb_shell(self, command, device_id):
  161. """
  162. Special wrapper around calling "adb shell" which will invoke a shell command on the android device
  163. :param command: The shell command to invoke on the android device
  164. :param device_id: The device id (from the 'get_target_android_devices' call) to invoke the shell call on
  165. :return: The stdout result of the call
  166. """
  167. shell_command = ['shell', command]
  168. return self.adb_call(shell_command, device_id=device_id)
  169. def adb_ls(self, path, device_id, args=None):
  170. """
  171. Request an 'ls' call on the android device
  172. :param path: The path to perform the 'ls' call on
  173. :param device_id: device id (from the 'get_target_android_devices' call) to invoke the shell call on
  174. :param args: Additional args to pass into the l'ls' call
  175. :return: Tuple of Boolean result of the call and the output of the call
  176. """
  177. error_messages = [
  178. 'No such file or directory',
  179. 'Permission denied'
  180. ]
  181. shell_command = ['ls']
  182. if args:
  183. shell_command.extend(args)
  184. shell_command.append(path)
  185. raw_output = self.adb_shell(command=' '.join(shell_command),
  186. device_id=device_id)
  187. if not raw_output:
  188. return False, None
  189. if raw_output is None or any([error for error in error_messages if error in raw_output]):
  190. status = False
  191. else:
  192. status = True
  193. return status, raw_output
  194. def get_target_android_devices(self):
  195. """
  196. Gets all of the connected android devices with adb, filtered by the set optional device filter
  197. :return: list of serial numbers of optionally filtered connected devices.
  198. """
  199. connected_devices = []
  200. # Call adb to get the device list and process the raw response
  201. raw_devices_output = self.adb_call("devices")
  202. if not raw_devices_output:
  203. raise common.LmbrCmdError("Error getting connected devices through adb")
  204. device_output_list = raw_devices_output.split(os.linesep)
  205. for device_output in device_output_list:
  206. if any(x in device_output for x in ['List', '*']):
  207. logging.debug(f"Skipping the following line as it has 'List', '*' or 'emulator' in it: {device_output}")
  208. continue
  209. device_serial = device_output.split()
  210. if device_serial:
  211. if 'unauthorized' in device_output.lower():
  212. logging.warning(f"Device {device_serial[0]} is not authorized for development access. Please reconnect the device and check for a confirmation dialog.")
  213. elif device_serial[0] in self.android_device_filter or not self.android_device_filter:
  214. connected_devices.append(device_serial[0])
  215. else:
  216. logging.debug(f"Skipping filtered out Device {device_serial[0]} .")
  217. if not connected_devices:
  218. raise common.LmbrCmdError("No connected android devices found")
  219. return connected_devices
  220. def check_known_android_paths(self, device_id):
  221. """
  222. Look for a known android path that is writeable and return the first one that is found
  223. :param device_id: The device id (from the 'get_target_android_devices' call) to invoke the shell call on
  224. :return: The first available android path if found, None if not
  225. """
  226. for path in KNOWN_ANDROID_EXTERNAL_STORAGE_PATHS:
  227. logging.debug(f"Checking known path '{path}' on device '{device_id}'")
  228. # Test the path by performing an 'ls' call on it and checking if an error is returned from the result
  229. result, output = self.adb_ls(path=path,
  230. args=None,
  231. device_id=device_id)
  232. if result:
  233. return path[:-1]
  234. return None
  235. def detect_device_storage_path(self, device_id):
  236. """
  237. Uses the device's environment variable "EXTERNAL_STORAGE" to determine the correct
  238. path to public storage that has write permissions. If at any point does the env var
  239. validation fail, fallback to checking known possible paths to external storage.
  240. :param device_id:
  241. :return: The first available storage device
  242. """
  243. external_storage = self.adb_shell(command="set | grep EXTERNAL_STORAGE",
  244. device_id=device_id)
  245. if not external_storage:
  246. logging.debug(f"Unable to get 'EXTERNAL_STORAGE' environment from device '{device_id}'. Falling back to known android paths.")
  247. return self.check_known_android_paths(device_id)
  248. # Given the 'EXTERNAL_STORAGE' environment, parse out the value and validate it
  249. storage_path_key_value = external_storage.split('=')
  250. if len(storage_path_key_value) != 2:
  251. logging.debug(f"The value for 'EXTERNAL_STORAGE' environment from device '{device_id}' does not represent a valid key-value pair: {storage_path_key_value}. Falling back to known android paths")
  252. return self.check_known_android_paths(device_id)
  253. # Check the existence and permissions issue of the storage path
  254. storage_path = storage_path_key_value[1].strip()
  255. is_external_valid, _ = self.adb_ls(path=storage_path,
  256. device_id=device_id)
  257. if is_external_valid:
  258. return storage_path
  259. # The set external path has an issue, attempt to determine its real path through an adb shell call
  260. logging.debug(f"The path specified in EXTERNAL_STORAGE seems to have permission issues, attempting to resolve with realpath for device {device_id}.")
  261. real_path = self.adb_shell(command=f'realpath {storage_path}',
  262. device_id=device_id)
  263. if not real_path:
  264. logging.debug(f"Unable to determine the real path '{storage_path}' (from EXTERNAL_STORAGE) for {self.game_name} on device {device_id}. Falling back to known android paths")
  265. return self.check_known_android_paths(device_id)
  266. real_path = real_path.strip()
  267. is_external_valid, _ = self.adb_ls(path=real_path,
  268. device_id=device_id)
  269. if is_external_valid:
  270. return real_path
  271. logging.debug(f'Unable to validate the resolved EXTERNAL_STORAGE environment variable path for device {device_id}.')
  272. return self.check_known_android_paths(device_id)
  273. def get_device_file_timestamp(self, remote_file_path, device_id):
  274. """
  275. Get the integer timestamp value of a file from a given device.
  276. :param remote_file_path: The path to the timestamp file on the android device
  277. :param device_id: The device id (from the 'get_target_android_devices' call) to invoke the shell call on
  278. :return: The time value if found, None if not
  279. """
  280. try:
  281. timestamp_string = self.adb_shell(command=f'cat {remote_file_path}',
  282. device_id=device_id).strip()
  283. except (common.LmbrCmdError, AttributeError):
  284. return None
  285. if not timestamp_string:
  286. return None
  287. for fmt in ('%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M:%S.%f'):
  288. try:
  289. target_time = time.mktime(time.strptime(timestamp_string, fmt))
  290. break
  291. except ValueError:
  292. target_time = None
  293. return target_time
  294. def update_device_file_timestamp(self, relative_assets_path, device_id):
  295. """
  296. Update the device timestamp file on an android device to track files that need updating on pushes
  297. :param relative_assets_path: The relative path to the assets on the android device
  298. :param device_id: The device id (from the 'get_target_android_devices' call) to invoke the shell call on
  299. """
  300. timestamp_str = str(datetime.datetime.now())
  301. logging.debug(f"Updating timestamp on device {device_id} to {timestamp_str}")
  302. local_timestamp_file_path = self.local_asset_path / ANDROID_TARGET_TIMESTAMP_FILENAME
  303. local_timestamp_file_path.write_text(timestamp_str)
  304. target_timestamp_file_path = f'{relative_assets_path}/{ANDROID_TARGET_TIMESTAMP_FILENAME}'
  305. self.adb_call(arg_list=['push', str(local_timestamp_file_path), target_timestamp_file_path],
  306. device_id=device_id)
  307. @staticmethod
  308. def should_copy_file(check_path, check_time):
  309. """
  310. Check if a source file should be copied, by checking if its timestamp is newer than the 'check_time'
  311. :param check_path: The path to the source file whose timestamp will be evaluated
  312. :param check_time: The baseline 'check_time' value to compare the source file timestamp against
  313. :return: True if the source file is newer than the baseline 'check_time', False if not
  314. """
  315. if not check_path.is_file():
  316. return False
  317. stat_src = check_path.stat()
  318. should_copy = stat_src.st_mtime >= check_time
  319. return should_copy
  320. def check_package_installed(self, package_name, target_device):
  321. """
  322. Checks if the package for the game is currently installed or not
  323. @param package_name: The name of the package to search for
  324. @param target_device: The target device to search for the package on
  325. @return: True if there an existing package on the device with the same package name, false if not
  326. """
  327. output_result = self.adb_call(['shell', 'cmd', 'package', 'list', 'packages', package_name],
  328. target_device)
  329. return output_result != ''
  330. def install_apk_to_device(self, target_device):
  331. """
  332. Install the APK to a target device
  333. @param target_device: The device id of the connected device to install to
  334. """
  335. if self.is_test_project:
  336. android_package_name = android_support.TEST_RUNNER_PACKAGE_NAME
  337. else:
  338. android_package_name = self.get_android_project_settings(key_name='package_name',
  339. default_value='org.o3de.sdk')
  340. if self.clean_deploy and self.check_package_installed(android_package_name, target_device):
  341. logging.info(f"Device '{target_device}': Uninstalling pre-existing APK for {self.game_name} ...")
  342. self.adb_call(arg_list=['uninstall', android_package_name],
  343. device_id=target_device)
  344. logging.info(f"Device '{target_device}': Installing APK for {self.game_name} ...")
  345. self.adb_call(arg_list=['install', '-t', '-r', str(self.apk_path.resolve())],
  346. device_id=target_device)
  347. def path_exists_on_device(self, path, device_id):
  348. try:
  349. result, _ = self.adb_ls(path=path,
  350. args=None,
  351. device_id=device_id)
  352. return result
  353. except (common.LmbrCmdError, AttributeError):
  354. return False
  355. def create_path_on_device(self, path, device_id):
  356. if not self.path_exists_on_device(path, device_id):
  357. self.adb_shell(command=f'mkdir {path}', device_id=device_id)
  358. def install_assets_to_device(self, detected_storage, target_device):
  359. """
  360. Install the assets for the game to a target device
  361. @param detected_storage: The detected storage path on the target device
  362. @param target_device: The ID of the target device
  363. """
  364. assert not self.is_test_project
  365. android_package_name = self.get_android_project_settings(key_name='package_name',
  366. default_value='org.o3de.sdk')
  367. output_package = f'{detected_storage}/Android/data/{android_package_name}'
  368. output_target = f'{output_package}/files'
  369. device_timestamp_file = f'{output_target}/{ANDROID_TARGET_TIMESTAMP_FILENAME}'
  370. # Track the current timestamp if possible to see if we can incrementally push files rather
  371. # than always pushing all files
  372. target_timestamp = self.get_device_file_timestamp(remote_file_path=device_timestamp_file,
  373. device_id=target_device)
  374. if self.clean_deploy:
  375. logging.info(f"Device '{target_device}': Cleaning target assets before deployment...")
  376. self.adb_shell(command=f'rm -rf {output_target}',
  377. device_id=target_device)
  378. logging.info(f"Device '{target_device}': Target cleaned.")
  379. # On certain devices pushing files with adb fails with 'remote secure_mkdirs failed' error.
  380. # Creating all dirs first on target to surpass the issue.
  381. self.create_path_on_device(output_package, device_id=target_device)
  382. self.create_path_on_device(output_target, device_id=target_device)
  383. for asset_path in self.files_in_asset_path:
  384. if asset_path.is_dir():
  385. relative_path = asset_path.relative_to(self.local_asset_path).as_posix()
  386. target_path = f"{output_target}/{relative_path}"
  387. self.create_path_on_device(target_path, device_id=target_device)
  388. if self.clean_deploy or not target_timestamp:
  389. logging.info(f"Device '{target_device}': Pushing {len(self.files_in_asset_path)} files from {str(self.local_asset_path)} to device {output_target} ...")
  390. try:
  391. # '/.' is necessary in the source path to not copy folder 'assets' to destination, but its content.
  392. self.adb_call(arg_list=['push', f'{str(self.local_asset_path)}/.', output_target],
  393. device_id=target_device)
  394. except common.LmbrCmdError as err:
  395. # Something went wrong, clean up before leaving
  396. self.adb_shell(command=f'rm -rf {output_target}',
  397. device_id=target_device)
  398. raise err
  399. else:
  400. # If no clean was specified, individually inspect all files to see if it needs to be updated
  401. files_to_copy = []
  402. for asset_file in self.files_in_asset_path:
  403. # TODO: Check if the target exists in the destination as well?
  404. if AndroidDeployment.should_copy_file(asset_file, target_timestamp):
  405. files_to_copy.append(asset_file)
  406. if len(files_to_copy) > 0:
  407. logging.info(f"Copying {len(files_to_copy)} assets to device {target_device}")
  408. for src_path in files_to_copy:
  409. relative_path = src_path.relative_to(self.local_asset_path).as_posix()
  410. target_path = f"{output_target}/{relative_path}"
  411. self.adb_call(arg_list=['push', str(src_path), target_path],
  412. device_id=target_device)
  413. self.update_device_file_timestamp(relative_assets_path=output_target,
  414. device_id=target_device)
  415. def execute(self):
  416. """
  417. Execute the asset deployment
  418. """
  419. if self.is_test_project:
  420. if not self.apk_path.is_file():
  421. raise common.LmbrCmdError(f"Missing apk for {android_support.TEST_RUNNER_PROJECT} ({str(self.apk_path)}). Make sure it is built and is set as a signed APK.")
  422. else:
  423. if self.deployment_type in (AndroidDeployment.DEPLOY_APK_ONLY, AndroidDeployment.DEPLOY_BOTH):
  424. if not self.apk_path.is_file():
  425. raise common.LmbrCmdError(f"Missing apk for game {self.game_name} ({str(self.apk_path)}). Make sure it is built and is set as a signed APK.")
  426. if self.deployment_type in (AndroidDeployment.DEPLOY_ASSETS_ONLY, AndroidDeployment.DEPLOY_BOTH):
  427. if not self.local_asset_path.is_dir():
  428. raise common.LmbrCmdError(f"Missing {self.asset_type} assets for game {self.game_name} .")
  429. try:
  430. logging.debug("Starting ADB Server")
  431. self.adb_call('start-server')
  432. self.adb_started = True
  433. # Get the list of target devices to deploy to
  434. target_devices = self.get_target_android_devices()
  435. if not target_devices:
  436. raise common.LmbrCmdError("No connected and eligible android devices found")
  437. for target_device in target_devices:
  438. detected_storage = self.detect_device_storage_path(target_device)
  439. if not detected_storage:
  440. logging.warning(f"Unable to resolve storage path for device '{target_device}'. Skipping.")
  441. continue
  442. if self.is_test_project:
  443. # If this is the unit test runner, then only install the APK, assets are not applicable
  444. self.install_apk_to_device(target_device=target_device)
  445. else:
  446. # Otherwise install the apk and assets based on the deployment type
  447. if self.deployment_type in (AndroidDeployment.DEPLOY_APK_ONLY, AndroidDeployment.DEPLOY_BOTH):
  448. self.install_apk_to_device(target_device=target_device)
  449. if self.deployment_type in (AndroidDeployment.DEPLOY_ASSETS_ONLY, AndroidDeployment.DEPLOY_BOTH):
  450. # Make sure the APK is installed first
  451. android_package_name = self.get_android_project_settings(key_name='package_name',
  452. default_value='org.o3de.sdk')
  453. if not self.check_package_installed(package_name=android_package_name,
  454. target_device=target_device):
  455. raise common.LmbrCmdError(f"Unable to locate APK for {self.game_name} on device '{target_device}'. Make sure it is installed "
  456. f"first before installing the assets.")
  457. self.install_assets_to_device(detected_storage=detected_storage,
  458. target_device=target_device)
  459. logging.info(f"{self.game_name} deployed to device {target_device}")
  460. finally:
  461. if self.adb_started and self.kill_adb_server:
  462. self.adb_call('kill-server')