autool/utils_android/download_app/apkpure_downloader.py
2026-06-17 19:44:18 +08:00

576 lines
25 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.

# -*- 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}"