blob: 88148f778f8be4930d117ae6d8b33fc18d2430d7 [file] [log] [blame]
"""UI Automator controller in python layer.
A python controller that can trigger mobly UI automator snippet to achieve some
automated UI operations on Android phones.
"""
import logging
import os
from mobly import utils
from mobly.controllers import android_device
from mobly.controllers.android_device_lib import adb
from mobly.snippet import errors as snippet_errors
from ui_automator import errors
_MOBLY_SNIPPET_APK: str = 'com.chip.interop.moblysnippet'
_MOBLY_SNIPPET_APK_NAME: str = 'mbs'
class UIAutomator:
"""UI Automator controller in python layer."""
def __init__(self, logger: logging.Logger = logging.getLogger()):
"""Inits UIAutomator.
Interacts with android device by pre-installed apk. Can perform
methods provided by a snippet installed on the android device.
Args:
logger: Injected logger, if None, specify the default(root) one.
"""
self._logger = logger
self._connected_device: android_device.AndroidDevice | None = None
def load_device(self):
"""Selects the first connected android device.
Raises:
NoAndroidDeviceError: When no device is connected.
"""
try:
android_devices = android_device.get_all_instances()
except adb.AdbError as exc:
raise errors.AdbError(
'Please install adb and add it to your PATH environment variable.'
) from exc
if not android_devices:
raise errors.NoAndroidDeviceError(
'No Android device connected to the host computer.'
)
self._connected_device = android_devices[0]
self._logger.info(f'connected device: [{self._connected_device}]')
def load_snippet(self):
"""Loads needed mobly snippet installed on android device.
Raises:
NoAndroidDeviceError: When no device is connected.
AndroidDeviceNotReadyError: When required snippet apk can not be loaded
on device.
"""
if not self._connected_device:
raise errors.NoAndroidDeviceError(
'No Android device connected to the host computer.'
)
if not self._is_apk_installed(self._connected_device, _MOBLY_SNIPPET_APK):
self._install_apk(self._connected_device, self._get_mbs_apk_path())
try:
self._connected_device.load_snippet(
_MOBLY_SNIPPET_APK_NAME, _MOBLY_SNIPPET_APK
)
except snippet_errors.ServerStartPreCheckError as exc:
# Raises when apk not be installed or instrumented on device.
raise errors.AndroidDeviceNotReadyError(
f"Check device({self._connected_device.device_info['serial']}) has"
' installed required apk.'
) from exc
except (
snippet_errors.ServerStartError,
snippet_errors.ProtocolError,
) as exc:
raise errors.AndroidDeviceNotReadyError(
f"Check device({self._connected_device.device_info['serial']}) can"
' load required apk.'
) from exc
except android_device.SnippetError as e:
# Error raises when registering a package twice or a package with
# duplicated name. Thus, It is okay to pass here as long as the snippet
# has been loaded.
self._logger.debug(str(e))
def _is_apk_installed(
self, device: android_device.AndroidDevice, package_name: str
) -> bool:
"""Checks if given package is already installed on the Android device.
Args:
device: An AndroidDevice object.
package_name: The Android app package name.
Raises:
adb.AdbError: When executing adb command fails.
Returns:
True if package is installed. False otherwise.
"""
out = device.adb.shell(['pm', 'list', 'package'])
return bool(utils.grep('^package:%s$' % package_name, out))
def _install_apk(
self, device: android_device.AndroidDevice, apk_path: str
) -> None:
"""Installs required apk snippet to the given Android device.
Args:
device: An AndroidDevice object.
apk_path: The absolute file path where the apk is located.
"""
device.adb.install(['-r', '-g', apk_path])
def _get_mbs_apk_path(self) -> str:
return os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'android',
'app',
'snippet-0.0.0.apk',
)
if __name__ == '__main__':
UIAutomator().load_device()