# coding: utf-8 """ Apple ID 登录和权限处理器 基于 IOSAutomationBase 实现的 Apple ID 登录和应用权限处理功能 """ from typing import Optional, Dict import time # 使用绝对导入 try: from utils_ios.package_init.ios_automation_base import IOSAutomationBase, logger except ImportError: from ios_automation_base import IOSAutomationBase, logger class AppleIDSigner(IOSAutomationBase): """Apple ID 登录和权限处理器 提供以下功能: - 自动处理应用权限弹窗 - 自动跳过应用引导页 - 查找并点击 Apple ID 登录按钮 - 跳过 Apple ID 登录 """ # Apple ID 登录相关的按钮文案 LOGIN_BUTTONS = [ # 中文 "使用 Apple ID登录", "使用 Apple 账号登录", "Apple 账号登录", "使用现有Apple ID", "通过 Apple 继续", "用Apple ID登录", # 英文 "Sign in with Apple ID", "Use Existing Apple ID", "Sign In with Apple ID", ] SKIP_LOGIN_BUTTONS = [ # 中文 "跳过", "稍后", "以后再说", "不使用Apple ID", # 英文 "Skip", "Not Now", "Skip this step", "Don't Use", ] # 引导页跳过按钮文案 SKIP_ONBOARDING_BUTTONS = [ # 中文 "跳过", "略过", "暂时不用", "以后再说", "稍后", "下次再说", # 英文 "Skip", "Skip for now", "Not now", "Maybe later", "Later", ] # 引导页继续按钮文案(如果找不到跳过按钮) CONTINUE_ONBOARDING_BUTTONS = [ # 中文 "继续", "下一步", "开始", "知道了", "我知道了", "完成", # 英文 "Continue", "Next", "Start", "Get Started", "Got it", "Done", ] def __init__(self, server_url: str, bundle_id: str, alert_strategy: str = "allow", skip_onboarding: bool = True): """ 初始化 Apple ID 登录器 Args: server_url: WDA Server URL bundle_id: 应用的 Bundle ID alert_strategy: Alert 处理策略 - "allow" 或 "deny" skip_onboarding: 是否跳过引导页 """ super().__init__(server_url, default_alert_strategy=alert_strategy) self.bundle_id = bundle_id self.skip_onboarding_enabled = skip_onboarding def _try_skip_onboarding_once(self) -> bool: """ 尝试跳过引导页(单次) Returns: 是否成功跳过了一个引导页 """ # 优先尝试跳过按钮(关闭 auto_handle_alert,外层已单独处理) for btn_text in self.SKIP_ONBOARDING_BUTTONS: clicked, _ = self.finder.find_and_click([btn_text], timeout=0.5, auto_handle_alert=False) if clicked: logger.debug(f"跳过引导页: {btn_text}") return True # 如果没找到跳过按钮,尝试继续按钮 for btn_text in self.CONTINUE_ONBOARDING_BUTTONS: clicked, _ = self.finder.find_and_click([btn_text], timeout=0.5, auto_handle_alert=False) if clicked: logger.debug(f"跳过引导页: {btn_text}") return True return False def _handle_permissions_and_onboarding(self, timeout: float = 30.0) -> Dict[str, int]: """ 处理权限弹窗和引导页 注意:Alert 权限弹窗会在每次控件操作前自动处理 此方法主动检查可以更快地处理弹窗 Args: timeout: 超时时间 Returns: 处理结果统计 {"permissions_handled": int, "onboarding_skipped": int} """ result = { "permissions_handled": 0, "onboarding_skipped": 0 } end_time = time.time() + timeout idle_count = 0 # 连续无操作计数 while time.time() < end_time: handled_something = False # 1. 主动处理 Alert 权限弹窗 alerts_handled = self.popup_handler.handle_alert(max_attempts=1) if alerts_handled > 0: result["permissions_handled"] += alerts_handled logger.debug(f"处理权限 Alert (总计: {result['permissions_handled']})") handled_something = True idle_count = 0 time.sleep(0.5) # 2. 尝试跳过引导页 if self.skip_onboarding_enabled: if self._try_skip_onboarding_once(): result["onboarding_skipped"] += 1 logger.debug(f"跳过引导页 (总计: {result['onboarding_skipped']})") handled_something = True idle_count = 0 time.sleep(0.5) # 3. 如果什么都没处理,短暂等待 if not handled_something: idle_count += 1 # 连续 2 次没有操作,认为已完成 if idle_count >= 2: logger.debug("连续多次无操作,退出权限和引导页处理循环") break time.sleep(0.5) return result def _click_login_button(self) -> bool: """查找并点击 Apple ID 登录按钮""" clicked, label = self.finder.find_and_click(self.LOGIN_BUTTONS, timeout=5.0) if clicked: logger.info(f"点击登录按钮: {label}") return True logger.warning("未找到 Apple ID 登录按钮") return False def _skip_login_button(self) -> bool: """查找并点击跳过登录按钮""" clicked, label = self.finder.find_and_click(self.SKIP_LOGIN_BUTTONS, timeout=2.0) if clicked: logger.info(f"跳过登录: {label}") return True logger.warning("未找到跳过登录按钮") return False def execute(self, timeout: float = 60.0, try_login: bool = True) -> bool: """ 执行 Apple ID 登录和权限处理 处理流程: 1. 启动应用 2. 处理权限弹窗和引导页 3. 如果 try_login=True,尝试点击登录按钮;找不到则尝试跳过 4. 如果 try_login=False,仅处理权限和引导页 Args: timeout: 超时时间 try_login: 是否尝试登录(True: 优先登录,False: 仅处理权限) Returns: bool: 是否成功执行 """ # 初始化会话 self._init_session() logger.info(f"开始处理应用: {self.bundle_id}, 尝试登录: {try_login}") try: # 1. 检查 WDA 就绪 if not self._check_wda_ready(): return False # 2. 解锁设备 self._unlock_device() # 3. 启动应用 if not self._launch_app(self.bundle_id): return False # 4. 处理权限和引导页 handle_result = self._handle_permissions_and_onboarding(timeout=timeout) logger.info(f"已处理 {handle_result['permissions_handled']} 个权限弹窗和 {handle_result['onboarding_skipped']} 个引导页") # 5. 处理登录逻辑 if try_login: # 先检查是否能直接找到登录按钮点击 if self._click_login_button(): logger.info("成功点击 Apple ID 登录按钮") return True else: # 不存在登录按钮,尝试跳过 logger.info("未找到登录按钮,尝试跳过登录流程") if self._skip_login_button(): logger.info("成功跳过登录流程") return True else: # 既没有登录按钮也没有跳过按钮,可能已经登录或不需要登录 logger.info("未找到登录/跳过按钮,可能已完成登录流程") return True else: # 仅处理权限,不处理登录 logger.info("仅处理权限和引导页,不处理登录") return True except Exception as e: logger.error(f"执行异常: {str(e)}") return False def sign_with_appleid(server_url: str, bundle_id: str, alert_strategy: str = "allow", skip_onboarding: bool = True, timeout: float = 60.0, try_login: bool = True) -> bool: """ 使用 Apple ID 登录或处理应用权限 Args: server_url: WDA Server URL bundle_id: 应用的 Bundle ID alert_strategy: Alert 处理策略 - "allow" 或 "deny" skip_onboarding: 是否跳过引导页 timeout: 超时时间 try_login: 是否尝试登录(True: 优先登录,False: 仅处理权限) Returns: bool: 是否成功执行 """ signer = AppleIDSigner(server_url, bundle_id, alert_strategy, skip_onboarding) return signer.execute(timeout, try_login) def handle_app_permissions(server_url: str, bundle_id: str, alert_strategy: str = "allow", timeout: float = 30.0) -> bool: """ 仅处理应用权限弹窗和引导页的便捷函数 Args: server_url: WDA Server URL bundle_id: 应用的 Bundle ID alert_strategy: Alert 处理策略 - "allow" 或 "deny" timeout: 超时时间 Returns: bool: 是否成功执行 """ return sign_with_appleid( server_url=server_url, bundle_id=bundle_id, alert_strategy=alert_strategy, skip_onboarding=True, timeout=timeout, try_login=False ) def click_appleid_login(server_url: str, bundle_id: str, alert_strategy: str = "allow", timeout: float = 60.0) -> bool: """ 点击 Apple ID 登录按钮的便捷函数(自动处理权限和引导页) Args: server_url: WDA Server URL bundle_id: 应用的 Bundle ID alert_strategy: Alert 处理策略 - "allow" 或 "deny" timeout: 超时时间 Returns: bool: 是否成功执行 """ return sign_with_appleid( server_url=server_url, bundle_id=bundle_id, alert_strategy=alert_strategy, skip_onboarding=True, timeout=timeout, try_login=True ) if __name__ == "__main__": import os # 从环境变量读取配置 server = os.getenv("WDA_SERVER", "http://192.168.7.7:8100") bundle = os.getenv("BUNDLE_ID", "com.duolingo.DuolingoMobile") mode = os.getenv("MODE", "login") # login 或 permissions_only print(f"WDA Server: {server}") print(f"Bundle ID: {bundle}") print(f"Mode: {mode}") print("-" * 60) # 执行操作 if mode == "permissions_only": # 仅处理权限和引导页 success = handle_app_permissions( server_url=server, bundle_id=bundle, alert_strategy="allow", timeout=30.0 ) else: # 处理登录(自动处理权限和引导页) success = click_appleid_login( server_url=server, bundle_id=bundle, alert_strategy="allow", timeout=60.0 ) # 打印结果 print("\n" + "=" * 60) if success: print("执行成功") else: print("执行失败") print("=" * 60)