Source code for adb_tool
import io
import os
import subprocess
import tempfile
from typing import Optional
import numpy as np
import adb_tool_py as adb_tool
import adb_tool_py.ui_node as ui_node
[docs]
class LogcatContext:
"""
A context manager for handling logcat subprocesses and output files.
"""
def __init__(self, subprocess: subprocess.Popen, file: io.TextIOWrapper, is_delete: bool = True):
"""
Initializes the LogcatContext class.
:param subprocess: The logcat subprocess.
:param file: The file to which logcat output is written.
:param is_delete: Whether to delete the file upon exit, defaults to True.
"""
self.subprocess = subprocess
self.file = file
self.is_delete = is_delete
def __enter__(self):
"""
Enters the context and returns the LogcatContext instance.
"""
return self
def __exit__(self, exc_type, exc_value, traceback):
"""
Exits the context, closing the file and terminating the subprocess.
"""
self.close()
[docs]
def open(self, mode: str = 'r') -> io.TextIOWrapper:
"""
Opens the logcat output file.
:param mode: The mode in which to open the file, defaults to 'r'.
:return: The opened file.
"""
return open(self.file.name, mode)
[docs]
def close(self) -> None:
"""
Closes the logcat output file and terminates the subprocess.
"""
self.file.close()
if self.is_delete:
os.remove(self.file.name)
self.subprocess.terminate()
self.subprocess.wait()
[docs]
class AdbTool:
"""
A class to interact with an Android device using ADB commands and UI interactions.
"""
def __init__(self, adb_command: Optional[adb_tool.AdbCommand] = None, adb: str = "adb", serial: Optional[str] = None):
"""
Initializes the AdbTool class.
:param adb_command: An instance of ADB command interface, defaults to None.
:param adb: Path to the ADB executable, defaults to "adb".
:param serial: The serial number of the target device, defaults to None.
"""
self.adb = adb_command
if adb_command is None:
self.adb = adb_tool.AdbCommand(adb, adb_tool.AdbDevice(serial))
self.avt = adb_tool.AdbViewTree(self.adb)
self.aic = adb_tool.AdbImageCV(self.adb)
[docs]
def query(self, cmd: str, stdout=subprocess.PIPE, stderr=subprocess.PIPE) -> subprocess.CompletedProcess:
"""
Executes an ADB command and waits for it to complete.
:param cmd: The command to execute.
:param stdout: Standard output pipe, defaults to subprocess.PIPE.
:param stderr: Standard error pipe, defaults to subprocess.PIPE.
:return: The completed process.
"""
return self.adb.query(cmd, stdout, stderr)
[docs]
def query_async(self, cmd: str, stdout=subprocess.PIPE, stderr=subprocess.PIPE) -> subprocess.Popen:
"""
Executes an ADB command asynchronously.
:param cmd: The command to execute.
:param stdout: Standard output pipe, defaults to subprocess.PIPE.
:param stderr: Standard error pipe, defaults to subprocess.PIPE.
:return: The process object.
"""
return self.adb.query_async(cmd, stdout, stderr)
[docs]
def logcat(self, output_file: Optional[io.TextIOWrapper] = None, cmd: str = '') -> LogcatContext:
"""
Starts a logcat subprocess and returns a LogcatContext for managing it.
:param output_file: The file to which logcat output is written, defaults to None.
:param cmd: Additional logcat command options, defaults to an empty string.
:return: A LogcatContext instance.
"""
is_delete = False
if output_file is None:
output_file = tempfile.NamedTemporaryFile(delete=False)
is_delete = True
sp = self.query_async(['logcat', *cmd.split(' ')], stdout=output_file)
return LogcatContext(sp, output_file, is_delete)
[docs]
def logcat_clear(self, cmd: str = '') -> subprocess.CompletedProcess:
"""
Clears the logcat logs on the device.
:param cmd: Additional logcat command options, defaults to an empty string.
:return: The completed process.
"""
return self.query(['logcat', '-c', *cmd.split(' ')])
[docs]
def logcat_dump(self, cmd: str = '') -> subprocess.CompletedProcess:
"""
Dumps the logcat logs from the device.
:param cmd: Additional logcat command options, defaults to an empty string.
:return: The completed process.
"""
return self.query(['logcat', '-d', *cmd.split(' ')])
[docs]
def capture_tree(self) -> None:
"""
Captures the current UI hierarchy of the connected Android device.
"""
self.avt.capture()
[docs]
def capture_screenshot(self) -> None:
"""
Captures the current screen of the connected Android device.
"""
self.aic.capture()
[docs]
def content_tree(self) -> ui_node.UINode:
"""
Returns the root node of the captured UI hierarchy.
:return: The root UINode.
"""
return self.avt.content_tree
[docs]
def content_screen(self) -> np.ndarray:
"""
Returns the captured screen as an OpenCV image.
:return: The captured screen as an OpenCV image.
"""
return self.aic.content_cv
[docs]
def find_texts(self, text: str, root_node: Optional[ui_node.UINode] = None, is_capture: bool = False) -> list[ui_node.UINode]:
"""
Finds nodes by their text attribute.
:param text: The text to search for.
:param root_node: The root node to start the search from, defaults to None.
:param is_capture: Whether to capture the UI hierarchy before searching, defaults to False.
:return: A list of matching UINodes.
"""
return self.avt.find_texts(text, root_node, is_capture)
[docs]
def find_text(self, text: str, index: int = 0, root_node: Optional[ui_node.UINode] = None, is_capture: bool = False) -> Optional[ui_node.UINode]:
"""
Finds a node by its text attribute.
:param text: The text to search for.
:param index: The index of the matching node to return, defaults to 0.
:param root_node: The root node to start the search from, defaults to None.
:param is_capture: Whether to capture the UI hierarchy before searching, defaults to False.
:return: The matching UINode, or None if not found.
"""
return self.avt.find_text(text, index, root_node, is_capture)
[docs]
def find_resource_id(self, resource_id: str, index: int = 0, root_node: Optional[ui_node.UINode] = None, is_capture: bool = False) -> Optional[ui_node.UINode]:
"""
Finds a node by its resource-id attribute.
:param resource_id: The resource-id to search for.
:param index: The index of the matching node to return, defaults to 0.
:param root_node: The root node to start the search from, defaults to None.
:param is_capture: Whether to capture the UI hierarchy before searching, defaults to False.
:return: The matching UINode, or None if not found.
"""
return self.avt.find_resource_id(resource_id, index, root_node, is_capture)
[docs]
def check_text(self, text: str, index: int = 0, root_node: Optional[ui_node.UINode] = None, is_capture: bool = False) -> bool:
"""
Checks if a node with the specified text exists.
:param text: The text to search for.
:param index: The index of the matching node to return, defaults to 0.
:param root_node: The root node to start the search from, defaults to None.
:param is_capture: Whether to capture the UI hierarchy before searching, defaults to False.
:return: True if the node exists, False otherwise.
"""
return self.avt.check_text(text, index, root_node, is_capture)
[docs]
def check_resource_id(self, resource_id: str, index: int = 0, root_node: Optional[ui_node.UINode] = None, is_capture: bool = False) -> bool:
"""
Checks if a node with the specified resource-id exists.
:param resource_id: The resource-id to search for.
:param index: The index of the matching node to return, defaults to 0.
:param root_node: The root node to start the search from, defaults to None.
:param is_capture: Whether to capture the UI hierarchy before searching, defaults to False.
:return: True if the node exists, False otherwise.
"""
return self.avt.check_resource_id(resource_id, index, root_node, is_capture)
[docs]
def touch_text(self, text: str, index: int = 0, root_node: Optional[ui_node.UINode] = None, is_capture: bool = False) -> bool:
"""
Simulates a tap on the node with the specified text.
:param text: The text to search for.
:param index: The index of the matching node to return, defaults to 0.
:param root_node: The root node to start the search from, defaults to None.
:param is_capture: Whether to capture the UI hierarchy before searching, defaults to False.
:return: True if the tap was successful, False otherwise.
"""
return self.avt.touch_text(text, index, root_node, is_capture)
[docs]
def touch_resource_id(self, resource_id: str, index: int = 0, root_node: Optional[ui_node.UINode] = None, is_capture: bool = False) -> bool:
"""
Simulates a tap on the node with the specified resource-id.
:param resource_id: The resource-id to search for.
:param index: The index of the matching node to return, defaults to 0.
:param root_node: The root node to start the search from, defaults to None.
:param is_capture: Whether to capture the UI hierarchy before searching, defaults to False.
:return: True if the tap was successful, False otherwise.
"""
return self.avt.touch_resource_id(resource_id, index, root_node, is_capture)
[docs]
def find_images(self, image_path: str, match_threshold: float = 0.99, merge_threshold: int = 10) -> list[tuple[int, int, int, int]]:
"""
Finds instances of a specified image within the captured screen image.
:param image_path: Path to the image file to search for.
:param match_threshold: Threshold for image matching, defaults to 0.99.
:param merge_threshold: Threshold for merging close rectangles, defaults to 10.
:return: A list of rectangles where the image was found.
"""
return self.aic.find_images(image_path, match_threshold, merge_threshold)
[docs]
def find_image(self, image_path: str, index: int = 0, is_capture: bool = False, match_threshold: float = 0.99, merge_threshold: int = 10) -> Optional[tuple[int, int, int, int]]:
"""
Finds the specified image on the screen and returns the rectangle of the match.
:param image_path: Path to the image file to search for.
:param index: Index of the matching image rectangle to return, defaults to 0.
:param is_capture: Whether to capture the screen before searching, defaults to False.
:param match_threshold: Threshold for image matching, defaults to 0.99.
:param merge_threshold: Threshold for merging close rectangles, defaults to 10.
:return: The rectangle of the found image, or None if not found.
"""
return self.aic.find_image(image_path, index, is_capture, match_threshold, merge_threshold)
[docs]
def touch_image(self, image_path: str, index: int = 0, is_capture: bool = False, match_threshold: float = 0.99, merge_threshold: int = 10) -> bool:
"""
Simulates a tap on the screen at the center of the specified image if found.
:param image_path: Path to the image file to search for.
:param index: Index of the matching image rectangle to use, defaults to 0.
:param is_capture: Whether to capture the screen before searching, defaults to False.
:param match_threshold: Threshold for image matching, defaults to 0.99.
:param merge_threshold: Threshold for merging close rectangles, defaults to 10.
:raises ValueError: If the image is not found.
:return: True if the tap was successful, False otherwise.
"""
return self.aic.touch_image(image_path, index, is_capture, match_threshold, merge_threshold)
[docs]
def check_image(self, image_path: str, index: int = 0, is_capture: bool = False, match_threshold: float = 0.99, merge_threshold: int = 10) -> bool:
"""
Checks if the specified image is present on the screen.
:param image_path: Path to the image file to search for.
:param index: Index of the matching image rectangle to use, defaults to 0.
:param is_capture: Whether to capture the screen before searching, defaults to False.
:param match_threshold: Threshold for image matching, defaults to 0.99.
:param merge_threshold: Threshold for merging close rectangles, defaults to 10.
:return: True if the image is found, False otherwise.
"""
return self.aic.check_image(image_path, index, is_capture, match_threshold, merge_threshold)