autool/DroidBot/platforms/android/android_device_state.py
2026-06-17 19:44:18 +08:00

360 lines
14 KiB
Python
Raw Permalink 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.

"""
Android Device State Implementation
Concrete implementation of AbstractDeviceState for Android devices.
"""
import copy
import os
from typing import Optional, Dict, Any, List, Set
from ...core.abstract_device_state import AbstractDeviceState
from ...core.abstract_input_event import EventType
from ...exceptions import FATAL_EXCEPTIONS
class AndroidDeviceState(AbstractDeviceState):
"""
Android 设备状态的具体实现
"""
def __init__(self, device, views, foreground_activity, activity_stack,
background_services, tag=None, screenshot_path=None):
"""
初始化 Android 设备状态
:param device: AndroidDevice 实例
:param views: 视图列表
:param foreground_activity: 前台 Activity
:param activity_stack: Activity 栈
:param background_services: 后台服务
:param tag: 状态标签
:param screenshot_path: 截图路径
"""
super().__init__(device, tag, screenshot_path)
self._foreground_activity = foreground_activity
self.activity_stack = activity_stack if isinstance(activity_stack, list) else []
self.background_services = background_services
# 解析视图
self._views = self._parse_views(views)
self._view_tree = {}
self._assemble_view_tree(self._view_tree, self._views)
self._generate_view_strs()
# 计算状态标识
self._state_str = self._get_state_str()
self._structure_str = self._get_content_free_state_str()
self._search_content = self._get_search_content()
# ==================== 属性实现 ====================
@property
def search_content(self) -> str:
return self._search_content
@property
def views(self) -> List[Dict[str, Any]]:
return self._views
@property
def view_tree(self) -> Dict[str, Any]:
return self._view_tree
@property
def state_str(self) -> str:
return self._state_str
@property
def structure_str(self) -> str:
return self._structure_str
@property
def foreground_page(self) -> Optional[str]:
"""实现抽象接口 - 返回当前页面标识符Android 上就是 foreground_activity"""
return self._foreground_activity
def _parse_views(self, raw_views) -> List[Dict[str, Any]]:
"""解析原始视图数据,确保符合 ViewDict 格式"""
views = []
if not raw_views or len(raw_views) == 0:
return views
for view_dict in raw_views:
# 添加来源标识
view_dict['source'] = 'accessibility'
views.append(view_dict)
return views
def _assemble_view_tree(self, root_view, views) -> None:
"""组装视图树"""
if not len(self._view_tree):
if not len(views):
return
self._view_tree = copy.deepcopy(views[0])
self._assemble_view_tree(self._view_tree, views)
else:
children = list(enumerate(root_view.get("children", [])))
if not len(children):
return
for i, j in children:
if j < len(self._views):
root_view["children"][i] = copy.deepcopy(self._views[j])
self._assemble_view_tree(root_view["children"][i], views)
def _generate_view_strs(self) -> None:
"""生成视图字符串标识"""
for view_dict in self._views:
self._get_view_str(view_dict)
# ==================== 状态标识 ====================
def _get_state_str(self) -> str:
"""获取状态唯一标识"""
state_str_raw = self._get_state_str_raw()
from .utils import md5
return md5(state_str_raw)
def _get_state_str_raw(self) -> str:
"""获取原始状态字符串"""
if self.device.humanoid is not None:
import json
from xmlrpc.client import ServerProxy
proxy = ServerProxy("http://%s/" % self.device.humanoid)
return proxy.render_view_tree(json.dumps({
"view_tree": self._view_tree,
"screen_res": [self.device.display_info["width"],
self.device.display_info["height"]]
}))
else:
view_signatures = set()
for view in self._views:
view_signature = self._get_view_signature(view)
if view_signature:
view_signatures.add(view_signature)
return "%s{%s}" % (self._foreground_activity, ",".join(sorted(view_signatures)))
def _get_content_free_state_str(self) -> str:
"""获取内容无关的状态标识"""
view_signatures = set()
for view in self._views:
view_signature = self._get_content_free_view_signature(view)
if view_signature:
view_signatures.add(view_signature)
state_str = "%s{%s}" % (self._foreground_activity, ",".join(sorted(view_signatures)))
import hashlib
return hashlib.md5(state_str.encode('utf-8')).hexdigest()
def _get_search_content(self) -> str:
"""获取搜索内容"""
words = [
",".join(self._get_property_from_all_views("resource_id")),
",".join(self._get_property_from_all_views("text"))
]
return "\n".join(words)
def _get_property_from_all_views(self, property_name: str) -> Set[str]:
"""从所有视图获取属性值"""
property_values = set()
for view in self._views:
property_value = self._safe_dict_get(view, property_name, None)
if property_value:
property_values.add(property_value)
return property_values
# ==================== 视图签名 ====================
@staticmethod
def _get_view_signature(view_dict: Dict[str, Any]) -> Optional[str]:
"""获取视图签名"""
if 'signature' in view_dict:
return view_dict['signature']
view_text = AndroidDeviceState._safe_dict_get(view_dict, 'text', "None")
if view_text is None or len(view_text) > 50:
view_text = "None"
signature = "[class]%s[resource_id]%s[text]%s[%s,%s,%s]" % (
AndroidDeviceState._safe_dict_get(view_dict, 'class_name', "None"),
AndroidDeviceState._safe_dict_get(view_dict, 'resource_id', "None"),
view_text,
AndroidDeviceState._key_if_true(view_dict, 'enabled'),
AndroidDeviceState._key_if_true(view_dict, 'checked'),
AndroidDeviceState._key_if_true(view_dict, 'selected')
)
view_dict['signature'] = signature
return signature
@staticmethod
def _get_content_free_view_signature(view_dict: Dict[str, Any]) -> Optional[str]:
"""获取内容无关的视图签名"""
if 'content_free_signature' in view_dict:
return view_dict['content_free_signature']
content_free_signature = "[class]%s[resource_id]%s" % (
AndroidDeviceState._safe_dict_get(view_dict, 'class_name', "None"),
AndroidDeviceState._safe_dict_get(view_dict, 'resource_id', "None")
)
view_dict['content_free_signature'] = content_free_signature
return content_free_signature
def _get_view_str(self, view_dict: Dict[str, Any]) -> str:
"""获取视图字符串"""
if 'view_str' in view_dict:
return view_dict['view_str']
view_signature = self._get_view_signature(view_dict)
parent_strs = []
for parent_id in self.get_all_ancestors(view_dict):
parent_strs.append(self._get_view_signature(self._views[parent_id]))
parent_strs.reverse()
child_strs = []
for child_id in self.get_all_children(view_dict):
child_strs.append(self._get_view_signature(self._views[child_id]))
child_strs.sort()
view_str = "Activity:%s\nSelf:%s\nParents:%s\nChildren:%s" % (
self._foreground_activity, view_signature,
"//".join(parent_strs), "||".join(child_strs)
)
import hashlib
view_str = hashlib.md5(view_str.encode('utf-8')).hexdigest()
view_dict['view_str'] = view_str
return view_str
# ==================== 辅助方法 ====================
@staticmethod
def _key_if_true(view_dict: Dict[str, Any], key: str) -> str:
return key if (key in view_dict and view_dict[key]) else ""
@staticmethod
def _safe_dict_get(view_dict: Dict[str, Any], key: str, default=None):
value = view_dict.get(key, None)
return value if value is not None else default
def get_all_ancestors(self, view_dict: Dict[str, Any]) -> List[int]:
"""获取所有祖先节点 ID"""
result = []
parent_id = self._safe_dict_get(view_dict, 'parent', -1)
if 0 <= parent_id < len(self._views):
result.append(parent_id)
result += self.get_all_ancestors(self._views[parent_id])
return result
def get_all_children(self, view_dict: Dict[str, Any]) -> Set[int]:
"""获取所有子节点 ID"""
children = self._safe_dict_get(view_dict, 'children')
if not children:
return set()
children = set(children)
for child in list(children):
if child < len(self._views):
children_of_child = self.get_all_children(self._views[child])
children = children.union(children_of_child)
return children
# ==================== 抽象方法实现 ====================
def get_possible_input(self) -> List:
"""获取可能的输入事件"""
if self._possible_events:
return [] + self._possible_events
from .android_input_event import (
AndroidTouchEvent, AndroidLongTouchEvent,
AndroidScrollEvent, AndroidSetTextEvent
)
possible_events = []
enabled_view_ids = []
touch_exclude_view_ids = set()
for view_dict in self._views:
if (self._safe_dict_get(view_dict, 'enabled') and
self._safe_dict_get(view_dict, 'visible') and
self._safe_dict_get(view_dict, 'resource_id') not in
['android:id/navigationBarBackground', 'android:id/statusBarBackground']):
enabled_view_ids.append(view_dict['temp_id'])
for view_id in enabled_view_ids:
if self._safe_dict_get(self._views[view_id], 'clickable'):
possible_events.append(AndroidTouchEvent(view=self._views[view_id]))
touch_exclude_view_ids.add(view_id)
touch_exclude_view_ids = touch_exclude_view_ids.union(
self.get_all_children(self._views[view_id])
)
# 添加滚动事件
possible_events.append(AndroidScrollEvent(direction="up"))
possible_events.append(AndroidScrollEvent(direction="up"))
possible_events.append(AndroidScrollEvent(direction="up"))
possible_events.append(AndroidScrollEvent(direction="down"))
for view_id in enabled_view_ids:
if self._safe_dict_get(self._views[view_id], 'checkable'):
possible_events.append(AndroidTouchEvent(view=self._views[view_id]))
touch_exclude_view_ids.add(view_id)
for view_id in enabled_view_ids:
if self._safe_dict_get(self._views[view_id], 'long_clickable'):
possible_events.append(AndroidLongTouchEvent(view=self._views[view_id]))
for view_id in enabled_view_ids:
if self._safe_dict_get(self._views[view_id], 'editable'):
possible_events.append(AndroidSetTextEvent(
view=self._views[view_id], text="cat"
))
touch_exclude_view_ids.add(view_id)
for view_id in enabled_view_ids:
if view_id in touch_exclude_view_ids:
continue
children = self._safe_dict_get(self._views[view_id], 'children')
if children and len(children) > 0:
continue
possible_events.append(AndroidTouchEvent(view=self._views[view_id]))
self._possible_events = possible_events
return [] + possible_events
def to_dict(self) -> Dict[str, Any]:
"""序列化为字典"""
return {
'tag': self.tag,
'state_str': self._state_str,
'state_str_content_free': self._structure_str,
'foreground_activity': self._foreground_activity,
'activity_stack': self.activity_stack,
'background_services': self.background_services,
'width': self.width,
'height': self.height,
'views': self._views
}
def get_app_page_depth(self) -> int:
"""获取应用页面深度 - 实现抽象接口"""
if not self.device._app:
return 0 # 没有指定应用时返回0在前台
package_name = self.device.app_identifier
if self._foreground_activity and package_name in self._foreground_activity:
return 0
try:
app_pid = self.device.get_app_pid(package_name)
if app_pid:
return 0
except FATAL_EXCEPTIONS:
raise
except Exception as e:
self.device.logger.error(f"Exception checking app PID: {e}")
return -1 # 非致命异常,返回默认深度
depth = 0
for activity_str in self.activity_stack:
if package_name in activity_str:
return depth
depth += 1
return -1