Files
2026-07-07 14:37:04 +08:00

2242 lines
102 KiB
Python
Raw Permalink 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.
#!/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 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)
def set_windows_app_user_model_id():
if sys.platform != 'win32':
return
try:
import ctypes
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
"yibin.keyi.x5plus.flash.tool"
)
except Exception:
pass
class ADKAPKGUI:
def __init__(self):
set_windows_app_user_model_id()
self.root = tk.Tk()
self.root.title("X5plus")
self.root.geometry("650x640")
self.root.resizable(True, True)
# 设置颜色主题
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': '长安X5Plus刷机工具',
'btn_push': '📦 刷入语言包',
'btn_patch_52': '🧩 5.2补丁',
'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_debug': '🔧 调试模式',
'status_connected': '已连接',
'status_disconnected': '未连接',
'status_detecting': '未检测',
'vin_none': '未获取',
'auth_none': '未验证',
'auth_yes': '已授权',
'auth_no': '未授权',
'btn_refresh': '🔄 检查',
'hint_factory_dynamic': '工程模式:拨号 *#*#888 ,工程密码:{password},调试密码:3821',
'hint_lines': [
'1. 安装语言过程中请保持车辆和电脑电量充足,不可中途停止。',
'2. 获取权限以后,车辆自动重启以后再进入语言刷入。',
'3. 部分语言需要重启后生效,可以一切工作完成以后再重启。',
],
'theme_dark': '🌙 暗色',
'theme_light': '☀️ 亮色',
'lang_zh': '中',
'lang_en': 'EN',
'msg_warn_title': '警告',
'msg_error_title': '错误',
'msg_success_title': '成功',
'msg_done_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_patch_missing': '未找到 5.2 补丁文件',
'msg_patch_push_failed': '5.2 补丁推送失败',
'msg_patch_apply_failed': '5.2 补丁刷入失败',
'msg_patch_done': '5.2 补丁刷入完成,重启设备后生效',
'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}',
'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_installing': '安装中',
'progress_installing_name': '安装中 ({name})',
'progress_done': '完成',
'progress_install_done': '安装完成',
'progress_patch_prepare': '准备刷入 5.2 补丁',
'progress_patch_push': '推送补丁文件',
'progress_patch_apply': '应用补丁文件',
'progress_patch_done': '5.2 补丁完成',
'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_invalid': '资源校验失败',
'log_resource_ready': '资源准备完成',
'log_resource_failed': '资源准备失败,请检查网络连接后重试',
'log_resource_dir_missing': '资源目录异常',
'log_no_language_files': '未找到语言包文件',
'log_cleanup_start': '正在执行刷入后清理...',
'log_cleanup_done': '刷入后清理完成',
'log_cleanup_partial': '刷入后清理完成,部分项目未完全成功',
'log_flash_start_notice': '开始刷入语言包,请勿断电或重启电脑和车机。',
'log_flash_done': '语言包刷入完成,共 {total} 个',
'log_flash_effective': '语言包已刷入完成,重启设备后生效,您可在适当时候重启',
'log_flash_partial': '部分刷入成功({success}/{total}',
'log_patch_missing': '未找到 5.2 补丁文件',
'log_patch_push_failed': '5.2 补丁推送失败',
'log_patch_apply_failed': '5.2 补丁刷入失败',
'log_patch_done': '5.2 补丁刷入完成,重启设备后生效',
'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_debug_off': '调试模式已关闭',
'log_debug_on': '调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志',
'err_extract_default': '资源准备失败',
'msg_start_failed': '程序启动失败',
'msg_python_version_error': '错误:需要Python 3.6或更高版本',
'unknown_error': '未知错误',
},
'en': {
'title': 'Changan X5Plus Flash Tool',
'btn_push': '📦 Flash Lang Pkg',
'btn_patch_52': '🧩 5.2 Patch',
'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_debug': '🔧 Debug Mode',
'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_dynamic': 'Factory mode: dial *#*#888, factory password: {password}, debug password: 3821',
'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.',
],
'theme_dark': '🌙 Dark',
'theme_light': '☀️ Light',
'lang_zh': '中',
'lang_en': 'EN',
'msg_warn_title': 'Warning',
'msg_error_title': 'Error',
'msg_success_title': 'Success',
'msg_done_title': 'Done',
'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_patch_missing': '5.2 patch file not found',
'msg_patch_push_failed': '5.2 patch push failed',
'msg_patch_apply_failed': '5.2 patch failed',
'msg_patch_done': '5.2 patch completed. Reboot the device to apply it.',
'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 file',
'filetype_apk': 'APK files',
'filetype_all': 'All files',
'quick_lang_title': 'Quick Language',
'quick_lang_header': 'Select target language',
'quick_lang_hint': 'Click a button to switch the system language. Reboot to apply.',
'quick_lang_system': '⚙️ Open system language settings',
'quick_lang_success_title': 'Success',
'quick_lang_success': 'System language has been set to {language}\n\n⚠️ Reboot the device to apply it.',
'quick_lang_failed_title': 'Failed',
'quick_lang_failed': 'Language setup 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 OTA disable',
'msg_disable_ota_confirm': '⚠️ Warning: after disabling OTA, the system will not receive updates.\n\nDisable the system update app now?',
'msg_disable_ota_success': 'System update has been disabled.',
'msg_disable_ota_failed': 'Disable failed',
'debug_title': 'Debug Mode',
'debug_prompt': 'Enter debug password:',
'debug_password_verifying': 'Verifying debug password...',
'debug_verify_failed': 'Debug password verification failed: {message}',
'msg_debug_wrong_password': 'Wrong password',
'debug_need_enable': 'Press Ctrl+Shift+D to enable debug mode first',
'debug_extract_title': 'Test Extraction',
'debug_extract_prompt': 'Enter package.bin password:',
'debug_extract_success_title': 'Test extraction succeeded',
'debug_extract_success': 'Resources extracted to:\n{path}',
'debug_extract_failed_title': 'Test extraction failed',
'debug_extract_failed': 'Check the 7za output in logs',
'progress_loading': 'Preparing resources',
'progress_loaded': 'Resources ready',
'progress_flashing': 'Flashing',
'progress_flash_done': 'Flash complete',
'progress_installing': 'Installing',
'progress_installing_name': 'Installing ({name})',
'progress_done': 'Done',
'progress_install_done': 'Install complete',
'progress_patch_prepare': 'Preparing 5.2 patch',
'progress_patch_push': 'Pushing patch file',
'progress_patch_apply': 'Applying patch file',
'progress_patch_done': '5.2 patch 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: authorization skipped',
'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. Put ADB files in this folder.',
'log_cache_invalid': 'Resource cache is invalid',
'log_resource_invalid': 'Resource validation failed',
'log_resource_ready': 'Resources ready',
'log_resource_failed': 'Resource preparation failed. Check the network and retry.',
'log_resource_dir_missing': 'Resource directory error',
'log_no_language_files': 'No language package files found',
'log_cleanup_start': 'Running post-flash cleanup...',
'log_cleanup_done': 'Post-flash cleanup complete',
'log_cleanup_partial': 'Post-flash cleanup complete, with partial failures',
'log_flash_start_notice': 'Starting language flashing. Do not power off or reboot the computer or head unit.',
'log_flash_done': 'Language package flashing complete, {total} item(s)',
'log_flash_effective': 'Language package flashing complete. Reboot the device when convenient.',
'log_flash_partial': 'Partially flashed ({success}/{total})',
'log_patch_missing': '5.2 patch file not found',
'log_patch_push_failed': '5.2 patch push failed',
'log_patch_apply_failed': '5.2 patch failed',
'log_patch_done': '5.2 patch completed. Reboot the device to apply it.',
'log_batch_install_start': 'Starting batch install: {count} APK file(s)...',
'log_install_many_start': 'Starting install: {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 setup failed',
'log_rebooting': 'Device is rebooting...',
'log_disable_ota_cancelled': 'OTA disable cancelled',
'log_disable_ota_success': 'System update disabled',
'log_disable_ota_failed': 'Failed to disable system update',
'log_debug_off': 'Debug mode disabled',
'log_debug_on': 'Debug mode enabled - detailed ADB logs will be shown',
'err_extract_default': 'Resource preparation failed',
'msg_start_failed': 'Program startup failed',
'msg_python_version_error': 'Error: Python 3.6 or later is required',
'unknown_error': 'Unknown error',
}
}
# 从 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.set_window_icon()
self.center_window()
# 检查环境
self.check_environment()
# 启动设备状态监控
self.start_device_monitor()
def set_window_icon(self):
"""Set the 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))
self._set_windows_hwnd_icon(icon_path)
except Exception:
pass
def _set_windows_hwnd_icon(self, icon_path):
if sys.platform != 'win32':
return
try:
import ctypes
user32 = ctypes.windll.user32
hwnd = self.root.winfo_id()
image_icon = 1
lr_loadfromfile = 0x00000010
wm_seticon = 0x0080
icon_small = 0
icon_big = 1
path = str(icon_path)
small = user32.LoadImageW(None, path, image_icon, 16, 16, lr_loadfromfile)
big = user32.LoadImageW(None, path, image_icon, 32, 32, lr_loadfromfile)
if small:
user32.SendMessageW(hwnd, wm_seticon, icon_small, small)
if big:
user32.SendMessageW(hwnd, wm_seticon, icon_big, big)
except Exception:
pass
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.title(self.t('title'))
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=42)
title_frame.pack(fill=tk.X, pady=(0, 10))
title_frame.pack_propagate(False)
# 标题:图标和文字分开,避免 emoji 与中文字体基线错位。
self.title_group = tk.Frame(title_frame, bg=self.colors['bg_dark'])
self.title_group.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
self.title_icon_label = tk.Label(self.title_group,
text="🚀",
font=('Segoe UI Emoji', 17),
fg=self.colors['accent'],
bg=self.colors['bg_dark'])
self.title_icon_label.pack(side=tk.LEFT, padx=(0, 8), pady=(1, 0))
self.title_label = tk.Label(self.title_group,
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)
self.btn_lang_switch = tk.Button(title_frame, text=self.t('lang_en'),
command=self.toggle_lang,
font=('Microsoft YaHei', 10, 'bold'),
fg='white',
bg=self.colors['accent'],
activebackground=self.colors['accent_hover'],
activeforeground='white',
relief=tk.FLAT,
cursor='hand2',
width=8,
height=1)
self.btn_lang_switch.pack(side=tk.RIGHT, padx=(8, 6), pady=6)
# 工程模式提示
eng_tips_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1)
eng_tips_frame.pack(fill=tk.X, pady=(0, 8), padx=5)
self.eng_tip_label = tk.Label(eng_tips_frame, text="",
font=('Microsoft YaHei', 9),
fg=self.colors['info'],
bg=self.colors['bg_light'],
wraplength=620,
justify=tk.LEFT)
self.eng_tip_label.pack(padx=10, pady=8)
self._update_fac_pwd_display()
# 按钮区域(两排,每排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_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_patch_52 = tk.Button(row1_frame, text=self.t('btn_patch_52'),
command=self.push_patch_52,
bg=self.colors['warning'],
**btn_params)
self.btn_patch_52.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=(8, 4), 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=7)
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, 4), 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=14)
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, 4), 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=10, 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)
tips = self.t('hint_lines')
self.tips_labels = []
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))
tip_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)
tip_label.pack(side=tk.LEFT)
self.tips_labels.append(tip_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('<Control-Shift-D>', self._toggle_debug)
self.root.bind('<Control-Shift-E>', self._debug_test_extract)
# 绑定悬停效果
self.bind_hover_effects()
def bind_hover_effects(self):
"""绑定按钮悬停效果"""
buttons = [self.btn_push, self.btn_patch_52, self.btn_install_all,
self.btn_language, self.btn_timezone, self.btn_settings,
self.btn_reboot, self.btn_clear, self.btn_exit]
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('<Enter>', on_enter)
btn.bind('<Leave>', 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 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 _calc_fac_pwd(self):
"""计算当前工程密码:分钟十位数 + 0 + 小时(如 14:58 → 5014"""
now = datetime.now()
hour = now.hour # 如 14
minute_tens = now.minute // 10 # 如 58 → 5
return f"{minute_tens}0{hour:02d}"
def _update_fac_pwd_display(self, schedule=True):
"""更新工程密码显示,每30秒刷新一次"""
pwd = self._calc_fac_pwd()
self.eng_tip_label.config(text=self.tf('hint_factory_dynamic', password=pwd))
# 每30秒刷新(密码每分钟可能变化)
if schedule:
self.root.after(30000, self._update_fac_pwd_display)
def run_on_ui_thread(self, func, *args, **kwargs):
"""将函数调度到主线程执行,确保线程安全"""
self.root.after(0, lambda: func(*args, **kwargs))
def _adb_cmd(self):
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 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')
if getattr(self, 'btn_lang_switch', None):
self.btn_lang_switch.configure(
fg='white',
bg=c['accent'],
activebackground=c['accent_hover'],
activeforeground='white'
)
def _refresh_ui_texts(self):
t = self.t
self.root.title(t('title'))
widgets = [
(getattr(self, 'title_label', None), 'title', None),
(getattr(self, 'btn_push', None), 'btn_push', None),
(getattr(self, 'btn_patch_52', None), 'btn_patch_52', 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),
]
for w, key, _ in widgets:
if not w:
continue
text = t(key)
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 getattr(self, 'status_text', None):
self.status_text.config(text=t('status_debug') if self.debug_mode else t('status_ready'))
if getattr(self, 'eng_tip_label', None):
self._update_fac_pwd_display(schedule=False)
for label, tip in zip(getattr(self, 'tips_labels', []), t('hint_lines')):
label.config(text=tip)
if getattr(self, 'device_status_label', None):
self._update_device_status_impl(self.device_connected, self.vin,
getattr(self, '_last_authorized', False))
def _sanitize_user_log_message(self, message):
"""Hide low-level commands, paths, package names, and APK names in normal logs."""
text = str(message)
if re.match(r'^[✓✗]\s+[\w.\-]+\.apk$', text):
return text
replacements = [
(r'com\.[\w.\-]+', '相关应用'),
(r'cn\.[\w.\-]+', '相关应用'),
(r'[\w.\-]+\.apk', '文件'),
(r'package\.bin', '资源文件'),
(r'7za(?:\.exe)?', '资源工具'),
(r'adb(?:\.exe)?', '设备连接工具'),
(r'pm\s+\S+', '系统操作'),
(r'\broot\b', '权限'),
(r'\bremount\b', '挂载'),
(r'(?<!VIN: )/[A-Za-z0-9_./-]+', '路径'),
(r'[A-Za-z]:\\[^\s]+', '路径'),
]
for pattern, replacement in replacements:
text = re.sub(pattern, replacement, text)
return text
def _log_impl(self, message, level="INFO"):
"""日志写入的实际实现(必须在主线程调用)"""
if not self.debug_mode and level in ("INFO", "CMD"):
return
if not self.debug_mode:
message = self._sanitize_user_log_message(message)
timestamp = datetime.now().strftime("%H:%M:%S")
log_entry = f"[{timestamp}] [{level}] {message}\n"
self.log_text.insert(tk.END, log_entry, level)
self.log_text.see(tk.END)
def log(self, message, level="INFO"):
"""添加日志(线程安全)"""
self.run_on_ui_thread(self._log_impl, message, level)
def clear_log(self):
"""清空日志"""
self.log_text.delete(1.0, tk.END)
self.log(self.t('log_cleared'), "INFO")
def _show_progress_impl(self, show=True, is_push=False):
"""显示/隐藏进度条的实际实现(必须在主线程调用)"""
if is_push:
if show:
self.push_progress_label.pack()
self.push_progress.pack(fill=tk.X, pady=(2, 0))
self.push_progress['value'] = 0
else:
self.push_progress_label.pack_forget()
self.push_progress.pack_forget()
else:
if show:
self.progress_label.pack()
self.progress.pack(fill=tk.X, pady=(2, 0))
self.progress['value'] = 0
else:
self.progress_label.pack_forget()
self.progress.pack_forget()
def show_progress(self, show=True, is_push=False):
"""显示/隐藏进度条(线程安全)"""
self.run_on_ui_thread(self._show_progress_impl, show, is_push)
def _update_progress_impl(self, value, max_value=100, label="", is_push=False):
"""更新进度条的实际实现(必须在主线程调用)"""
if is_push:
percent = (value / max_value) * 100
self.push_progress['value'] = percent
self.push_progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)")
else:
percent = (value / max_value) * 100
self.progress['value'] = percent
self.progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)")
self.root.update_idletasks()
def update_progress(self, value, max_value=100, label="", is_push=False):
"""更新进度条(线程安全)"""
self.run_on_ui_thread(self._update_progress_impl, value, max_value, label, is_push)
def update_device_status(self, connected, vin=None, authorized=False):
"""更新设备状态显示(线程安全:立即设状态变量,UI走主线程)"""
self.device_connected = connected
if vin is not None:
self.vin = vin
self.run_on_ui_thread(self._update_device_status_impl, connected, vin, authorized)
def _update_device_status_impl(self, connected, vin, authorized):
"""设备状态UI更新的实际实现(必须在主线程调用)"""
self._last_authorized = authorized
t = self.t
if connected:
self.status_indicator.itemconfig(self.status_dot, fill=self.colors['success'])
self.device_status_label.config(text=t('status_connected'), fg=self.colors['success'])
if vin:
self.vin_label.config(text=vin, fg=self.colors['success'])
if authorized:
self.auth_label.config(text=t('auth_yes'), fg=self.colors['success'])
else:
self.auth_label.config(text=t('auth_no'), fg=self.colors['error'])
else:
self.vin_label.config(text=t('vin_none'), fg=self.colors['error'])
self.auth_label.config(text=t('auth_none'), fg=self.colors['error'])
else:
self.status_indicator.itemconfig(self.status_dot, fill=self.colors['error'])
self.device_status_label.config(text=t('status_disconnected'), fg=self.colors['error'])
self.vin_label.config(text=t('vin_none'), fg=self.colors['error'])
self.auth_label.config(text=t('auth_none'), fg=self.colors['error'])
def check_device_connection(self):
"""检查设备是否连接"""
if self.debug_mode:
return True
if not self.device_connected:
messagebox.showwarning(self.t('msg_device_not_connected_title'), self.t('msg_device_not_connected'))
return False
return True
def start_device_monitor(self):
"""启动设备状态监控(每5秒检查一次)"""
def monitor():
while True:
try:
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 and not self.device_connected and not self._refreshing:
# 设备新连接,刷新状态
self.refresh_device_status()
elif not devices and self.device_connected:
# 设备断开连接
self.update_device_status(False)
self.log(self.t('log_device_disconnected'), "WARNING")
time.sleep(5)
except:
time.sleep(5)
threading.Thread(target=monitor, daemon=True).start()
def check_package_extracted(self):
"""检查语言包是否已解压"""
has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 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"已解压缓存无效: {reason}", "ERROR")
else:
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, "未找到可用 APK"
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
self.temp_dir = None
def _cache_dir_path(self):
local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local'))
return Path(local_appdata) / ".cache" / "system" / ".android" / "apps_cache_X5plus"
def cleanup_cache_on_exit(self):
cache_dir = self.temp_dir if self.temp_dir else self._cache_dir_path()
if cache_dir and cache_dir.exists():
shutil.rmtree(cache_dir, ignore_errors=True)
self.apps_dir = None
self.priv_apps_dir = None
self.temp_dir = None
def on_close(self):
self.cleanup_cache_on_exit()
self.root.destroy()
def _format_extract_error(self, err_msg, return_code):
if not self.debug_mode:
return self.t('err_extract_default')
text = (err_msg or "").lower()
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 "解压密码错误,请重新确认 package.bin 密码"
if "data error" in text:
return "资源包数据错误,可能是密码错误或 package.bin 损坏"
if "headers error" in text or "unexpected end" in text:
return "资源包损坏或不完整,请检查 package.bin"
if err_msg.strip():
return f"解压失败: {err_msg.strip()[:300]}"
return f"解压失败 (返回码 {return_code}),请检查密码是否正确"
def _decode_7z_output(self, output):
for enc in ('gbk', 'utf-8'):
try:
return output.decode(enc)
except UnicodeDecodeError:
continue
return output.decode('utf-8', errors='replace')
def _seven_zip_supports_progress_stream(self):
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_with_7za_progress(self):
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'])
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdin=subprocess.DEVNULL,
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0,
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 return_code == 0:
self.update_progress(100, 100, self.t('progress_loaded'))
return True, decoded_output
return False, decoded_output
def extract_package_silent(self):
"""Extract package.bin silently with progress."""
if not self.package_file.exists():
if self.debug_mode:
self.log(f"Error: package not found ({self.package_file})", "ERROR")
else:
self.log(self.t('log_package_missing'), "ERROR")
return False
if not self.extract_password:
self.log(self.t('log_data_prepare_failed'), "ERROR")
return False
if not os.path.exists(self.sz):
if self.debug_mode:
self.log(f"Error: 7za.exe not found ({self.sz})", "ERROR")
else:
self.log(self.t('log_data_prepare_failed'), "ERROR")
return False
try:
hidden_path = self._cache_dir_path().parent
hidden_path.mkdir(parents=True, exist_ok=True)
self.temp_dir = self._cache_dir_path()
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)
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('progress_loading'), "INFO")
ok, err_msg = self._extract_with_7za_progress()
if not ok:
self.log(self._format_extract_error(err_msg, 1), "ERROR")
self._clear_extracted_cache()
return False
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"解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。", "ERROR")
else:
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"Package 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):
"""检查磁盘上是否已有解压好的资源,有则直接复用"""
cache_dir = self._cache_dir_path()
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"缓存资源无效,已清理: {reason}", "WARNING")
else:
self.log(self.t('log_cache_invalid'), "WARNING")
self._clear_extracted_cache()
return
# self.log("已复用缓存的资源文件", "INFO")
def refresh_device_status(self):
"""Refresh device status."""
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().splitlines()
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 = ''
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), "INFO")
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 device status failed: {str(e)}", "ERROR")
else:
self.log(self.t('log_refresh_failed'), "ERROR")
finally:
self._refreshing = False
threading.Thread(target=refresh, daemon=True).start()
def check_authorization(self, vin):
"""Check authorization."""
if getattr(self, 'debug_mode', False):
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), "INFO")
return True
else:
self.log(self.t('log_auth_failed'), "ERROR")
return False
except Exception:
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()
if data.get('authorized') is True and vehicle_name:
self.vehicle_name = vehicle_name
return data.get('authorized') is True, vehicle_name, data
def fetch_package_password(self):
"""Fetch package password from server."""
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"Data preparation failed: {data.get('message', self.t('unknown_error'))}", "ERROR")
else:
self.log(self.t('log_data_prepare_failed'), "ERROR")
return False
except Exception as e:
if self.debug_mode:
self.log(f"Data preparation failed: {str(e)}", "ERROR")
else:
self.log(self.t('log_data_prepare_failed'), "ERROR")
return False
def _post_json(self, url, payload, timeout=10):
body = json.dumps(payload, ensure_ascii=False).encode('utf-8')
req = Request(
url,
data=body,
method='POST',
headers={
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0',
}
)
with urlopen(req, timeout=timeout) as response:
return json.loads(response.read().decode('utf-8'))
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}", "CMD")
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失败: {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"cp失败: {err}"
return True, ""
def cleanup_after_language_push(self):
"""刷入语言包后禁用并卸载指定预装应用。"""
cleanup_packages = [
"com.incall.apps.softmanager",
"cn.com.os.changan.appstore",
"com.chinatsp.onecall",
"com.changan.oushangCos1",
]
self.log(self.t('log_cleanup_start'), "INFO")
failed_count = 0
for pkg in cleanup_packages:
disable_ok, disable_err = self.run_adb_command(
f'adb -d shell pm disable-user {pkg}')
uninstall_ok, uninstall_err = self.run_adb_command(
f'adb -d shell pm uninstall --user 0 {pkg}')
if disable_ok and uninstall_ok:
if self.debug_mode:
self.log(f"已禁用并卸载: {pkg}", "SUCCESS")
else:
failed_count += 1
detail = uninstall_err or disable_err or "应用可能不存在或已处理"
if self.debug_mode:
self.log(f"清理未完全成功: {pkg} ({detail})", "WARNING")
if failed_count:
self.log(self.t('log_cleanup_partial'), "WARNING")
else:
self.log(self.t('log_cleanup_done'), "SUCCESS")
def _get_patch_52_apk(self):
"""B561_Navi_5.2.apk lives next to the extracted app directory."""
if not self.apps_dir or not self.apps_dir.exists():
return None
patch_apk = self.apps_dir.parent / "B561_Navi_5.2.apk"
if patch_apk.exists():
return patch_apk
# Compatibility fallback in case a resource package places it inside app/.
patch_apk = self.apps_dir / "B561_Navi_5.2.apk"
if patch_apk.exists():
return patch_apk
return None
def push_patch_52(self):
"""刷入 B561_Navi 5.2 补丁 APK。"""
if not self.check_device_connection():
return
if not self.vin:
messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_need_vin'))
return
def do_patch():
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)
patch_apk = self._get_patch_52_apk()
if not patch_apk:
if self.debug_mode:
self.log("未找到 5.2 补丁文件: B561_Navi_5.2.apk", "ERROR")
else:
self.log(self.t('log_patch_missing'), "ERROR")
self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.t('msg_patch_missing'))
return
self.show_progress(True, is_push=True)
try:
self.update_progress(10, 100, self.t('progress_patch_prepare'), is_push=True)
ok, err = self.run_adb_command('adb -d root')
if not ok:
if self.debug_mode:
self.log(f"adb root 失败: {err}", "WARNING")
else:
time.sleep(2)
ok, err = self.run_adb_command('adb -d remount')
if not ok:
if self.debug_mode:
self.log(f"adb remount 失败: {err}", "WARNING")
self.update_progress(40, 100, self.t('progress_patch_push'), is_push=True)
ok, err = self.run_adb_command(f'adb -d push "{patch_apk}" /data/local/tmp/B561_Navi_5.2.apk')
if not ok:
if self.debug_mode:
self.log(f"5.2 补丁推送失败: {err}", "ERROR")
else:
self.log(self.t('log_patch_push_failed'), "ERROR")
self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.t('msg_patch_push_failed'))
return
self.update_progress(80, 100, self.t('progress_patch_apply'), is_push=True)
ok, err = self.run_adb_command(
'adb -d shell cp -f /data/local/tmp/B561_Navi_5.2.apk /system/app/B561_Navi/B561_Navi.apk')
if not ok:
if self.debug_mode:
self.log(f"5.2 补丁刷入失败: {err}", "ERROR")
else:
self.log(self.t('log_patch_apply_failed'), "ERROR")
self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.t('msg_patch_apply_failed'))
return
self.update_progress(100, 100, self.t('progress_patch_done'), is_push=True)
self.log(self.t('log_patch_done'), "SUCCESS")
self.run_on_ui_thread(messagebox.showinfo, self.t('msg_done_title'), self.t('msg_patch_done'))
finally:
self.show_progress(False, is_push=True)
threading.Thread(target=do_patch, daemon=True).start()
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'))
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.log(self.t('log_flash_start_notice'), "SUCCESS")
# 推送系统分区前先获取 root 权限并重新挂载
ok, err = self.run_adb_command('adb -d root')
if not ok:
if self.debug_mode:
self.log(f"adb root 失败: {err}", "WARNING")
else:
# adbd 重启后需要短暂等待
time.sleep(2)
ok, err = self.run_adb_command('adb -d remount')
if not ok:
if self.debug_mode:
self.log(f"adb remount 失败: {err}", "WARNING")
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
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
self.update_progress(i, total, self.t('progress_flashing'), is_push=True)
self.update_progress(total, total, self.t('progress_flash_done'), is_push=True)
# 删除旧应用
self.log(self.t('log_cleanup_start'), "INFO")
for pkg in [
"/system/app/B561_OSWeather",
"/system/app/B561_WeChatSendCar",
"/system/app/B561_VoiceCamera",
"/system/app/B561_KugouCC",
"/system/app/AiQiYi",
]:
self.run_adb_command(f'adb -d shell rm -r {pkg}')
# 推送字体文件
font_file = self.temp_dir / "FZLTHPro_GB18030.ttf" if self.temp_dir else None
if font_file and font_file.exists():
self.run_adb_command(f'adb -d push "{font_file}" /data/local/tmp/FZLTHPro_GB18030.ttf')
self.run_adb_command('adb -d shell mkdir -p /system/fonts')
self.run_adb_command('adb -d shell cp /data/local/tmp/FZLTHPro_GB18030.ttf /system/fonts/FZLTHPro_GB18030.ttf')
self.run_adb_command('adb -d shell rm -f /data/local/tmp/FZLTHPro_GB18030.ttf')
self.cleanup_after_language_push()
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")
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"安装过程异常: {str(e)}", "ERROR")
else:
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"安装过程异常: {str(e)}", "ERROR")
else:
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)
languages = list(zip(self.t('quick_lang_names'), [
"zh-CN", "en-EN", "ru-RU", "fr-FR",
"es-ES", "pt-BR", "it-IT", "ar-SA",
]))
# 创建按钮容器
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"✗ 语言设置失败: {output}", "ERROR")
else:
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'), "INFO")
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"禁用系统升级失败: {output}", "ERROR")
else:
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 _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('status_debug'))
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 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"安装过程异常: {str(e)}", "ERROR")
else:
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 _debug_test_extract(self, event=None):
"""Debug-only package extraction test."""
if not getattr(self, 'debug_mode', False):
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
self.extract_password = pwd
try:
self.show_progress(True, is_push=False)
if self.extract_package_silent():
self.log(self.t('debug_extract_success_title'), "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('debug_extract_failed_title'), "ERROR")
self.run_on_ui_thread(messagebox.showerror, self.t('debug_extract_failed_title'),
self.t('debug_extract_failed'))
finally:
self.extract_password = old_password
self.show_progress(False, is_push=False)
threading.Thread(target=do_extract, daemon=True).start()
def run(self):
"""运行程序"""
self.root.mainloop()
def main():
"""主函数"""
if sys.version_info < (3, 6):
print("错误:需要Python 3.6或更高版本 / 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"程序启动失败 / Startup failed: {e}")
if __name__ == "__main__":
main()