autool/DroidBot/core/abstract_device_state.py
2026-06-17 19:44:18 +08:00

257 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Abstract Device State Base Class
Platform-agnostic device state interface.
"""
from abc import ABC, abstractmethod
from typing import Optional, Dict, Any, List, Set
try:
from typing import TypedDict
except ImportError:
from typing_extensions import TypedDict
import os
class ViewDict(TypedDict, total=False):
"""
统一的视图/控件字典结构
所有平台Android/iOS/Web和来源Accessibility Tree/CV
都必须输出此格式,确保事件处理代码可以统一处理。
"""
# === 必需字段 ===
bounds: List[List[int]] # [[x1, y1], [x2, y2]] 坐标
text: str # 文本内容
visible: bool # 是否可见
enabled: bool # 是否启用
clickable: bool # 是否可点击
editable: bool # 是否可编辑
scrollable: bool # 是否可滚动
children: List[int] # 子节点索引列表
view_str: str # 唯一标识符(自动生成)
# === 可选字段 ===
content_description: str # 无障碍描述
resource_id: str # 资源ID
class_name: str # 控件类名(避免 'class' 关键字)
temp_id: int # 临时索引
parent: int # 父节点索引
source: str # 来源: 'accessibility' 或 'cv'
# === 扩展字段(特定平台可能需要)===
signature: str # 内容签名
long_clickable: bool # 是否可长按
checkable: bool # 是否可勾选
checked: bool # 是否已勾选
selected: bool # 是否已选中
class AbstractDeviceState(ABC):
"""
设备状态抽象基类
定义了设备状态的标准接口,包括:
- 视图/控件信息
- 状态标识
- 可能的输入事件
- 前台活动信息
"""
def __init__(self, device: 'AbstractDevice', tag: Optional[str] = None,
screenshot_path: Optional[str] = None):
"""
初始化设备状态
:param device: 设备对象
:param tag: 状态标签
:param screenshot_path: 截图路径
"""
self.device = device
self._screenshot_path = screenshot_path
if tag is None:
from datetime import datetime
tag = datetime.now().strftime("%Y-%m-%d_%H%M%S")
self.tag = tag
# 缓存的状态信息
self._state_str = None
self._possible_events = None
self._views = None
# ==================== 视图信息 ====================
@property
@abstractmethod
def views(self) -> List[Dict[str, Any]]:
"""
获取当前界面的所有视图/控件元素
:return: 视图字典列表,每个字典包含控件的属性信息
"""
pass
@property
def view_tree(self) -> Dict[str, Any]:
"""
获取视图树结构(可选实现)
:return: 视图树字典
"""
return {}
@property
def cv_views(self) -> List['ViewDict']:
"""
获取 CV 检测到的视图列表
:return: CV 视图列表,符合 ViewDict 格式
"""
return []
# ==================== 状态标识 ====================
@property
@abstractmethod
def state_str(self) -> str:
"""
获取状态的唯一标识字符串
:return: 状态标识字符串(通常是 MD5 哈希)
"""
pass
@property
def structure_str(self) -> str:
"""
获取状态的结构标识(忽略内容)
:return: 结构标识字符串
"""
return self.state_str
@property
def search_content(self) -> str:
"""
获取用于搜索的文本内容(可选,用于 UTG 可视化)
:return: 搜索内容字符串,默认返回空字符串
"""
return ""
# ==================== 输入事件 ====================
@abstractmethod
def get_possible_input(self) -> List['AbstractInputEvent']:
"""
获取当前状态可能的输入事件列表
:return: 可能的输入事件列表
"""
pass
# ==================== 活动/页面信息 ====================
@property
@abstractmethod
def foreground_page(self) -> Optional[str]:
"""
获取前台页面/窗口标识
:return: 前台页面名称,如果无法获取则返回 None
"""
pass
# ==================== 截图 ====================
@property
def screenshot_path(self) -> Optional[str]:
"""获取截图路径"""
return self._screenshot_path
@screenshot_path.setter
def screenshot_path(self, value: str):
"""设置截图路径"""
self._screenshot_path = value
# ==================== 屏幕尺寸 ====================
@property
def width(self) -> int:
"""获取屏幕宽度"""
return self.device.get_width()
@property
def height(self) -> int:
"""获取屏幕高度"""
return self.device.get_height()
# ==================== 序列化 ====================
@abstractmethod
def to_dict(self) -> Dict[str, Any]:
"""
序列化为字典
:return: 状态信息字典
"""
pass
def to_json(self) -> str:
"""
序列化为 JSON 字符串
:return: JSON 字符串
"""
import json
return json.dumps(self.to_dict(), indent=2)
# ==================== 状态保存 ====================
def save2dir(self, output_dir: Optional[str] = None) -> None:
"""
保存状态到目录,使用 flush + fsync 确保数据落盘
:param output_dir: 输出目录,默认使用设备的输出目录
"""
try:
if output_dir is None:
if self.device.output_dir is None:
return
output_dir = os.path.join(self.device.output_dir, "states")
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 保存状态 JSON
dest_state_json_path = os.path.join(output_dir, f"state_{self.tag}.json")
with open(dest_state_json_path, "w") as f:
f.write(self.to_json())
f.flush()
os.fsync(f.fileno())
# 复制截图
if self.screenshot_path and os.path.exists(self.screenshot_path):
import shutil
ext = os.path.splitext(self.screenshot_path)[1]
dest_screenshot_path = os.path.join(output_dir, f"screen_{self.tag}{ext}")
if os.path.abspath(self.screenshot_path) != os.path.abspath(dest_screenshot_path):
shutil.copyfile(self.screenshot_path, dest_screenshot_path)
self._screenshot_path = dest_screenshot_path
except Exception as e:
self.device.logger.error(f"Error saving state: {e}")
# ==================== 辅助方法 ====================
@staticmethod
def get_view_center(view_dict: Dict[str, Any]) -> tuple:
"""
获取视图中心点坐标
:param view_dict: 视图字典
:return: (x, y) 坐标元组
"""
bounds = view_dict.get('bounds', [[0, 0], [0, 0]])
return (bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2