from __future__ import annotations from typing import List import logging import os import pathlib import subprocess from enum import Enum, unique from typing import Optional from massapk import pkg_root, runtime_platform from massapk.exceptions import MassApkError from massapk.helpers import PLATFORM log = logging.getLogger(__name__) class AdbError(MassApkError): pass class ApkAlreadyExists(AdbError): pass class Adb(object): @unique class ConnectionState(Enum): """Define adb-server state enum""" CONNECTED = True DISCONNECTED = False @classmethod def _get_adb_path(cls) -> os.PathLike: """Return adb path based on operating system detected during import""" if runtime_platform == PLATFORM.OSX: path = os.path.join(pkg_root, "bin", "osx", "adb") elif runtime_platform == PLATFORM.WIN: path = os.path.join(pkg_root, "bin", "win", "adb.exe") elif runtime_platform == PLATFORM.LINUX: path = os.path.join(pkg_root, "bin", "linux", "adb") else: raise RuntimeError("Unsupported runtime platform") return pathlib.Path(path) def __init__(self, auto_connect=False): self._path = self._get_adb_path() self._state = self.__class__.ConnectionState.DISCONNECTED if auto_connect: self.start_server() def __enter__(self): try: self.start_server() except AdbError: self.stop_server() self.start_server() return self def __exit__(self, exc_type, exc, exc_tb): self.stop_server() @property def path(self): """Get access to detected adb path""" return self._path @property def state(self) -> ConnectionState: """ Gets connection state of adb server If `adb-server` state is `device` then phone is connected """ return self._update_state() def _update_state(self) -> ConnectionState: """Checks if an android phone is connected to adb-server via cable.""" command_output = self.exec_command("get-state", return_stdout=True, silence_errors=True) if command_output and "error" not in command_output: log.warning("No phone connected waiting to connect phone") self._state = self.__class__.ConnectionState.CONNECTED return self._state def start_server(self): """Starts adb-server process.""" log.info("Starting adb server...") self.exec_command("start-server") def stop_server(self): """Kills adb server.""" log.info("Killing adb server...") self.exec_command("kill-server") def exec_command(self, cmd, return_stdout=False, case_sensitive=False, silence_errors=False) -> Optional[str]: """Low level function to send shell commands to running adb-server process. :raises AdbError """ cmd = f"{self._path} {cmd}" log.info("Executing " + cmd) return_code, output = subprocess.getstatusoutput(cmd) if return_code: if silence_errors: log.error(output) return None if output is None: log.warning(f"command returned error code {return_code}, but no output, {cmd}") raise AdbError(f"Command returned error code {cmd}") raise AdbError(output + f" {cmd}") if return_stdout: return output.lower() if case_sensitive else output return None def push(self, source_path, ignore_errors=True) -> None: """Pushes apk package to android device. Before calling `push` function make sure function `connect` has been called earlier and `self.state` value is set to `connected` extra parameters are passed to adb-server in order to avoid errors like the following faulty error messages: `operation failed apk is already installed on the device` `operation failed apk version is lower than the one currently installed on the device` -d allow apk version down grade -r reinstall apk if already installed on device """ try: self.exec_command(f"install -d -r {source_path}") except AdbError as error: log.warning(repr(error)) if ignore_errors: return raise error from None def pull(self, apk_path: str) -> None: """Pull's an apk from the following path in the android device.""" self.exec_command(cmd=f" pull {apk_path}") def list_device(self, flag: str) -> Optional[List[str, str]]: """Lists installed apk packages on the android device. Results can be filtered with PKG_FILTER to get only apk packages you are interested. Defaults to list 3d party apps. list packages [-f] [-d] [-e] [-s] [-3] [-i] [-l] [-u] [-U] [--show-versioncode] [--apex-only] [--uid UID] [--user USER_ID] [FILTER] Prints all packages; optionally only those whose name contains the text in FILTER. Options are: -f: see their associated file -a: all known packages (but excluding APEXes) -d: filter to only show disabled packages -e: filter to only show enabled packages -s: filter to only show system packages -3: filter to only show third party packages -i: see the installer for the packages -l: ignored (used for compatibility with older releases) -U: also show the package UID -u: also include uninstalled packages """ log.info("Listing installed apk's in the device ...") output = self.exec_command(f"shell pm list packages -{flag}", return_stdout=True, case_sensitive=True,) # adb returns packages name in the form # package:com.skype.raider # we need to strip "package:" prefix if output: return [ line.split(":", maxsplit=1)[1].strip() for line in output.splitlines() if line.startswith("package:") ]