#!/usr/bin/env python3 # -*- coding: utf-8 -*- import os import sys import subprocess import json import re import threading import atexit import tkinter as tk from tkinter import ttk, scrolledtext, filedialog, messagebox, simpledialog from pathlib import Path from urllib.request import urlopen, Request from urllib.error import URLError, HTTPError from urllib.parse import urlencode from datetime import datetime import zipfile try: import pyzipper except ImportError: pyzipper = None import shutil import time def set_windows_app_id(): if sys.platform != 'win32': return try: import ctypes app_id = 'DeepalS05.LanguageInstaller.1.0' ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id) except Exception: pass def get_app_dir(): return Path(sys.executable).resolve().parent if getattr(sys, 'frozen', False) else Path(__file__).resolve().parent def resource_candidates(file_name): base_dir = get_app_dir() candidates = [] if getattr(sys, 'frozen', False): candidates.append(Path(getattr(sys, '_MEIPASS', base_dir)) / file_name) candidates.extend([ base_dir / file_name, base_dir / 'tools' / file_name, base_dir / 'shared' / file_name, base_dir.parent / 'tools' / file_name, base_dir.parent / 'shared' / file_name, base_dir.parent / file_name, ]) unique = [] for candidate in candidates: if candidate not in unique: unique.append(candidate) return unique def find_resource(file_name): candidates = resource_candidates(file_name) for candidate in candidates: if candidate.exists(): return candidate return candidates[0] def find_tool(file_name, fallback=None): path = find_resource(file_name) if path.exists(): return str(path) return fallback or str(path) class ADKAPKGUI: def __init__(self): set_windows_app_id() self.root = tk.Tk() self.root.title("Deepal S05") self.root.geometry("650x640") self.root.resizable(True, True) self.set_window_icon() self.root.after(200, self.set_window_icon) # 设置颜色主题 self.colors_dark = { 'bg_dark': '#1e1e2e', 'bg_light': '#2a2a3e', 'accent': '#6c5ce7', 'accent_hover': '#5b4bc4', 'success': '#00b894', 'error': '#d63031', 'warning': '#fdcb6e', 'info': '#0984e3', 'text': '#dfe6e9', 'text_secondary': '#b2bec3', 'border': '#3d3d5e' } self.colors_light = { 'bg_dark': '#f5f5f5', 'bg_light': '#ffffff', 'accent': '#6c5ce7', 'accent_hover': '#5b4bc4', 'success': '#00b894', 'error': '#d63031', 'warning': '#e17055', 'info': '#0984e3', 'text': '#2d3436', 'text_secondary': '#636e72', 'border': '#dfe6e9' } self.colors = dict(self.colors_dark) self.theme = 'dark' # 多语言 self.lang = 'zh' self.T = { 'zh': { 'title': '深蓝S05多语言安装', 'btn_root': '🔓 获取权限', 'btn_push': '📦 刷入语言包', 'btn_install': '📱 安装App', 'btn_language': '🌐 语言设置', 'btn_timezone': '⏰ 时区设置', 'btn_settings': '⚙️ 安卓设置', 'btn_reboot': '🔄 重启设备', 'btn_disable_upgrade': '❌ 禁用升级', 'btn_clear_log': '🗑 清空日志', 'device_label': '设备:', 'vin_label': 'VIN码:', 'auth_label': '授权:', 'log_title': '📋 运行日志', 'status_ready': '就绪', 'status_connected': '已连接', 'status_disconnected': '未连接', 'status_detecting': '未检测', 'vin_none': '未获取', 'auth_none': '未验证', 'auth_yes': '已授权', 'auth_no': '未授权', 'btn_refresh': '🔄 检查', 'hint_factory': '🔧 断开车机网络,拨号获取到的密码进入工厂模式。', 'theme_dark': '🌙 暗色', 'theme_light': '☀️ 亮色', 'lang_zh': '中', 'lang_en': 'English', 'switch_lang': '语言 / Language', 'switch_theme': '切换主题', 'pwd_query_label': '工程密码查询:', 'vin_placeholder': '请输入VIN', 'btn_query_pwd': '查询密码', 'pwd_empty': '', 'pwd_success': '密码: *#{password}#*', 'pwd_failed': '失败: {message}', 'pwd_request_failed': '请求失败', 'hint_lines': [ '1. 安装语言过程中请保持车辆和电脑电量充足,不可中途停止。', '2. 获取权限以后,车辆自动重启以后再进入语言刷入。', '3. 部分语言需要重启后生效,可以一切工作完成以后再重启。', ], 'msg_warn_title': '警告', 'msg_error_title': '错误', 'msg_success_title': '成功', 'msg_hint_title': '提示', 'msg_device_not_connected_title': '设备未连接', 'msg_device_not_connected': '请先连接设备并点击「检查」按钮刷新状态!', 'msg_need_vin': '请先刷新设备状态并获取VIN码', 'msg_auth_failed_title': '授权失败', 'msg_device_unauthorized': '设备未授权', 'msg_data_prepare_failed': '资源准备失败!', 'msg_resource_dir_missing': '资源目录未找到', 'msg_flash_warning_title': '⚠️ 重要提示', 'msg_flash_warning': '刷入过程中请勿:\n ● 重启车机\n ● 退出本程序\n ● 关闭电脑\n\n否则可能导致车机系统损坏!', 'msg_no_apks_in_folder': '所选文件夹中没有APK文件!', 'msg_install_confirm_title': '确认安装', 'msg_install_confirm_folder': '找到 {count} 个APK文件\n\n是否开始批量安装?', 'msg_install_confirm_many': '已选择 {count} 个APK文件\n\n是否开始安装?', 'msg_install_done_title': '安装完成', 'msg_install_done_all': '成功安装 {count} 个APK!', 'msg_install_partial_title': '部分成功', 'msg_install_partial': '成功: {success}\n失败: {failed}', 'msg_install_failed_title': '安装失败', 'msg_install_failed_all': '所有APK安装失败!', 'msg_install_exception': '安装过程异常', 'file_select_folder_title': '选择包含APK文件的文件夹', 'file_select_apk_title': '选择APK文件', 'filetype_apk': 'APK文件', 'filetype_all': '所有文件', 'quick_lang_title': '快捷语言设置', 'quick_lang_header': '选择目标语言', 'quick_lang_hint': '点击按钮即可将系统语言切换为对应语言,重启后生效', 'quick_lang_system': '⚙️ 打开系统语言设置(手动选择)', 'quick_lang_success_title': '设置成功', 'quick_lang_success': '系统语言已设置为 {language}\n\n⚠️ 请重启设备使其生效。', 'quick_lang_failed_title': '设置失败', 'quick_lang_failed': '语言设置失败!', 'quick_lang_names': ['🇨🇳 中文', '英 English', '俄 Русский', '法 Français', '西 Español', '葡 Português', '意 Italiano', '阿 العربية'], 'msg_reboot_title': '确认重启', 'msg_reboot_confirm': '确定要重启设备吗?', 'msg_disable_ota_title': '确认禁用升级', 'msg_disable_ota_confirm': '⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n是否确定要禁用系统升级应用?', 'msg_disable_ota_success': '系统升级已成功禁用!', 'msg_disable_ota_failed': '禁用失败', 'debug_title': '调试模式', 'debug_prompt': '请输入调试密码:', 'debug_password_verifying': '正在校验调试模式密码...', 'debug_verify_failed': '调试模式密码校验失败: {message}', 'debug_status': '🔧 调试模式', 'msg_debug_wrong_password': '密码错误', 'debug_need_enable': '请先按 Ctrl+Shift+D 开启调试模式', 'debug_extract_title': '测试解压', 'debug_extract_prompt': '请输入 package.bin 解压密码:', 'debug_extract_success_title': '测试解压成功', 'debug_extract_success': '资源已解压到:\n{path}', 'debug_extract_failed_title': '测试解压失败', 'debug_extract_failed': '请查看日志中的 7za 输出', 'progress_loading': '资源加载中', 'progress_loaded': '资源加载完成', 'progress_flashing': '正在刷入', 'progress_flash_done': '刷入完成', 'progress_aborted': '已终止', 'progress_installing': '安装中', 'progress_installing_name': '安装中 ({name})', 'progress_done': '完成', 'progress_install_done': '安装完成', 'log_lang_changed': '语言已切换为中文', 'log_cleared': '日志已清空', 'log_device_connected': '设备已连接', 'log_device_disconnected': '设备未连接', 'log_vin': 'VIN: {vin}', 'log_vin_unavailable': '无法获取VIN', 'log_refresh_failed': '刷新设备状态失败', 'log_debug_skip_auth': '调试模式: 跳过授权验证', 'log_auth_checking': '正在验证授权...', 'log_auth_success': '授权验证通过', 'log_auth_failed': '授权验证失败', 'log_vehicle_name': '车辆名称: {vehicle}', 'log_need_adb': '请先连接adb!', 'log_data_prepare_failed': '资源准备失败', 'log_package_missing': '未找到资源包文件', 'log_adb_missing': '未找到adb命令,请将ADB文件放入本目录', 'log_cache_invalid': '资源缓存无效', 'log_resource_missing': '资源目录异常', 'log_resource_invalid': '资源校验失败', 'log_resource_ready': '资源准备完成', 'log_resource_failed': '资源准备失败,请检查网络连接后重试', 'log_no_language_files': '未找到语言包文件', 'log_permission_root_failed': '获取 root 失败', 'log_permission_failed': '获取权限失败', 'log_permission_reboot_required': '首次获取权限,需要重启设备...', 'log_permission_rebooting': '设备即将重启,重启后权限生效', 'log_permission_reboot_failed': '重启失败', 'log_permission_success': '已获取权限', 'log_flash_readonly': '请先点击「获取权限」获取权限后再试', 'log_flash_done': '语言包刷入完成,共 {total} 个', 'log_flash_effective': '语言包已刷入完成,重启设备后生效,您可在适当时候重启', 'log_flash_partial': '部分刷入成功({success}/{total})', 'log_batch_install_start': '开始批量安装 {count} 个APK...', 'log_install_many_start': '开始安装 {count} 个APK...', 'log_install_done_all': '安装完成:全部 {count} 个成功', 'log_install_done_partial': '安装完成:{success}/{count} 成功', 'log_install_success': '安装成功', 'log_install_failed': '安装失败', 'log_install_exception': '安装过程异常', 'log_quick_lang_setting': '正在设置系统语言为: {language} ({locale})', 'log_quick_lang_success': '语言已设置为 {language}', 'log_quick_lang_failed': '语言设置失败', 'log_rebooting': '设备正在重启...', 'log_disable_ota_cancelled': '已取消禁用升级操作', 'log_disable_ota_success': '系统升级已禁用', 'log_disable_ota_failed': '禁用系统升级失败', 'log_pwd_success': '密码查询成功 VIN={vin}', 'log_pwd_failed': '密码查询失败', 'log_pwd_request_failed': '密码查询请求失败', 'log_debug_off': '调试模式已关闭', 'log_debug_on': '调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志', 'err_extract_wrong_password': '资源准备失败', 'err_extract_data': '资源准备失败', 'err_extract_headers': '资源准备失败', 'err_extract_detail': '资源准备失败', 'err_extract_default': '资源准备失败,请检查解压密码是否正确', 'msg_enter_vin': '请输入VIN码', 'msg_start_failed': '程序启动失败', 'msg_python_version_error': '错误:需要Python 3.6或更高版本', 'log_flash_start_notice': '开始刷入语言包,请勿断电或重启电脑和车机。', 'log_resource_prepare_start': '资源准备中', 'log_extract_password_missing': '资源准备失败', 'log_7za_missing': '资源准备失败', 'log_resource_dir_missing': '资源目录异常', 'log_debug_extract_success': '测试解压成功', 'log_debug_extract_failed': '测试解压失败', 'unknown_error': '未知错误', }, 'en': { 'title': 'Deepal S05 Multi-Language', 'btn_root': '🔓 Get Root', 'btn_push': '📦 Flash Lang Pkg', 'btn_install': '📱 Install App', 'btn_language': '🌐 Language', 'btn_timezone': '⏰ Timezone', 'btn_settings': '⚙️ Settings', 'btn_reboot': '🔄 Reboot', 'btn_disable_upgrade': '❌ Disable OTA', 'btn_clear_log': '🗑 Clear Log', 'device_label': 'Device:', 'vin_label': 'VIN:', 'auth_label': 'Auth:', 'log_title': '📋 Log', 'status_ready': 'Ready', 'status_connected': 'Connected', 'status_disconnected': 'Disconnected', 'status_detecting': 'Detecting', 'vin_none': 'None', 'auth_none': 'Unknown', 'auth_yes': 'Authorized', 'auth_no': 'Unauthorized', 'btn_refresh': '🔄 Check', 'hint_factory': '🔧 Disconnect the head unit network, then enter Factory Mode with the password from dialing.', 'theme_dark': '🌙 Dark', 'theme_light': '☀️ Light', 'lang_zh': '中文', 'lang_en': 'English', 'switch_lang': 'Language', 'switch_theme': 'Theme', 'pwd_query_label': 'Factory password:', 'vin_placeholder': 'Enter VIN', 'btn_query_pwd': 'Query Password', 'pwd_empty': '', 'pwd_success': 'Password: *#{password}#*', 'pwd_failed': 'Failed: {message}', 'pwd_request_failed': 'Request failed', 'hint_lines': [ '1. Keep the vehicle and computer powered during language installation.', '2. After getting permission, wait for the vehicle to reboot before flashing.', '3. Some languages apply after reboot; reboot after all work is complete.', ], 'msg_warn_title': 'Warning', 'msg_error_title': 'Error', 'msg_success_title': 'Success', 'msg_hint_title': 'Hint', 'msg_device_not_connected_title': 'Device not connected', 'msg_device_not_connected': 'Connect the device and click "Check" first.', 'msg_need_vin': 'Refresh device status and get VIN first', 'msg_auth_failed_title': 'Authorization failed', 'msg_device_unauthorized': 'Device is not authorized', 'msg_data_prepare_failed': 'Resource preparation failed!', 'msg_resource_dir_missing': 'Resource directory not found', 'msg_flash_warning_title': 'Important warning', 'msg_flash_warning': 'During flashing, do not:\n - reboot the head unit\n - close this program\n - shut down the computer\n\nOtherwise the system may be damaged.', 'msg_no_apks_in_folder': 'No APK files found in the selected folder.', 'msg_install_confirm_title': 'Confirm install', 'msg_install_confirm_folder': 'Found {count} APK file(s).\n\nStart batch install?', 'msg_install_confirm_many': 'Selected {count} APK file(s).\n\nStart installing?', 'msg_install_done_title': 'Install complete', 'msg_install_done_all': 'Successfully installed {count} APK file(s).', 'msg_install_partial_title': 'Partially complete', 'msg_install_partial': 'Succeeded: {success}\nFailed: {failed}', 'msg_install_failed_title': 'Install failed', 'msg_install_failed_all': 'All APK installs failed.', 'msg_install_exception': 'Install process exception', 'file_select_folder_title': 'Select folder containing APK files', 'file_select_apk_title': 'Select APK files', 'filetype_apk': 'APK files', 'filetype_all': 'All files', 'quick_lang_title': 'Quick Language', 'quick_lang_header': 'Select Target Language', 'quick_lang_hint': 'Tap a language to switch system locale. Reboot to apply.', 'quick_lang_system': 'Open system language settings', 'quick_lang_success_title': 'Set Successfully', 'quick_lang_success': 'System language has been set to {language}.\n\nReboot the device to apply.', 'quick_lang_failed_title': 'Set Failed', 'quick_lang_failed': 'Language setting failed.', 'quick_lang_names': ['🇨🇳 Chinese', 'English', 'Russian', 'French', 'Spanish', 'Portuguese', 'Italian', 'Arabic'], 'msg_reboot_title': 'Confirm reboot', 'msg_reboot_confirm': 'Reboot the device now?', 'msg_disable_ota_title': 'Confirm Disable OTA', 'msg_disable_ota_confirm': 'Warning: after disabling OTA, the system will not receive updates.\n\nDisable the OTA app now?', 'msg_disable_ota_success': 'System OTA has been disabled.', 'msg_disable_ota_failed': 'Disable failed', 'debug_title': 'Debug Mode', 'debug_prompt': 'Enter debug password:', 'debug_password_verifying': 'Verifying debug mode password...', 'debug_verify_failed': 'Debug mode password verification failed: {message}', 'debug_status': 'Debug Mode', 'msg_debug_wrong_password': 'Wrong password', 'debug_need_enable': 'Press Ctrl+Shift+D to enable debug mode first', 'debug_extract_title': 'Extract Test', 'debug_extract_prompt': 'Enter package.bin extraction password:', 'debug_extract_success_title': 'Extract Test Succeeded', 'debug_extract_success': 'Resources extracted to:\n{path}', 'debug_extract_failed_title': 'Extract Test Failed', 'debug_extract_failed': 'Check the log for 7za output', 'progress_loading': 'Preparing resources', 'progress_loaded': 'Resources ready', 'progress_flashing': 'Flashing', 'progress_flash_done': 'Flash complete', 'progress_aborted': 'Aborted', 'progress_installing': 'Installing', 'progress_installing_name': 'Installing ({name})', 'progress_done': 'Done', 'progress_install_done': 'Install complete', 'log_lang_changed': 'Language switched to English', 'log_cleared': 'Log cleared', 'log_device_connected': 'Device connected', 'log_device_disconnected': 'Device disconnected', 'log_vin': 'VIN: {vin}', 'log_vin_unavailable': 'Unable to read VIN', 'log_refresh_failed': 'Failed to refresh device status', 'log_debug_skip_auth': 'Debug mode: skipping authorization', 'log_auth_checking': 'Checking authorization...', 'log_auth_success': 'Authorization passed', 'log_auth_failed': 'Authorization failed', 'log_vehicle_name': 'Vehicle name: {vehicle}', 'log_need_adb': 'Connect ADB first.', 'log_data_prepare_failed': 'Resource preparation failed', 'log_package_missing': 'Resource file not found', 'log_adb_missing': 'adb not found. Place ADB files in this folder.', 'log_cache_invalid': 'Resource cache is invalid', 'log_resource_missing': 'Resource directory is invalid', 'log_resource_invalid': 'Resource validation failed', 'log_resource_ready': 'Resources ready', 'log_resource_failed': 'Resource preparation failed. Check the network and try again.', 'log_no_language_files': 'No language package files found', 'log_permission_root_failed': 'Root permission failed', 'log_permission_failed': 'Permission failed', 'log_permission_reboot_required': 'First permission setup requires reboot...', 'log_permission_rebooting': 'Device will reboot; permission takes effect after reboot', 'log_permission_reboot_failed': 'Reboot failed', 'log_permission_success': 'Permission ready', 'log_flash_readonly': 'Get permission first, then try again', 'log_flash_done': 'Language package flashing complete, total {total}', 'log_flash_effective': 'Language package flashing complete. Reboot later to apply.', 'log_flash_partial': 'Partially flashed ({success}/{total})', 'log_batch_install_start': 'Starting batch install for {count} APK file(s)...', 'log_install_many_start': 'Starting install for {count} APK file(s)...', 'log_install_done_all': 'Install complete: all {count} succeeded', 'log_install_done_partial': 'Install complete: {success}/{count} succeeded', 'log_install_success': 'Install succeeded', 'log_install_failed': 'Install failed', 'log_install_exception': 'Install process exception', 'log_quick_lang_setting': 'Setting system language to {language} ({locale})', 'log_quick_lang_success': 'Language set to {language}', 'log_quick_lang_failed': 'Language setting failed', 'log_rebooting': 'Device is rebooting...', 'log_disable_ota_cancelled': 'Disable OTA operation cancelled', 'log_disable_ota_success': 'System OTA disabled', 'log_disable_ota_failed': 'Disable OTA failed', 'log_pwd_success': 'Password query succeeded VIN={vin}', 'log_pwd_failed': 'Password query failed', 'log_pwd_request_failed': 'Password query request failed', 'log_debug_off': 'Debug mode disabled', 'log_debug_on': 'Debug mode enabled - authorization/device checks skipped, detailed ADB logs shown', 'err_extract_wrong_password': 'Resource preparation failed', 'err_extract_data': 'Resource preparation failed', 'err_extract_headers': 'Resource preparation failed', 'err_extract_detail': 'Resource preparation failed', 'err_extract_default': 'Resource preparation failed. Check the extraction password.', 'msg_enter_vin': 'Enter VIN first', 'msg_start_failed': 'Program startup failed', 'msg_python_version_error': 'Error: Python 3.6 or later is required', 'log_flash_start_notice': 'Starting language package flash. Do not power off or restart the computer or head unit.', 'log_resource_prepare_start': 'Preparing resources', 'log_extract_password_missing': 'Resource preparation failed', 'log_7za_missing': 'Resource preparation failed', 'log_resource_dir_missing': 'Resource directory is invalid', 'log_debug_extract_success': 'Extract test succeeded', 'log_debug_extract_failed': 'Extract test failed', 'unknown_error': 'Unknown error', } } self.root.title(self.t('title')) # 从 exe/py 所在目录查找资源文件 self.base_dir = get_app_dir() self.adb = find_tool('adb.exe', 'adb') self.sz = find_tool('7za.exe') self.package_file = find_resource("package.bin") self.extract_password = None self.apps_dir = None self.priv_apps_dir = None self.temp_dir = None self.api_url = "https://api.changan.softwindy.cn/api/authorizations/auth-check" self.debug_password_api_url = "https://api.changan.softwindy.cn/api/authorizations/verify-debug-mode-password" self.vin = None self.vehicle_name = "" self.device_connected = False self._refreshing = False # 防止并发刷新 self.debug_mode = False # 调试模式 atexit.register(self.cleanup_cache_on_exit) # 设置样式 self.setup_styles() self.setup_ui() self.root.protocol("WM_DELETE_WINDOW", self.on_close) self.center_window() # 检查环境 self.check_environment() # 启动设备状态监控 self.start_device_monitor() def setup_styles(self): """设置自定义样式""" style = ttk.Style() style.theme_use('clam') # 配置主颜色 style.configure('TFrame', background=self.colors['bg_dark']) style.configure('TLabel', background=self.colors['bg_dark'], foreground=self.colors['text']) style.configure('TLabelframe', background=self.colors['bg_dark'], foreground=self.colors['text']) style.configure('TLabelframe.Label', background=self.colors['bg_dark'], foreground=self.colors['accent']) # 配置进度条 style.configure('TProgressbar', background=self.colors['accent'], troughcolor=self.colors['bg_light'], borderwidth=0) def setup_ui(self): """设置UI界面""" # 配置根窗口 self.root.configure(bg=self.colors['bg_dark']) # 创建主框架 main_frame = tk.Frame(self.root, bg=self.colors['bg_dark']) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 顶部标题栏 title_frame = tk.Frame(main_frame, bg=self.colors['bg_dark'], height=65) title_frame.pack(fill=tk.X, pady=(0, 10)) title_frame.pack_propagate(False) title_content_frame = tk.Frame(title_frame, bg=self.colors['bg_dark']) title_content_frame.pack(fill=tk.X, expand=True) # 标题 self.title_label = tk.Label(title_content_frame, text="🚀 " + self.t('title'), font=('Microsoft YaHei', 18, 'bold'), fg=self.colors['accent'], bg=self.colors['bg_dark']) self.title_label.pack(side=tk.LEFT, expand=True, padx=(0, 10)) self.btn_lang_switch = tk.Button(title_content_frame, text=self.t('lang_en'), command=self.toggle_lang, font=('Microsoft YaHei', 9, 'bold'), fg='white', bg=self.colors['accent'], activeforeground='white', activebackground=self.colors['accent_hover'], relief=tk.FLAT, cursor='hand2', width=7, height=1) self.btn_lang_switch.pack(side=tk.RIGHT, padx=(8, 4)) # 工程密码查询区域 pwd_query_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) pwd_query_frame.pack(fill=tk.X, pady=(0, 5), padx=5) self.pwd_query_label = tk.Label(pwd_query_frame, text=self.t('pwd_query_label'), font=('Microsoft YaHei', 9), fg=self.colors['text'], bg=self.colors['bg_light']) self.pwd_query_label.pack(side=tk.LEFT, padx=(10, 5), pady=5) self.vin_input = tk.Entry(pwd_query_frame, font=('Consolas', 9), bg='#2d2d3d', fg='#636e72', insertbackground='white', relief=tk.FLAT, width=20) self.vin_input.insert(0, self.t('vin_placeholder')) self.vin_input.bind("", self._on_vin_input_focus_in) self.vin_input.bind("", self._on_vin_input_focus_out) self.vin_input.pack(side=tk.LEFT, padx=5, pady=5) self.btn_query_pwd = tk.Button(pwd_query_frame, text=self.t('btn_query_pwd'), command=self.query_password_by_vin, font=('Microsoft YaHei', 8), fg='white', bg=self.colors['accent'], relief=tk.FLAT, cursor='hand2') self.btn_query_pwd.pack(side=tk.LEFT, padx=5, pady=5) self.pwd_result_label = tk.Label(pwd_query_frame, text=self.t('pwd_empty'), font=('Microsoft YaHei', 9, 'bold'), fg=self.colors['success'], bg=self.colors['bg_light']) self.pwd_result_label.pack(side=tk.LEFT, padx=10, pady=5) # 工厂模式提示 factory_hint_frame = tk.Frame(main_frame, bg=self.colors['bg_dark']) factory_hint_frame.pack(fill=tk.X, pady=(0, 3)) self.hint_label = tk.Label(factory_hint_frame, text=self.t('hint_factory'), font=('Microsoft YaHei', 8), fg=self.colors['warning'], bg=self.colors['bg_dark']) self.hint_label.pack(side=tk.LEFT, padx=2) # 按钮区域(两排,每排5个) button_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) button_frame.pack(fill=tk.X, pady=(0, 10), padx=5) # 按钮样式参数 btn_params = { 'font': ('Microsoft YaHei', 9), 'fg': 'white', 'relief': tk.FLAT, 'cursor': 'hand2', 'height': 1, 'width': 14 } # 第一排按钮 row1_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) row1_frame.pack(pady=(8, 4)) self.btn_root = tk.Button(row1_frame, text=self.t('btn_root'), command=self.get_root_permission, bg=self.colors['success'], **btn_params) self.btn_root.pack(side=tk.LEFT, padx=4) self.btn_push = tk.Button(row1_frame, text=self.t('btn_push'), command=self.push_all_apks, bg=self.colors['accent'], **btn_params) self.btn_push.pack(side=tk.LEFT, padx=4) self.btn_install_all = tk.Button(row1_frame, text=self.t('btn_install'), command=self.install_apps, bg=self.colors['accent'], **btn_params) self.btn_install_all.pack(side=tk.LEFT, padx=4) self.btn_language = tk.Button(row1_frame, text=self.t('btn_language'), command=self.open_language_quick_set, bg=self.colors['accent'], **btn_params) self.btn_language.pack(side=tk.LEFT, padx=4) # 第二排按钮 row2_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) row2_frame.pack(pady=(4, 8)) self.btn_timezone = tk.Button(row2_frame, text=self.t('btn_timezone'), command=self.open_timezone_settings, bg=self.colors['accent'], **btn_params) self.btn_timezone.pack(side=tk.LEFT, padx=4) self.btn_settings = tk.Button(row2_frame, text=self.t('btn_settings'), command=self.open_android_settings, bg=self.colors['accent'], **btn_params) self.btn_settings.pack(side=tk.LEFT, padx=4) self.btn_reboot = tk.Button(row2_frame, text=self.t('btn_reboot'), command=self.reboot_device, bg=self.colors['warning'], **btn_params) self.btn_reboot.pack(side=tk.LEFT, padx=4) self.btn_exit = tk.Button(row2_frame, text=self.t('btn_disable_upgrade'), command=self.on_disable_upgrade, bg=self.colors['error'], **btn_params) self.btn_exit.pack(side=tk.LEFT, padx=4) # 设备状态栏(横条) status_bar_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) status_bar_frame.pack(fill=tk.X, pady=(0, 5)) # 状态指示器 status_indicator_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) status_indicator_frame.pack(side=tk.LEFT, padx=10, pady=5) self.status_indicator = tk.Canvas(status_indicator_frame, width=10, height=10, bg=self.colors['bg_light'], highlightthickness=0) self.status_indicator.pack(side=tk.LEFT) self.status_dot = self.status_indicator.create_oval(2, 2, 8, 8, fill='#636e72') self.device_label = tk.Label(status_indicator_frame, text=self.t('device_label'), font=('Microsoft YaHei', 9), fg=self.colors['text'], bg=self.colors['bg_light']) self.device_label.pack(side=tk.LEFT, padx=(5, 3)) self.device_status_label = tk.Label(status_indicator_frame, text=self.t('status_detecting'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light'], anchor='w', width=11) self.device_status_label.pack(side=tk.LEFT) # VIN信息 vin_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) vin_frame.pack(side=tk.LEFT, padx=8, pady=5) self.vin_label_title = tk.Label(vin_frame, text=self.t('vin_label'), font=('Microsoft YaHei', 9), fg=self.colors['text'], bg=self.colors['bg_light']) self.vin_label_title.pack(side=tk.LEFT) self.vin_label = tk.Label(vin_frame, text=self.t('vin_none'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light'], anchor='w', width=17) self.vin_label.pack(side=tk.LEFT, padx=(5, 0)) # 授权状态 auth_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) auth_frame.pack(side=tk.LEFT, padx=8, pady=5) self.auth_label_title = tk.Label(auth_frame, text=self.t('auth_label'), font=('Microsoft YaHei', 9), fg=self.colors['text'], bg=self.colors['bg_light']) self.auth_label_title.pack(side=tk.LEFT) self.auth_label = tk.Label(auth_frame, text=self.t('auth_none'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light'], anchor='w', width=10) self.auth_label.pack(side=tk.LEFT, padx=(5, 0)) # 刷新按钮 self.btn_refresh = tk.Button(status_bar_frame, text=self.t('btn_refresh'), command=self.refresh_device_status, font=('Microsoft YaHei', 8), fg=self.colors['accent'], bg=self.colors['bg_light'], relief=tk.FLAT, cursor='hand2', width=8) self.btn_refresh.pack(side=tk.RIGHT, padx=(4, 8), pady=5) # 提示信息区域(设备状态下方) tips_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) tips_frame.pack(fill=tk.X, pady=(5, 5), padx=5) self.tips_labels = [] tips = self.t('hint_lines') for i, tip in enumerate(tips): tip_row = tk.Frame(tips_frame, bg=self.colors['bg_light']) tip_row.pack(fill=tk.X, padx=10, pady=(5 if i == 0 else 0, 5 if i == len(tips) - 1 else 0)) label = tk.Label(tip_row, text=tip, font=('Microsoft YaHei', 9), fg=self.colors['warning'], bg=self.colors['bg_light'], wraplength=600, justify=tk.LEFT) label.pack(side=tk.LEFT) self.tips_labels.append(label) # 解压进度条框架 progress_frame = tk.Frame(main_frame, bg=self.colors['bg_dark']) progress_frame.pack(fill=tk.X, pady=(5, 5)) self.progress_label = tk.Label(progress_frame, text="", font=('Microsoft YaHei', 9), fg=self.colors['text_secondary'], bg=self.colors['bg_dark']) self.progress_label.pack() self.progress = ttk.Progressbar(progress_frame, mode='determinate', style='TProgressbar') self.progress.pack(fill=tk.X, pady=(2, 0)) # 推送进度条 self.push_progress_label = tk.Label(progress_frame, text="", font=('Microsoft YaHei', 9), fg=self.colors['text_secondary'], bg=self.colors['bg_dark']) self.push_progress = ttk.Progressbar(progress_frame, mode='determinate', style='TProgressbar') # 日志区域(下方) log_card = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) log_card.pack(fill=tk.BOTH, expand=True, pady=(5, 0)) # 日志标题栏 log_title_frame = tk.Frame(log_card, bg=self.colors['bg_dark'], height=30) log_title_frame.pack(fill=tk.X) log_title_frame.pack_propagate(False) self.log_title_label = tk.Label(log_title_frame, text=self.t('log_title'), font=('Microsoft YaHei', 10, 'bold'), fg=self.colors['accent'], bg=self.colors['bg_dark']) self.log_title_label.pack(side=tk.LEFT, padx=10) self.btn_clear = tk.Button(log_title_frame, text=self.t('btn_clear_log'), command=self.clear_log, font=('Microsoft YaHei', 8), fg=self.colors['text_secondary'], bg=self.colors['bg_dark'], relief=tk.FLAT, cursor='hand2') self.btn_clear.pack(side=tk.RIGHT, padx=10) # 日志文本框 text_frame = tk.Frame(log_card, bg=self.colors['bg_light']) text_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.log_text = scrolledtext.ScrolledText(text_frame, height=12, wrap=tk.WORD, font=('Consolas', 9), bg='#2d2d3d', fg='#e0e0e0', insertbackground='white', relief=tk.FLAT, borderwidth=0) self.log_text.pack(fill=tk.BOTH, expand=True) # 配置日志颜色标签 self.log_text.tag_config('INFO', foreground='#74b9ff') self.log_text.tag_config('SUCCESS', foreground='#55efc4') self.log_text.tag_config('ERROR', foreground='#ff7675') self.log_text.tag_config('WARNING', foreground='#ffeaa7') self.log_text.tag_config('CMD', foreground='#a29bfe') # 底部状态栏 bottom_status = tk.Frame(main_frame, bg=self.colors['bg_light'], height=22) bottom_status.pack(fill=tk.X, pady=(5, 0)) bottom_status.pack_propagate(False) self.status_text = tk.Label(bottom_status, text=self.t('status_ready'), font=('Microsoft YaHei', 8), fg=self.colors['text_secondary'], bg=self.colors['bg_light']) self.status_text.pack(side=tk.LEFT, padx=10) # 主题切换按钮 self.btn_theme_switch = tk.Button(bottom_status, text=self.t('theme_light'), command=self.toggle_theme, font=('Microsoft YaHei', 8), fg=self.colors['accent'], bg=self.colors['bg_light'], relief=tk.FLAT, cursor='hand2') self.btn_theme_switch.pack(side=tk.RIGHT, padx=5) # 调试模式快捷键 self.root.bind('', self._toggle_debug) self.root.bind('', self._debug_test_extract) # 绑定悬停效果 self.bind_hover_effects() def bind_hover_effects(self): """绑定按钮悬停效果""" buttons = [self.btn_root, self.btn_push, self.btn_install_all, self.btn_language, self.btn_timezone, self.btn_settings, self.btn_reboot, self.btn_clear, self.btn_exit, self.btn_lang_switch] for btn in buttons: original_bg = btn.cget('bg') def on_enter(e, btn=btn, bg=original_bg): btn.config(bg=self.lighten_color(bg)) def on_leave(e, btn=btn, bg=original_bg): btn.config(bg=bg) btn.bind('', on_enter) btn.bind('', on_leave) def lighten_color(self, color): """调亮颜色""" if color == self.colors['accent']: return self.colors['accent_hover'] elif color == self.colors['warning']: return '#feca57' elif color == self.colors['info']: return '#0984e3' elif color == self.colors['error']: return '#e17055' elif color == self.colors['success']: return '#00a884' return color def set_window_icon(self): """Set Tk window/taskbar icon at runtime; PyInstaller --icon only sets the exe file icon.""" try: icon_path = find_resource("app.ico") if icon_path.exists(): self.root.iconbitmap(str(icon_path)) if sys.platform == 'win32': import ctypes hwnd = self.root.winfo_id() image = ctypes.windll.user32.LoadImageW( None, str(icon_path), 1, 0, 0, 0x00000010 ) if image: ctypes.windll.user32.SendMessageW(hwnd, 0x0080, 0, image) ctypes.windll.user32.SendMessageW(hwnd, 0x0080, 1, image) except Exception: pass def center_window(self): """将窗口居中显示在屏幕上""" self.root.update_idletasks() screen_w = self.root.winfo_screenwidth() screen_h = self.root.winfo_screenheight() win_w = self.root.winfo_reqwidth() win_h = self.root.winfo_reqheight() x = (screen_w - win_w) // 2 y = (screen_h - win_h) // 2 self.root.geometry(f"+{x}+{y}") def run_on_ui_thread(self, func, *args, **kwargs): """将函数调度到主线程执行,确保线程安全""" self.root.after(0, lambda: func(*args, **kwargs)) def _adb_cmd(self): """返回可安全用于 shell 命令字符串的 adb 路径""" return subprocess.list2cmdline([self.adb]) def t(self, key): """获取翻译文本""" return self.T.get(self.lang, self.T['zh']).get(key, key) def tf(self, key, **kwargs): try: return self.t(key).format(**kwargs) except Exception: return self.t(key) def is_placeholder_vin(self, value): return value in ( self.T['zh'].get('vin_placeholder'), self.T['en'].get('vin_placeholder'), ) def toggle_lang(self): """切换语言""" self.lang = 'en' if self.lang == 'zh' else 'zh' self.btn_lang_switch.config(text=self.t('lang_en') if self.lang == 'zh' else self.t('lang_zh')) self._refresh_ui_texts() self.log(self.t('log_lang_changed'), "INFO") def toggle_theme(self): """切换主题""" if self.theme == 'dark': self.colors = dict(self.colors_light) self.theme = 'light' self.btn_theme_switch.config(text=self.t('theme_dark')) else: self.colors = dict(self.colors_dark) self.theme = 'dark' self.btn_theme_switch.config(text=self.t('theme_light')) self._apply_theme() def _apply_theme(self): """应用当前主题到所有控件""" c = self.colors self.root.configure(bg=c['bg_dark']) style = ttk.Style() style.configure('TFrame', background=c['bg_dark']) style.configure('TLabel', background=c['bg_dark'], foreground=c['text']) style.configure('TLabelframe', background=c['bg_dark'], foreground=c['text']) style.configure('TLabelframe.Label', background=c['bg_dark'], foreground=c['accent']) style.configure('TProgressbar', background=c['accent'], troughcolor=c['bg_light'], borderwidth=0) self.log_text.tag_config('INFO', foreground='#74b9ff') self.log_text.tag_config('SUCCESS', foreground='#55efc4') self.log_text.tag_config('ERROR', foreground='#ff7675') self.log_text.tag_config('WARNING', foreground='#ffeaa7') self.log_text.tag_config('CMD', foreground='#a29bfe') if self.theme == 'light': self.log_text.configure(bg='#ffffff', fg='#2d3436') else: self.log_text.configure(bg='#2d2d3d', fg='#e0e0e0') def _refresh_ui_texts(self): """刷新所有UI文本""" t = self.t self.root.title(t('title')) widgets = [ (getattr(self, 'title_label', None), 'title', None), (getattr(self, 'pwd_query_label', None), 'pwd_query_label', None), (getattr(self, 'btn_query_pwd', None), 'btn_query_pwd', None), (getattr(self, 'btn_root', None), 'btn_root', None), (getattr(self, 'btn_push', None), 'btn_push', None), (getattr(self, 'btn_install_all', None), 'btn_install', None), (getattr(self, 'btn_language', None), 'btn_language', None), (getattr(self, 'btn_timezone', None), 'btn_timezone', None), (getattr(self, 'btn_settings', None), 'btn_settings', None), (getattr(self, 'btn_reboot', None), 'btn_reboot', None), (getattr(self, 'btn_exit', None), 'btn_disable_upgrade', None), (getattr(self, 'btn_clear', None), 'btn_clear_log', None), (getattr(self, 'log_title_label', None), 'log_title', None), (getattr(self, 'status_text', None), 'status_ready', None), (getattr(self, 'device_label', None), 'device_label', None), (getattr(self, 'vin_label_title', None), 'vin_label', None), (getattr(self, 'auth_label_title', None), 'auth_label', None), (getattr(self, 'btn_refresh', None), 'btn_refresh', None), (getattr(self, 'hint_label', None), 'hint_factory', None), ] for w, key, _ in widgets: if w: text = t(key) if key == 'title': text = "🚀 " + text w.config(text=text) self.btn_theme_switch.config(text=t('theme_light') if self.theme == 'dark' else t('theme_dark')) self.btn_lang_switch.config(text=t('lang_en') if self.lang == 'zh' else t('lang_zh')) if self.is_placeholder_vin(self.vin_input.get()): self.vin_input.delete(0, tk.END) self.vin_input.insert(0, t('vin_placeholder')) for label, tip in zip(getattr(self, 'tips_labels', []), t('hint_lines')): label.config(text=tip) self._update_device_status_impl(self.device_connected, self.vin, getattr(self, '_last_authorized', False)) def _sanitize_user_log_message(self, message): text = str(message) replacements = [ (r'com\.[\w.\-]+', '相关应用'), (r'cn\.[\w.\-]+', '相关应用'), (r'[\w.\-]+\.apk', '文件'), (r'package\.bin', '资源文件'), (r'7za(?:\.exe)?', '资源工具'), (r'adb(?:\.exe)?', '设备连接工具'), (r'pm\s+\S+', '系统操作'), (r'(? 0 has_priv = self.priv_apps_dir and self.priv_apps_dir.exists() and len(list(self.priv_apps_dir.glob("*.apk"))) > 0 if has_app or has_priv: ok, reason = self._validate_extracted_apks() if not ok: if self.debug_mode: self.log(f"Cached resource invalid: {reason}", "ERROR") self.log(self.t('log_cache_invalid'), "ERROR") self._clear_extracted_cache() return False return has_app or has_priv def _validate_extracted_apks(self): apks = [] if self.apps_dir and self.apps_dir.exists(): apks.extend(self.apps_dir.glob("*.apk")) if self.priv_apps_dir and self.priv_apps_dir.exists(): apks.extend(self.priv_apps_dir.glob("*.apk")) if not apks: return False, self.t('log_no_language_files') zero_apks = [apk.name for apk in apks if apk.stat().st_size <= 0] if zero_apks: preview = ", ".join(zero_apks[:5]) suffix = "..." if len(zero_apks) > 5 else "" return False, f"0KB APK: {preview}{suffix}" return True, "" def _clear_extracted_cache(self): if self.temp_dir and self.temp_dir.exists(): shutil.rmtree(self.temp_dir, ignore_errors=True) time.sleep(0.5) self.apps_dir = None self.priv_apps_dir = None def cleanup_cache_on_exit(self): local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) cache_dir = Path(local_appdata) / ".cache" / "system" / ".android" / "apps_cache_S05" if (not self.temp_dir or self.temp_dir != cache_dir) and cache_dir.exists(): shutil.rmtree(cache_dir, ignore_errors=True) self._clear_extracted_cache() def on_close(self): self.cleanup_cache_on_exit() self.root.destroy() def _format_extract_error(self, err_msg): text = (err_msg or "").lower() if self.debug_mode and err_msg and err_msg.strip(): return f"Resource preparation failed: {err_msg.strip()[:1000]}" if any(marker in text for marker in ( "wrong password", "incorrect password", "password is incorrect", "data error in encrypted file", "can not open encrypted archive", )): return self.t('err_extract_wrong_password') if "data error" in text: return self.t('err_extract_data') if "headers error" in text or "unexpected end" in text: return self.t('err_extract_headers') if err_msg.strip(): return self.t('err_extract_detail') return self.t('err_extract_default') def _decode_7z_output(self, output): """解码 7za 输出,兼容中文 Windows 控制台编码""" for enc in ('gbk', 'utf-8'): try: return output.decode(enc) except UnicodeDecodeError: continue return output.decode('utf-8', errors='replace') def _extract_with_7za_progress(self): """运行 7za 并实时解析百分比进度""" self.update_progress(0, 100, self.t('progress_loading')) cmd = [ self.sz, 'x', str(self.package_file), f'-p{self.extract_password}', f'-o{self.temp_dir}', '-y' ] if self._seven_zip_supports_progress_stream(): cmd.extend(['-bsp1', '-bso0', '-bse1']) creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, creationflags=creationflags, bufsize=0 ) output = bytearray() last_percent = -1 while True: chunk = proc.stdout.read(1) if proc.stdout else b'' if not chunk: if proc.poll() is not None: break time.sleep(0.05) continue output.extend(chunk) if len(output) > 60000: del output[:-60000] matches = re.findall(rb'(\d{1,3})%', bytes(output[-512:])) if matches: percent = min(100, int(matches[-1])) if percent != last_percent: last_percent = percent self.update_progress(percent, 100, self.t('progress_loading')) return_code = proc.wait() decoded_output = self._decode_7z_output(bytes(output)) if self.debug_mode: self.log("7ZA OUTPUT:\n" + decoded_output, "CMD" if return_code == 0 else "ERROR") if return_code == 0: self.update_progress(100, 100, self.t('progress_loaded')) return True, decoded_output return False, decoded_output def _seven_zip_supports_progress_stream(self): """检测 7za 是否支持 -bsp1 进度流参数""" try: result = subprocess.run( [self.sz], capture_output=True, text=True, errors='ignore', creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 ) return '-bs{o|e|p}' in (result.stdout + result.stderr) except Exception: return False def extract_package_silent(self): """静默解压语言包(带进度)""" if not self.package_file.exists(): if self.debug_mode: self.log(f"Package file missing: {self.package_file}", "ERROR") self.log(self.t('log_package_missing'), "ERROR") return False if not self.extract_password: self.log(self.t('log_extract_password_missing'), "ERROR") return False if not os.path.exists(self.sz): if self.debug_mode: self.log(f"7za missing: {self.sz}", "ERROR") self.log(self.t('log_7za_missing'), "ERROR") return False try: # 使用用户目录,无需管理员权限 local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) hidden_path = Path(local_appdata) / ".cache" / "system" / ".android" hidden_path.mkdir(parents=True, exist_ok=True) self.temp_dir = hidden_path / "apps_cache_S05" # 如果已存在,先清理 if self.temp_dir.exists(): shutil.rmtree(self.temp_dir, ignore_errors=True) time.sleep(0.5) self.temp_dir.mkdir(parents=True, exist_ok=True) # 设置隐藏属性(Windows) if sys.platform == 'win32': subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True) subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True) self.log(self.t('log_resource_prepare_start'), "SUCCESS") ok, err_msg = self._extract_with_7za_progress() if not ok: self.log(self._format_extract_error(err_msg), "ERROR") self._clear_extracted_cache() return False # 查找app和priv-app目录 self.apps_dir = None self.priv_apps_dir = None app_candidates = list(self.temp_dir.rglob("app")) or list(self.temp_dir.rglob("apps")) if app_candidates: self.apps_dir = app_candidates[0] priv_app_candidates = list(self.temp_dir.rglob("priv-app")) or list(self.temp_dir.rglob("priv-apps")) if priv_app_candidates: self.priv_apps_dir = priv_app_candidates[0] if not self.apps_dir and not self.priv_apps_dir: self.log(self.t('log_resource_dir_missing'), "WARNING") self._clear_extracted_cache() return False ok, reason = self._validate_extracted_apks() if not ok: if self.debug_mode: self.log(f"Extracted resource invalid: {reason}", "ERROR") self.log(self.t('log_resource_invalid'), "ERROR") self._clear_extracted_cache() return False self.log(self.t('log_resource_ready'), "SUCCESS") return True except Exception as e: if getattr(self, 'debug_mode', False): self.log(f"Resource preparation failed: {str(e)}", "ERROR") import traceback self.log(traceback.format_exc(), "ERROR") else: self.log(self.t('log_resource_failed'), "ERROR") self._clear_extracted_cache() return False def check_environment(self): """检查环境""" try: result = subprocess.run(f'{self._adb_cmd()} version', shell=True, capture_output=True, text=True) if result.returncode == 0: self.refresh_device_status() if not self.package_file.exists(): self.log(self.t('log_package_missing'), "WARNING") else: self._try_reuse_extracted() else: self.log(self.t('log_adb_missing'), "ERROR") except FileNotFoundError: self.log(self.t('log_adb_missing'), "ERROR") def _try_reuse_extracted(self): """检查磁盘上是否已有解压好的资源,有则直接复用""" local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) cache_dir = Path(local_appdata) / ".cache" / "system" / ".android" / "apps_cache_S05" if not cache_dir.exists(): return app_candidates = list(cache_dir.rglob("app")) or list(cache_dir.rglob("apps")) priv_candidates = list(cache_dir.rglob("priv-app")) or list(cache_dir.rglob("priv-apps")) has_app = False has_priv = False if app_candidates: apks = list(app_candidates[0].glob("*.apk")) has_app = len(apks) > 0 if priv_candidates: apks = list(priv_candidates[0].glob("*.apk")) has_priv = len(apks) > 0 if has_app or has_priv: if has_app: self.apps_dir = app_candidates[0] if has_priv: self.priv_apps_dir = priv_candidates[0] self.temp_dir = cache_dir ok, reason = self._validate_extracted_apks() if not ok: if self.debug_mode: self.log(f"Cached resource invalid, cleared: {reason}", "WARNING") self.log(self.t('log_cache_invalid'), "WARNING") self._clear_extracted_cache() return # self.log("已复用缓存的资源文件", "INFO") def refresh_device_status(self): """刷新设备状态""" # 防止并发刷新 if self._refreshing: return self._refreshing = True def refresh(): try: was_connected = self.device_connected # 检查设备连接 result = subprocess.run(f'{self._adb_cmd()} -d devices', shell=True, capture_output=True, text=True) lines = result.stdout.strip().split('\n') devices = [line for line in lines[1:] if line.strip() and 'device' in line and 'offline' not in line] if devices: # 只在首次连接时打日志 if not was_connected: self.log(self.t('log_device_connected'), "SUCCESS") # 获取VIN — 兼容两种 key,过滤 Android null 返回值 vin = '' for key in ('ca_vin_info', 'VIN'): vin_result = subprocess.run( f'{self._adb_cmd()} -d shell settings get system {key}', shell=True, capture_output=True, text=True) vin = vin_result.stdout.strip() if vin and vin != 'null': break vin = '' if vin: self.log(self.tf('log_vin', vin=vin), "SUCCESS") # 验证授权 authorized = self.check_authorization(vin) self.update_device_status(True, vin, authorized) else: self.log(self.t('log_vin_unavailable'), "WARNING") self.update_device_status(True, None, False) else: if was_connected: self.log(self.t('log_device_disconnected'), "WARNING") self.update_device_status(False) except Exception as e: if self.debug_mode: self.log(f"Refresh failed: {str(e)}", "ERROR") self.log(self.t('log_refresh_failed'), "ERROR") finally: self._refreshing = False threading.Thread(target=refresh, daemon=True).start() def check_authorization(self, vin): """检查授权""" if self.debug_mode: self.log(self.t('log_debug_skip_auth'), "WARNING") return True self.log(self.t('log_auth_checking'), "INFO") try: authorized, vehicle_name, _ = self.query_authorization_info(vin) if authorized: self.log(self.t('log_auth_success'), "SUCCESS") if vehicle_name: self.log(self.tf('log_vehicle_name', vehicle=vehicle_name), "SUCCESS") return True else: self.log(self.t('log_auth_failed'), "ERROR") return False except Exception as e: if self.debug_mode: self.log(f"Auth check failed: {str(e)}", "ERROR") self.log(self.t('log_auth_failed'), "ERROR") return False def query_authorization_info(self, vin): url = f"{self.api_url}?{urlencode({'vin': vin})}" req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) with urlopen(req, timeout=10) as response: data = json.loads(response.read().decode('utf-8')) payload = data.get('data', {}) if isinstance(data, dict) else {} vehicle_name = payload.get('vehicleName') or payload.get('vehicle_name') or "" vehicle_name = str(vehicle_name).strip() authorized = data.get('authorized') is True or payload.get('authorized') is True if authorized and vehicle_name: self.vehicle_name = vehicle_name return authorized, vehicle_name, data def _post_json(self, url, payload, timeout=10): body = json.dumps(payload).encode('utf-8') req = Request( url, data=body, method='POST', headers={ 'User-Agent': 'Mozilla/5.0', 'Content-Type': 'application/json', }, ) with urlopen(req, timeout=timeout) as response: return json.loads(response.read().decode('utf-8')) def fetch_package_password(self): """从服务端获取资源包解压密码""" if not self.vin: self.log(self.t('log_need_adb'), "ERROR") return False try: vehicle_name = self.vehicle_name if not vehicle_name: authorized, vehicle_name, _ = self.query_authorization_info(self.vin) if not authorized: self.log(self.t('log_auth_failed'), "ERROR") return False if not vehicle_name: self.log(self.t('log_data_prepare_failed'), "ERROR") return False pwd_api_url = "https://api.changan.softwindy.cn/api/authorizations/package-key" url = f"{pwd_api_url}?{urlencode({'vin': self.vin, 'vehicleName': vehicle_name})}" if self.debug_mode: self.log(f"PACKAGE KEY URL: {url}", "CMD") req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) with urlopen(req, timeout=10) as response: data = json.loads(response.read().decode('utf-8')) if data.get('success') and 'data' in data and 'password' in data['data']: self.extract_password = data['data']['password'] if self.debug_mode: self.log("PACKAGE KEY: password received", "CMD") return True else: if self.debug_mode: self.log(f"PACKAGE KEY RESPONSE: {data}", "CMD") self.log(self.t('log_data_prepare_failed'), "ERROR") return False except Exception as e: if self.debug_mode: import traceback self.log(traceback.format_exc(), "ERROR") self.log(self.t('log_data_prepare_failed'), "ERROR") return False def run_adb_command(self, command): """执行 adb 命令,静默执行,仅返回结果""" command = command.replace('adb', self._adb_cmd(), 1) if self.debug_mode: self.log(f"CMD: {command}", "CMD") try: result = subprocess.run(command, shell=True, capture_output=True, text=True, encoding='utf-8') if self.debug_mode: out = result.stdout.strip() err = result.stderr.strip() if out: self.log(f"STDOUT:\n{out}", "CMD") if err: self.log(f"STDERR:\n{err}", "ERROR") if result.returncode == 0: return True, result.stdout.strip() else: return False, result.stderr.strip() except Exception as e: return False, str(e) def push_single_apk(self, apk_path, apk_name, target_type="app"): """推送单个APK到系统分区,返回 (成功, 错误信息)""" temp_apk_path = f"/data/local/tmp/{apk_name}.apk" target_dir = f"/system/priv-app/{apk_name}" if target_type == "priv-app" else f"/system/app/{apk_name}" target_apk_path = f"{target_dir}/{apk_name}.apk" ok, err = self.run_adb_command(f'adb -d push "{apk_path}" {temp_apk_path}') if not ok: return False, f"push failed: {err}" self.run_adb_command(f'adb -d shell mkdir -p {target_dir}') ok, err = self.run_adb_command(f'adb -d shell cp {temp_apk_path} {target_apk_path}') self.run_adb_command(f'adb -d shell rm -f {temp_apk_path}') if not ok: return False, f"copy failed: {err}" return True, "" def push_all_apks(self): """推送APK到系统分区(支持app和priv-app)""" if not self.check_device_connection(): return if not self.vin: messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_need_vin')) return messagebox.showwarning(self.t('msg_flash_warning_title'), self.t('msg_flash_warning')) self.log(self.t('log_flash_start_notice'), "SUCCESS") def do_push_all(): if not self.check_authorization(self.vin): self.run_on_ui_thread( messagebox.showerror, self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized') ) return if not self.extract_password: if not self.fetch_package_password(): self.run_on_ui_thread( messagebox.showerror, self.t('msg_error_title'), self.t('msg_data_prepare_failed') ) return if not self.check_package_extracted(): self.show_progress(True, is_push=False) if not self.extract_package_silent(): self.show_progress(False, is_push=False) self.run_on_ui_thread( messagebox.showerror, self.t('msg_error_title'), self.t('msg_data_prepare_failed') ) return self.show_progress(False, is_push=False) if (not self.apps_dir or not self.apps_dir.exists()) and \ (not self.priv_apps_dir or not self.priv_apps_dir.exists()): self.run_on_ui_thread( messagebox.showerror, self.t('msg_error_title'), self.t('msg_resource_dir_missing') ) return self.show_progress(True, is_push=True) self.run_adb_command('adb -d shell mkdir -p /data/local/tmp') all_apks = [] if self.apps_dir and self.apps_dir.exists(): for apk in self.apps_dir.glob("*.apk"): all_apks.append((apk, "app")) if self.priv_apps_dir and self.priv_apps_dir.exists(): for apk in self.priv_apps_dir.glob("*.apk"): all_apks.append((apk, "priv-app")) if not all_apks: # 缓存可能过期,强制重新解压 self.apps_dir = None self.priv_apps_dir = None self.temp_dir = None if not self.fetch_package_password() or not self.extract_package_silent(): self.log(self.t('log_no_language_files'), "WARNING") self.show_progress(False, is_push=True) return # 重新收集 all_apks = [] if self.apps_dir and self.apps_dir.exists(): for apk in self.apps_dir.glob("*.apk"): all_apks.append((apk, "app")) if self.priv_apps_dir and self.priv_apps_dir.exists(): for apk in self.priv_apps_dir.glob("*.apk"): all_apks.append((apk, "priv-app")) if not all_apks: self.log(self.t('log_no_language_files'), "WARNING") self.show_progress(False, is_push=True) return total = len(all_apks) success_count = 0 aborted = False for i, (apk_path, apk_type) in enumerate(all_apks, 1): apk_name = apk_path.stem ok, err = self.push_single_apk(apk_path, apk_name, apk_type) if ok: success_count += 1 else: if "Read-only file system" in err: if self.debug_mode: self.log(err, "ERROR") self.log(self.t('log_flash_readonly'), "ERROR") aborted = True break self.update_progress(i, total, self.t('progress_flashing'), is_push=True) self.update_progress(total, total, self.t('progress_flash_done') if not aborted else self.t('progress_aborted'), is_push=True) if success_count == total: self.log(self.tf('log_flash_done', total=total), "SUCCESS") self.log(self.t('log_flash_effective'), "WARNING") elif success_count > 0: self.log(self.tf('log_flash_partial', success=success_count, total=total), "WARNING") if not aborted: self.log(self.t('log_flash_effective'), "WARNING") self.show_progress(False, is_push=True) threading.Thread(target=do_push_all, daemon=True).start() def install_all_apks(self): """批量安装APK — 手动选择文件夹""" if not self.check_device_connection(): return apk_dir = filedialog.askdirectory(title=self.t('file_select_folder_title')) if not apk_dir: return apk_files = list(Path(apk_dir).glob("*.apk")) if not apk_files: messagebox.showerror(self.t('msg_error_title'), self.t('msg_no_apks_in_folder')) return result = messagebox.askyesno( self.t('msg_install_confirm_title'), self.tf('msg_install_confirm_folder', count=len(apk_files)) ) if not result: return def install(): self.show_progress(True, is_push=True) total = len(apk_files) self.log(self.tf('log_batch_install_start', count=total), "INFO") success_count = 0 try: self.run_adb_command('adb -d shell setprop vecentek.model 1') for i, apk_path in enumerate(apk_files, 1): self.update_progress(i, total, self.t('progress_installing'), is_push=True) success, _ = self.run_adb_command(f'adb -d install -r "{apk_path}"') if success: success_count += 1 self.update_progress(total, total, self.t('progress_install_done'), is_push=True) if success_count == total: self.log(self.tf('log_install_done_all', count=total), "SUCCESS") self.run_on_ui_thread( messagebox.showinfo, self.t('msg_install_done_title'), self.tf('msg_install_done_all', count=total) ) elif success_count > 0: self.log(self.tf('log_install_done_partial', success=success_count, count=total), "WARNING") self.run_on_ui_thread( messagebox.showwarning, self.t('msg_install_partial_title'), self.tf('msg_install_partial', success=success_count, failed=total - success_count) ) else: self.log(self.t('log_install_failed'), "ERROR") self.run_on_ui_thread( messagebox.showerror, self.t('msg_install_failed_title'), self.t('msg_install_failed_all') ) except Exception as e: if self.debug_mode: self.log(f"Install exception: {str(e)}", "ERROR") self.log(self.t('log_install_exception'), "ERROR") self.run_on_ui_thread( messagebox.showerror, self.t('msg_install_failed_title'), self.t('msg_install_exception') ) finally: self.run_adb_command('adb -d shell setprop vecentek.model 0') self.show_progress(False, is_push=True) threading.Thread(target=install, daemon=True).start() def install_single_apk(self): """安装单个APK""" # 检查设备连接 if not self.check_device_connection(): return file_path = filedialog.askopenfilename( title=self.t('file_select_apk_title'), filetypes=[(self.t('filetype_apk'), "*.apk"), (self.t('filetype_all'), "*.*")] ) if not file_path: return def install(): self.show_progress(True, is_push=True) self.update_progress(50, 100, self.t('progress_installing'), is_push=True) try: self.run_adb_command('adb -d shell setprop vecentek.model 1') success, _ = self.run_adb_command(f'adb -d install -r "{file_path}"') self.update_progress(100, 100, self.t('progress_done'), is_push=True) if success: self.log(self.t('log_install_success'), "SUCCESS") else: self.log(self.t('log_install_failed'), "ERROR") except Exception as e: if self.debug_mode: self.log(f"Install exception: {str(e)}", "ERROR") self.log(self.t('log_install_exception'), "ERROR") finally: self.run_adb_command('adb -d shell setprop vecentek.model 0') self.show_progress(False, is_push=True) threading.Thread(target=install, daemon=True).start() def open_language_settings(self): """打开系统语言设置""" if not self.check_device_connection(): return self.run_adb_command('adb -d shell am start -a android.settings.LOCALE_SETTINGS') def open_language_quick_set(self): """打开快捷语言设置弹窗""" # 检查设备连接 if not self.check_device_connection(): return # 创建弹窗 popup = tk.Toplevel(self.root) popup.title(self.t('quick_lang_title')) popup.geometry("520x320") popup.configure(bg=self.colors['bg_dark']) popup.resizable(False, False) # 居中显示 popup.update_idletasks() x = self.root.winfo_x() + (self.root.winfo_width() - 520) // 2 y = self.root.winfo_y() + (self.root.winfo_height() - 320) // 2 popup.geometry(f"+{x}+{y}") popup.transient(self.root) popup.grab_set() # 标题 header = tk.Label(popup, text=self.t('quick_lang_header'), font=('Microsoft YaHei', 13, 'bold'), fg=self.colors['accent'], bg=self.colors['bg_dark']) header.pack(pady=(15, 10)) hint = tk.Label(popup, text=self.t('quick_lang_hint'), font=('Microsoft YaHei', 9), fg=self.colors['text_secondary'], bg=self.colors['bg_dark']) hint.pack(pady=(0, 12)) # 语言列表:(显示名, locale_code) language_codes = ["zh-CN", "en-US", "ru-RU", "fr-FR", "es-ES", "pt-BR", "it-IT", "ar-SA"] languages = list(zip(self.t('quick_lang_names'), language_codes)) # 创建按钮容器 btn_frame = tk.Frame(popup, bg=self.colors['bg_dark']) btn_frame.pack(pady=(0, 10)) btn_colors = [ self.colors['accent'], self.colors['info'], self.colors['success'], self.colors['warning'], '#e17055', '#00b894', '#6c5ce7', '#0984e3', ] for i, (label, locale) in enumerate(languages): row = i // 4 col = i % 4 def make_cmd(loc=locale, lbl=label): return lambda: self._quick_set_language(loc, lbl, popup) btn = tk.Button(btn_frame, text=label, command=make_cmd(), font=('Microsoft YaHei', 10), fg='white', bg=btn_colors[i], relief=tk.FLAT, cursor='hand2', width=12, height=2) btn.grid(row=row, column=col, padx=5, pady=5) # 底部分隔 + 打开系统设置入口 sep = tk.Frame(popup, bg=self.colors['border'], height=1) sep.pack(fill=tk.X, padx=20, pady=(8, 6)) sys_btn = tk.Button(popup, text=self.t('quick_lang_system'), command=lambda: self._open_sys_and_close(popup), font=('Microsoft YaHei', 9), fg=self.colors['text_secondary'], bg=self.colors['bg_light'], relief=tk.FLAT, cursor='hand2') sys_btn.pack(pady=(0, 10)) def _quick_set_language(self, locale_code, language_name, popup): """执行快捷语言设置""" popup.destroy() def do_set(): self.log(self.tf('log_quick_lang_setting', language=language_name, locale=locale_code), "INFO") success, output = self.run_adb_command( f'adb -d shell settings put system system_locales {locale_code}' ) if success: self.log(self.tf('log_quick_lang_success', language=language_name), "SUCCESS") self.run_on_ui_thread( messagebox.showinfo, self.t('quick_lang_success_title'), self.tf('quick_lang_success', language=language_name) ) else: if self.debug_mode: self.log(f"Language setting failed: {output}", "ERROR") self.log(self.t('log_quick_lang_failed'), "ERROR") self.run_on_ui_thread( messagebox.showerror, self.t('quick_lang_failed_title'), self.t('quick_lang_failed') ) threading.Thread(target=do_set, daemon=True).start() def _open_sys_and_close(self, popup): """关闭弹窗并打开系统语言设置""" popup.destroy() self.open_language_settings() def open_timezone_settings(self): """打开时区设置""" if not self.check_device_connection(): return self.run_adb_command('adb -d shell am start -a android.settings.TIMEZONE_SETTINGS') def open_android_settings(self): """打开安卓原生设置""" if not self.check_device_connection(): return self.run_adb_command('adb -d shell am start -a android.settings.SETTINGS') def reboot_device(self): """重启设备""" if not self.check_device_connection(): return if messagebox.askyesno(self.t('msg_reboot_title'), self.t('msg_reboot_confirm')): subprocess.Popen(f'{self._adb_cmd()} -d shell reboot', shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) self.log(self.t('log_rebooting'), "SUCCESS") self.update_device_status(False) def on_disable_upgrade(self): """禁用系统升级""" # 检查设备连接 if not self.check_device_connection(): return # 弹窗确认 result = messagebox.askyesno( self.t('msg_disable_ota_title'), self.t('msg_disable_ota_confirm') ) if not result: self.log(self.t('log_disable_ota_cancelled'), "INFO") return def disable(): self.show_progress(True, is_push=False) success, output = self.run_adb_command( 'adb -d shell pm disable-user --user 0 com.incall.apps.softmanager') if success: self.log(self.t('log_disable_ota_success'), "SUCCESS") self.run_on_ui_thread( messagebox.showinfo, self.t('msg_success_title'), self.t('msg_disable_ota_success') ) else: if self.debug_mode: self.log(f"Disable OTA failed: {output}", "ERROR") self.log(self.t('log_disable_ota_failed'), "ERROR") self.run_on_ui_thread( messagebox.showerror, self.t('msg_error_title'), self.t('msg_disable_ota_failed') ) self.show_progress(False, is_push=False) threading.Thread(target=disable, daemon=True).start() def _on_vin_input_focus_in(self, event): """输入框获得焦点时清除占位符""" if self.is_placeholder_vin(self.vin_input.get()): self.vin_input.delete(0, tk.END) self.vin_input.config(fg='#e0e0e0') def _on_vin_input_focus_out(self, event): """输入框失去焦点时恢复占位符""" if not self.vin_input.get(): self.vin_input.insert(0, self.t('vin_placeholder')) self.vin_input.config(fg='#636e72') def query_password_by_vin(self): """通过VIN查询密码""" vin = self.vin_input.get().strip() if not vin or self.is_placeholder_vin(vin): messagebox.showwarning(self.t('msg_hint_title'), self.t('msg_enter_vin')) return def do_query(): try: api_url = "https://api.changan.softwindy.cn/api/authorizations/generate-password-by-vin" url = f"{api_url}?{urlencode({'vin': vin})}" req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) with urlopen(req, timeout=10) as response: data = json.loads(response.read().decode('utf-8')) def update_ui(): if data.get('success'): pwd = data.get('data', {}).get('devicePassword', self.t('unknown_error')) self.pwd_result_label.config( text=self.tf('pwd_success', password=pwd), fg=self.colors['success'] ) if self.debug_mode: self.log(f"Password query succeeded VIN={vin} password={pwd}", "CMD") self.log(self.tf('log_pwd_success', vin=vin), "SUCCESS") else: msg = data.get('message', self.t('log_pwd_failed')) self.pwd_result_label.config( text=self.tf('pwd_failed', message=msg), fg=self.colors['error'] ) if self.debug_mode: self.log(f"Password query failed: {msg}", "ERROR") self.log(self.t('log_pwd_failed'), "ERROR") self.run_on_ui_thread(update_ui) except Exception as e: def update_ui_error(): self.pwd_result_label.config( text=self.t('pwd_request_failed'), fg=self.colors['error'] ) if self.debug_mode: self.log(f"Password query request failed: {str(e)}", "ERROR") self.log(self.t('log_pwd_request_failed'), "ERROR") self.run_on_ui_thread(update_ui_error) threading.Thread(target=do_query, daemon=True).start() def _toggle_debug(self, event=None): """切换调试模式(隐藏入口,Ctrl+Shift+D)""" if self.debug_mode: self.debug_mode = False self.log(self.t('log_debug_off'), "WARNING") self.status_text.config(text=self.t('status_ready')) self.refresh_device_status() return pwd = simpledialog.askstring(self.t('debug_title'), self.t('debug_prompt'), show='*', parent=self.root) if not pwd: return self.log(self.t('debug_password_verifying'), "WARNING") def verify(): valid, message = self.verify_debug_mode_password(pwd) if valid: def enable_debug(): self.debug_mode = True self.update_device_status(True, "", True) self.log(self.t('log_debug_on'), "WARNING") self.status_text.config(text=self.t('debug_status')) self.run_on_ui_thread(enable_debug) else: def show_failed(): msg = message or self.t('msg_debug_wrong_password') self.log(self.tf('debug_verify_failed', message=msg), "WARNING") messagebox.showwarning(self.t('msg_error_title'), msg) self.run_on_ui_thread(show_failed) threading.Thread(target=verify, daemon=True).start() def verify_debug_mode_password(self, password): try: data = self._post_json(self.debug_password_api_url, {"password": password}) if data.get('success') is True and data.get('valid') is True: return True, data.get('message', '') return False, data.get('message') or self.t('msg_debug_wrong_password') except Exception as e: return False, str(e) def _debug_test_extract(self, event=None): """调试模式下仅测试资源包解压,不检查设备和授权""" if not self.debug_mode: messagebox.showwarning(self.t('debug_title'), self.t('debug_need_enable')) return pwd = simpledialog.askstring(self.t('debug_extract_title'), self.t('debug_extract_prompt'), show='*', parent=self.root) if not pwd: return def do_extract(): old_password = self.extract_password old_apps_dir = self.apps_dir old_priv_apps_dir = self.priv_apps_dir old_temp_dir = self.temp_dir self.extract_password = pwd try: self.show_progress(True, is_push=False) if self.extract_package_silent(): self.log(self.t('log_debug_extract_success'), "SUCCESS") self.run_on_ui_thread( messagebox.showinfo, self.t('debug_extract_success_title'), self.tf('debug_extract_success', path=self.temp_dir) ) else: self.log(self.t('log_debug_extract_failed'), "ERROR") self.run_on_ui_thread( messagebox.showerror, self.t('debug_extract_failed_title'), self.t('debug_extract_failed') ) finally: self.show_progress(False, is_push=False) self.extract_password = old_password self.apps_dir = old_apps_dir self.priv_apps_dir = old_priv_apps_dir self.temp_dir = old_temp_dir threading.Thread(target=do_extract, daemon=True).start() def install_apps(self): """安装App — 支持单选或多选APK文件""" if not self.check_device_connection(): return file_paths = filedialog.askopenfilenames( title=self.t('file_select_apk_title'), filetypes=[(self.t('filetype_apk'), "*.apk"), (self.t('filetype_all'), "*.*")] ) if not file_paths: return count = len(file_paths) result = messagebox.askyesno( self.t('msg_install_confirm_title'), self.tf('msg_install_confirm_many', count=count) ) if not result: return def install(): self.show_progress(True, is_push=True) self.log(self.tf('log_install_many_start', count=count), "INFO") success_count = 0 try: self.run_adb_command('adb -d shell setprop vecentek.model 1') for i, file_path in enumerate(file_paths, 1): apk_name = Path(file_path).stem self.update_progress(i, count, self.tf('progress_installing_name', name=apk_name), is_push=True) success, _ = self.run_adb_command(f'adb -d install -r "{file_path}"') if success: self.log(f"✓ {apk_name}.apk", "SUCCESS") success_count += 1 else: self.log(f"✗ {apk_name}.apk", "ERROR") self.update_progress(count, count, self.t('progress_install_done'), is_push=True) if success_count == count: self.log(self.tf('log_install_done_all', count=count), "SUCCESS") self.run_on_ui_thread( messagebox.showinfo, self.t('msg_install_done_title'), self.tf('msg_install_done_all', count=count) ) elif success_count > 0: self.log(self.tf('log_install_done_partial', success=success_count, count=count), "WARNING") self.run_on_ui_thread( messagebox.showwarning, self.t('msg_install_partial_title'), self.tf('msg_install_partial', success=success_count, failed=count - success_count) ) else: self.log(self.t('log_install_failed'), "ERROR") self.run_on_ui_thread( messagebox.showerror, self.t('msg_install_failed_title'), self.t('msg_install_failed_all') ) except Exception as e: if self.debug_mode: self.log(f"Install exception: {str(e)}", "ERROR") self.log(self.t('log_install_exception'), "ERROR") self.run_on_ui_thread( messagebox.showerror, self.t('msg_install_failed_title'), self.t('msg_install_exception') ) finally: self.run_adb_command('adb -d shell setprop vecentek.model 0') self.show_progress(False, is_push=True) threading.Thread(target=install, daemon=True).start() def run(self): """运行程序""" self.root.mainloop() def main(): """主函数""" if sys.version_info < (3, 6): print("Error: Python 3.6 or later is required") sys.exit(1) try: app = ADKAPKGUI() app.run() except Exception as e: print(f"Startup failed: {e}") import traceback traceback.print_exc() messagebox.showerror("Error", f"Program startup failed: {e}") if __name__ == "__main__": main()