""" 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