576 lines
25 KiB
Python
576 lines
25 KiB
Python
# -*- 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}"
|