# -*- encoding=utf8 -*- import logging import time import subprocess import os from airtest.core.api import * from poco.drivers.android.uiautomation import AndroidUiautomationPoco # Use the same logger as controller logger = logging.getLogger("DownloadController") # 导入 ADB Helper from adb_helper import ADBHelper from DroidBot.exceptions import ADBException class ApkPureDownloader: def __init__(self): self.apkpure_package_name = "com.apkpure.aegon" self.poco = None # 初始化 ADB Helper self.adb_helper = ADBHelper() def _init_poco(self): if not self.poco: # Initialize Poco only when needed to avoid overhead if not used self.poco = AndroidUiautomationPoco(use_airtest_input=True, screenshot_each_action=False) def _run_cmd(self, cmd): try: process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = process.communicate() return stdout.decode('utf-8', errors='ignore'), process.returncode except Exception as e: logger.error(f"Error running command '{cmd}': {e}") return "", -1 def _picture_click(self, picture_name): # Original parameters: record_pos=(-0.003, 0.796), threshold=0.5, rgb=True try: current_dir = os.path.dirname(os.path.abspath(__file__)) picture_path = os.path.join(current_dir, picture_name) tpl = Template(picture_path, record_pos=(-0.003, 0.796), threshold=0.5, rgb=True) if exists(tpl): touch(tpl) logger.info(f'APKPure: click [{picture_name}] picture') time.sleep(2) return True except Exception as e: logger.warning(f"APKPure: picture_click failed for {picture_name}: {e}") return False def _obtain_package_list(self): """使用 ADBHelper 获取已安装包列表""" output = self.adb_helper.shell("pm list packages") packages = [] for line in output.splitlines(): line = line.strip() if line.startswith("package:"): packages.append(line.replace("package:", "").strip()) return packages def _control_click(self, control, text): if control.exists(): control.click() logger.info(f'APKPure: click [{text}] icon') time.sleep(2) return True return False def _control_input(self, control, text): if control.exists(): control.set_text(text) logger.info(f'APKPure: find icon and input [{text}]') time.sleep(2) return True return False def _check_package_name(self, package_name_list_now, package_name_list_new): # Simplified version of check_package_name to just find the new package # But in our case, we know the target package name, so we can just check if it exists in new list # However, keeping original logic structure for compatibility re_package_name = "unknown" new_success = False # Find what's new diff = set(package_name_list_new) - set(package_name_list_now) if diff: re_package_name = list(diff)[0] # Just take one logger.info(f"APKPure: Get new package name = {re_package_name}") new_success = True if not new_success: logger.error("APKPure: Failed to get new package name!") return new_success, re_package_name def _deal_wait_button(self): # 关闭广告按钮 controls = self.poco(name='android.widget.ImageView', type='android.widget.ImageView', package=self.apkpure_package_name) if controls.exists(): for control in controls: position = control.get_position() if 0.85 < position[0] < 0.90 and 0.08 < position[1] < 0.085: control.click() logger.info('APKPure: find [close] icon according to position, click it') time.sleep(2) break # 检查升级取消 按钮 control = self.poco(text='UPDATE', type='android.widget.Button', package=self.apkpure_package_name) if control.exists(): control = self.poco(text='CANCEL', type='android.widget.Button', package=self.apkpure_package_name) self._control_click(control, 'CANCEL') # 检查NO MORE NOTICE 按钮 control = self.poco(text='NO MORE NOTICE', package=self.apkpure_package_name, type='android.widget.Button', name='com.apkpure.aegon:id/arg') self._control_click(control, 'NO MORE NOTICE') # 检查安装按钮 control = self.poco(textMatches='.*Install(.*', package=self.apkpure_package_name, type='android.widget.TextView', name='com.apkpure.aegon:id/arg') self._control_click(control, '.*Install(.*') # 检查更新按钮 control = self.poco(textMatches='.*Update(.*', package=self.apkpure_package_name, type='android.widget.TextView', name='com.apkpure.aegon:id/arg') self._control_click(control, '.*Update(.*') # 检查设置页面 允许安装位置应用 control = self.poco(package='com.android.settings', type='android.widget.Switch', name='android:id/switch_widget') if self._control_click(control, 'Allow from this source'): control = self.poco(package='com.android.settings', type='android.widget.ImageButton', name='Navigate up') self._control_click(control, 'Navigate up') # 检查安装弹窗 control = self.poco(package='com.android.packageinstaller', type='android.widget.Button', name='android:id/button1', text='INSTALL') if control.exists(): control.click() logger.info('APKPure: click [INSTALL] icon') time.sleep(2) # 检查GRANT按钮 control = self.poco(package='com.apkpure.aegon', type='android.widget.Button', name='android:id/button1', text='GRANT') self._control_click(control, 'GRANT') # 检查重下按钮 control = self.poco(package='com.apkpure.aegon', type='android.widget.Button', name='com.apkpure.aegon:id/arg', text='Restart') self._control_click(control, 'Restart') def _deal_initial_abnormal(self): # 检查升级取消 按钮 control = self.poco(text='UPDATE', type='android.widget.Button', package=self.apkpure_package_name) if control.exists(): control = self.poco(text='CANCEL', type='android.widget.Button', package=self.apkpure_package_name) self._control_click(control, 'CANCEL') # 检查NO MORE NOTICE 按钮 control = self.poco(text='NO MORE NOTICE', package=self.apkpure_package_name, type='android.widget.Button', name='com.apkpure.aegon:id/arg') self._control_click(control, 'NO MORE NOTICE') # 检查设置页面 Allow from this source->Navigate up ins_text = 'Allow from this source' control = self.poco(package='com.android.settings', type='android.widget.Switch', name='android:id/switch_widget') if self._control_click(control, ins_text): ins_text = 'Navigate up' control = self.poco(package='com.android.settings', type='android.widget.ImageButton', name='Navigate up') self._control_click(control, ins_text) # 检查安装弹窗 INSTALL control = self.poco(text='INSTALL', package='com.android.packageinstaller', type='android.widget.Button', name='android:id/button1') self._control_click(control, 'INSTALL') def _initialize_apkpure(self, apkpure_path=None): '''初始化APKPure应用''' # 卸载应用保证每次都从干净的状态开始 logger.info("APKPure: Uninstall Apkpure") if self.adb_helper.uninstall(self.apkpure_package_name): logger.info("APKPure: Uninstall Apkpure done") else: logger.warning("APKPure: Uninstall Apkpure failed") time.sleep(1) logger.info("APKPure: Install Apkpure and initialize") try: install(apkpure_path) except Exception as e: logger.warning(f"APKPure: Could not install {apkpure_path} (might be missing file or network path issue): {e}") try: start_app(self.apkpure_package_name) time.sleep(10) self._deal_initial_abnormal() stop_app(self.apkpure_package_name) logger.info("APKPure: Initialize Apkpure done") except Exception as e: logger.error(f"APKPure: Error during initialization: {e}") def _deal_start_abnormal(self): control = self.poco(text='UPDATE', type='android.widget.Button', package=self.apkpure_package_name) if control.exists(): control = self.poco(text='CANCEL', type='android.widget.Button', package=self.apkpure_package_name) self._control_click(control, 'CANCEL') control = self.poco(text='Navigate up') self._control_click(control, 'Navigate up') time.sleep(2) # 按 BACK 键 keyevent("BACK") time.sleep(2) def _start_apkpure(self): logger.info('APKPure: Start apkpure') start_app(self.apkpure_package_name) try: logger.info("APKPure: Waiting for app page to load...") search_text = self.poco(package=self.apkpure_package_name, type='android.widget.TextView', name='android.widget.TextView') self.poco.wait_for_any([search_text], timeout=20) logger.info("APKPure: UI found, waiting 5s for it to stabilize...") time.sleep(5) except Exception as e: logger.error(f"APKPure: Error waiting for app page to load: {e}") def _search_apk(self, package_name): search_success = False # 点击搜索框 ins_text = 'search' control = self.poco(package=self.apkpure_package_name, type='android.widget.TextView', name='android.widget.TextView') if self._control_click(control, ins_text): # 输入应用包名 # 尝试多种方式查找输入框 control = self.poco(package=self.apkpure_package_name, type='android.widget.EditText', name='com.apkpure.aegon:id/arg') if not control.exists(): # 尝试更通用的查找:只通过类型 control = self.poco(package=self.apkpure_package_name, type='android.widget.EditText') if control.exists(): self._control_input(control, package_name) else: logger.warning("APKPure: Could not find search input field, trying blind input") text(package_name) time.sleep(2) # 回车并点击搜索按钮 logger.info('APKPure: Click [ENTER]') keyevent("ENTER") keyevent("ENTER") logger.info('APKPure: Coordinate click search button') touch((498, 42)) time.sleep(1) # 等待应用页面加载 try: logger.info("APKPure: Waiting for app page to load...") cancel_control = self.poco(text='CANCEL', type='android.widget.Button') update_control = self.poco(text='UPDATE', type='android.widget.Button') install_control = self.poco(text="Install", type="android.widget.Button") self.poco.wait_for_any([install_control, cancel_control, update_control], timeout=60) logger.info("APKPure: UI found, waiting 1s for it to stabilize...") time.sleep(1) search_success = True except Exception as e: logger.error(f"APKPure: Error waiting for app page to load: {e}") # 处理异常 control = self.poco(text='UPDATE', type='android.widget.Button', package=self.apkpure_package_name) if control.exists(): control = self.poco(text='CANCEL', type='android.widget.Button', package=self.apkpure_package_name) self._control_click(control, 'CANCEL') return search_success def _click_search_app(self, try_app_name_list): click_success = False # 升级取消按钮 control = self.poco(text='UPDATE', type='android.widget.Button', package=self.apkpure_package_name) if control.exists(): control = self.poco(text='CANCEL', type='android.widget.Button', package=self.apkpure_package_name) self._control_click(control, 'CANCEL') logger.info(f'APKPure: Try app name list: {try_app_name_list}') for try_app_name in try_app_name_list: logger.info(f'APKPure: Try app name: {try_app_name}') controls = self.poco(package=self.apkpure_package_name, type='android.widget.TextView', name='com.apkpure.aegon:id/arg') if controls.exists(): for control in controls: try: text = control.get_text() if not text: continue # 完全匹配 if try_app_name == text: control.click() logger.info(f'APKPure: click [{try_app_name}] icon') time.sleep(2) click_success = True break # 包含匹配 if try_app_name in text or text in try_app_name: control.click() logger.info(f'APKPure: click [{text}] icon') time.sleep(2) click_success = True break # 包含匹配(忽略大小写) if try_app_name.lower() in text.lower(): control.click() logger.info(f'APKPure: click [{text}] icon') time.sleep(2) click_success = True break except: pass if click_success: break if not click_success: logger.info(f'APKPure: 未匹配到目标应用[{try_app_name_list}],定位点击(0.5,0.35)') touch((0.5, 0.34)) time.sleep(2) click_success = True return click_success def _get_try_app_name_list(self, app_name): try_app_name_list = [app_name] clean_app_name = app_name.replace(':', ' ').replace('-', ' ').replace('–', ' ') word_list = clean_app_name.split(" ") if len(word_list) >= 3: two_words = ' '.join(clean_app_name.split(' ')[:2]) single_word = clean_app_name.split(' ')[0] try_app_name_list.append(two_words) try_app_name_list.append(single_word) elif len(word_list) == 2: single_word = clean_app_name.split(' ')[0] try_app_name_list.append(single_word) return try_app_name_list def _install_one_app(self): installing = False installed = False # 等待页面加载 try: logger.info("APKPure: Waiting for app page to load...") install_btn_text = self.poco(textMatches='.*Install(.*', package=self.apkpure_package_name, type='android.widget.TextView', name='com.apkpure.aegon:id/arg') open_btn_text = self.poco(textMatches='.*Open*', package=self.apkpure_package_name, type='android.widget.TextView', name='com.apkpure.aegon:id/arg') update_btn_text = self.poco(textMatches='.*Update*', package=self.apkpure_package_name, type='android.widget.TextView', name='com.apkpure.aegon:id/arg') self.poco.wait_for_any([install_btn_text, open_btn_text, update_btn_text], timeout=30) logger.info("APKPure: UI found, waiting 1s for it to stabilize...") time.sleep(1) except Exception as e: logger.error(e) # 点击安装按钮 或 正在安装中 control = self.poco(textMatches='.*Install(.*', package=self.apkpure_package_name, type='android.widget.TextView', name='com.apkpure.aegon:id/arg') installing = self._control_click(control, 'Install(') installing_control = self.poco(textMatches='.*%', package=self.apkpure_package_name, type='android.widget.TextView', name='com.apkpure.aegon:id/arg') if installing_control.exists(): logger.info('APKPure: find [~%] icon') logger.info('APKPure: app is installing') installing = True # 已安装 ins_text_list = ['.*Update(.*', 'Open'] for inst_text in ins_text_list: control = self.poco(textMatches=inst_text, package=self.apkpure_package_name, type='android.widget.TextView', name='com.apkpure.aegon:id/arg') if control.exists(): logger.info(f'find [{inst_text}] icon') logger.info('app already installed') installed = True break return installing, installed def _check_complete_install(self, package_name, wait_time): install_success = False error = False # 检查下载完成:发现控件 done_text_list = ['DONE', 'OPEN', 'Open'] for done_text in done_text_list: control_done = self.poco(package='com.apkpure.aegon', type='android.widget.TextView', name='com.apkpure.aegon:id/arg', text=done_text) if control_done.exists(): logger.info(f'APKPure: find [{done_text_list}] icon') install_success = True return install_success, error # 检查下载完成:打开应用 try: # We don't want to actually start it here if we can avoid it, or if we do, we assume it means success # But the original code did start_app(package_name). # Note: start_app is from Airtest API. # To avoid interfering with check_app_state in controller, we might want to be careful. # But here we are just checking if it is installed/downloaded. # If start_app succeeds (no error), it means it's installed. # However, start_app doesn't return True/False usually, it just runs. # We can check pm list instead. # But sticking to original logic: start_app(package_name) logger.info(f'APKPure: open {package_name} success, download complete!') install_success = True return install_success, error except: pass # 检查异常:长时间未开始下载 control = self.poco(text='0.0%', package=self.apkpure_package_name, type='android.widget.TextView', name='com.apkpure.aegon:id/arg') if control.exists(): logger.info('APKPure: find [0.0%] icon') if wait_time > 300: logger.error(f"APKPure: Download error: haven't start install for {wait_time} s") error = True return install_success, error # 检查异常:发现搜索输入框 control = self.poco(package=self.apkpure_package_name, type='android.widget.TextView', name='android.widget.TextView') if control.exists(): if wait_time > 60: logger.error('APKPure: Download error: find [search] icon') error = True return install_success, error # 处理异常:发现重下按钮 control = self.poco(package='com.apkpure.aegon', type='android.widget.Button', name='com.apkpure.aegon:id/arg', text='Restart') self._control_click(control, 'Restart') return install_success, error def _set_phone_permission(self): success_flag = False flag = False logger.info("APKPure: Start set permission") control = self.poco(text="APKPure") for i in range(10): if control.exists(): control.click() time.sleep(1) flag = True break else: swipe((0.5, 0.7), (0.5, 0.25)) time.sleep(1) if flag: control = self.poco(type="android.widget.Switch", name="android:id/switch_widget") if control.exists(): control.click() time.sleep(1) control = self.poco( text="I'm aware of the possible risks, and assume all possible consequences voluntarily.") if control.exists(): control.click() time.sleep(1) time.sleep(10) control = self.poco(text="OK") if control.exists(): control.click() time.sleep(1) control = self.poco(text="Install unknown apps") if control.exists(): control = self.poco(type="android.widget.ImageView", desc="Back", name="com.android.settings:id/up") if control.exists(): control.click() time.sleep(1) if control.exists(): control.click() time.sleep(1) logger.info("APKPure: set permission success") success_flag = True return success_flag def _wait_download(self, package_name): # 等待20分钟 logger.info('APKPure: Wait for app download') wait_time = 0 try_time = 120 install_success = False error = False for i in range(try_time): # 处理等待按钮 self._deal_wait_button() # 检查下载完成 install_success, error = self._check_complete_install(package_name, wait_time) # 下载完成则跳出循环 if install_success: break # 下载出错则跳出循环 if error: break time.sleep(10) wait_time += 10 logger.info(f'APKPure: wait {wait_time}s (maxtime is {10 * try_time} s)') # 真机设置权限 if self.poco(text="Install unknown apps").exists(): self._set_phone_permission() # 打开APKPure防止异常关闭 start_app(self.apkpure_package_name) return install_success, error def start(self, package_name, **kwargs): """ Unified entry point for ApkPure download """ self._init_poco() app_name = kwargs.get('app_name', package_name) apkpure_path = kwargs.get('apkpure_path', None) logger.info(f"ApkPureDownloader: Start processing for {package_name}") try: # 1. Clean start package_name_list_now = self._obtain_package_list() self._initialize_apkpure(apkpure_path) self._start_apkpure() self._deal_start_abnormal() # 2. Search if not self._search_apk(package_name): return False, "Search failed" # 3. Select App try_app_name_list = self._get_try_app_name_list(app_name) if not self._click_search_app(try_app_name_list): return False, "Could not find app in search results" # 4. Install installing, installed = self._install_one_app() if installed: return True, "App already installed/opened" if installing: success, error = self._wait_download(package_name) if not success: return False, f"Download failed or timed out. Error: {error}" # 5. Verify package_name_list_new = self._obtain_package_list() success, new_package_name = self._check_package_name(package_name_list_now, package_name_list_new) if success: if new_package_name == package_name: return True, "Install success and verified" else: return True, f"Install success but package name mismatch? Found: {new_package_name}" else: # Double check if package exists anyway if package_name in package_name_list_new: return True, "Install verified (package found)" return False, "Install not verified in package list" except ADBException: raise except Exception as e: logger.error(f"ApkPureDownloader execution error: {e}") return False, f"Execution error: {e}"