2233 lines
96 KiB
Python
2233 lines
96 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
import os
|
||
import sys
|
||
import subprocess
|
||
import json
|
||
import re
|
||
import threading
|
||
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.cs75pro.language.installer"
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
class ADKAPKGUI:
|
||
# CS75Pro package-key vehicleName is intentionally hardcoded.
|
||
# Do not replace this with auth-check data.vehicleName in future edits.
|
||
PACKAGE_KEY_VEHICLE_NAME = "CS75Pro"
|
||
|
||
def __init__(self):
|
||
set_windows_app_user_model_id()
|
||
self.root = tk.Tk()
|
||
self.root.title("长安语言刷入工具")
|
||
self.root.geometry("900x620")
|
||
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': {
|
||
'window_title': 'CS75Pro 语言刷入工具',
|
||
'title': 'CS75Pro',
|
||
'btn_unlock_install': '🔓 解锁安装权限',
|
||
'btn_push': '📦 刷入语言包',
|
||
'btn_install': '📱 安装App',
|
||
'btn_language': '🌐 语言设置',
|
||
'btn_timezone': '⏰ 时区设置',
|
||
'btn_settings': '⚙️ 安卓设置',
|
||
'btn_reboot': '🔄 重启设备',
|
||
'btn_disable_upgrade': '❌ 禁用升级',
|
||
'btn_clear_log': '🗑 清空日志',
|
||
'btn_query_pwd': '查询密码',
|
||
'pwd_query_label': '工程密码查询:',
|
||
'vin_placeholder': '请输入VIN',
|
||
'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': '🔧 关闭车辆WI-FI和4G网络,拨号获取的密码进入工程模式',
|
||
'hotspot_title': '📶 电脑热点',
|
||
'hotspot_start': '🔧 打开热点设置',
|
||
'hotspot_name_detecting': '名称: 检测中...',
|
||
'hotspot_name_value': '名称: {ssid}',
|
||
'hotspot_name_empty': '名称: 未配置',
|
||
'hotspot_pwd_default': '密码: changan2024',
|
||
'hotspot_pwd_value': '密码: {password}',
|
||
'hotspot_status_value': '状态: {status}',
|
||
'hotspot_status_off': '状态: 未启动',
|
||
'hint_title': '💡 使用提示',
|
||
'hint_lines': [
|
||
'1. 确保电脑已开启热点',
|
||
'2. 拨号进入工厂模式,点击调试工具',
|
||
'3. 需要云端认证时,点击车机状态栏',
|
||
' Wi-Fi图标,连接上方显示的热点',
|
||
'4. 连接后点击车机“云端认证”按钮',
|
||
'5. 打开ADB后即可正常刷入语言包',
|
||
],
|
||
'theme_dark': '🌙 暗色',
|
||
'theme_light': '☀️ 亮色',
|
||
'lang_zh': '中',
|
||
'lang_en': 'EN',
|
||
'log_lang_changed': '语言已切换为中文',
|
||
'log_cleared': '日志已清空',
|
||
'log_unlock_success': '安装权限已解锁',
|
||
'log_unlock_failed': '安装权限解锁失败: {output}',
|
||
'msg_success_title': '成功',
|
||
'msg_error_title': '错误',
|
||
'msg_warn_title': '警告',
|
||
'msg_device_not_connected_title': '设备未连接',
|
||
'msg_device_not_connected': '请先连接设备并点击「检查」按钮刷新状态!',
|
||
'msg_unlock_success': '安装权限已解锁,可以继续安装或刷入语言包。',
|
||
'msg_unlock_failed': '安装权限解锁失败:{output}',
|
||
'msg_input_vin': '请输入VIN码',
|
||
'msg_need_vin': '请先刷新设备状态并获取VIN码',
|
||
'msg_auth_failed_title': '授权失败',
|
||
'msg_device_unauthorized': '设备未授权',
|
||
'msg_data_prepare_failed': '数据准备失败!',
|
||
'msg_resource_prepare_failed': '资源准备失败!',
|
||
'msg_resource_dir_missing': '资源目录未找到',
|
||
'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': '语言设置失败!\n\n{output}',
|
||
'quick_lang_names': ['🇨🇳 中文', '英 English', '俄 Русский', '法 Français', '西 Español', '葡 Português', '意 Italiano', '阿 العربية'],
|
||
'log_quick_lang_success': '语言已设置为 {language}',
|
||
'log_quick_lang_failed': '语言设置失败: {output}',
|
||
},
|
||
'en': {
|
||
'window_title': 'CS75Pro Language Installer',
|
||
'title': 'CS75Pro',
|
||
'btn_unlock_install': '🔓 Unlock Install',
|
||
'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',
|
||
'btn_query_pwd': 'Query Pwd',
|
||
'pwd_query_label': 'Factory password:',
|
||
'vin_placeholder': 'Enter VIN',
|
||
'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': '🔧 Turn off WiFi & 4G, enter factory mode with dial code',
|
||
'hotspot_title': '📶 Hotspot',
|
||
'hotspot_start': '🔧 Open Hotspot Settings',
|
||
'hotspot_name_detecting': 'Name: detecting...',
|
||
'hotspot_name_value': 'Name: {ssid}',
|
||
'hotspot_name_empty': 'Name: not configured',
|
||
'hotspot_pwd_default': 'Password: changan2024',
|
||
'hotspot_pwd_value': 'Password: {password}',
|
||
'hotspot_status_value': 'Status: {status}',
|
||
'hotspot_status_off': 'Status: off',
|
||
'hint_title': '💡 Tips',
|
||
'hint_lines': [
|
||
'1. Make sure the PC hotspot is enabled',
|
||
'2. Enter factory mode from the dialer',
|
||
'3. When cloud auth is needed, tap the',
|
||
' Wi-Fi icon and connect to the hotspot',
|
||
'4. Tap Cloud Auth on the vehicle screen',
|
||
'5. Enable ADB, then flash the language pack',
|
||
],
|
||
'theme_dark': '🌙 Dark',
|
||
'theme_light': '☀️ Light',
|
||
'lang_zh': '中',
|
||
'lang_en': 'EN',
|
||
'log_lang_changed': 'Language switched to English',
|
||
'log_cleared': 'Log cleared',
|
||
'log_unlock_success': 'Install permission unlocked',
|
||
'log_unlock_failed': 'Install permission unlock failed: {output}',
|
||
'msg_success_title': 'Success',
|
||
'msg_error_title': 'Error',
|
||
'msg_warn_title': 'Warning',
|
||
'msg_device_not_connected_title': 'Device Not Connected',
|
||
'msg_device_not_connected': 'Connect the device and click Check first.',
|
||
'msg_unlock_success': 'Install permission is unlocked. You can continue installing or flashing.',
|
||
'msg_unlock_failed': 'Install permission unlock failed: {output}',
|
||
'msg_input_vin': 'Enter VIN',
|
||
'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': 'Data preparation failed.',
|
||
'msg_resource_prepare_failed': 'Resource preparation failed.',
|
||
'msg_resource_dir_missing': 'Resource directory not found',
|
||
'quick_lang_title': 'Quick Language',
|
||
'quick_lang_header': 'Choose target language',
|
||
'quick_lang_hint': 'Tap a button to switch system language. Reboot to apply.',
|
||
'quick_lang_system': '⚙️ Open system language settings',
|
||
'quick_lang_success_title': 'Language Set',
|
||
'quick_lang_success': 'System language was set to {language}.\n\n⚠️ Reboot the device to apply it.',
|
||
'quick_lang_failed_title': 'Language Failed',
|
||
'quick_lang_failed': 'Language setting failed.\n\n{output}',
|
||
'quick_lang_names': ['Chinese', 'English', 'Russian', 'French', 'Spanish', 'Portuguese', 'Italian', 'Arabic'],
|
||
'log_quick_lang_success': 'Language set to {language}',
|
||
'log_quick_lang_failed': 'Language setting failed: {output}',
|
||
}
|
||
}
|
||
|
||
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.temp_dir = None
|
||
self.api_url = "https://api.changan.softwindy.cn/api/authorizations/auth-check"
|
||
self.vin = None
|
||
self.device_connected = False
|
||
self._refreshing = False # 防止并发刷新
|
||
self.debug_mode = False # 调试模式
|
||
self.root.title(self.t('window_title'))
|
||
|
||
# 设置样式
|
||
self.setup_styles()
|
||
self.setup_ui()
|
||
self.root.after(200, 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("cs75pro.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.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)
|
||
|
||
# 左侧内容区
|
||
left_frame = tk.Frame(main_frame, bg=self.colors['bg_dark'])
|
||
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
|
||
# 右侧提示面板
|
||
right_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1, width=235)
|
||
right_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=(10, 0))
|
||
right_frame.pack_propagate(False)
|
||
|
||
# 顶部标题栏
|
||
title_frame = tk.Frame(left_frame, bg=self.colors['bg_dark'], height=65)
|
||
title_frame.pack(fill=tk.X, pady=(0, 10))
|
||
title_frame.pack_propagate(False)
|
||
|
||
# 标题
|
||
self.title_label = tk.Label(title_frame,
|
||
text="🚀 " + self.t('title'),
|
||
font=('Microsoft YaHei', 18, 'bold'),
|
||
fg=self.colors['accent'],
|
||
bg=self.colors['bg_dark'])
|
||
self.title_label.pack()
|
||
|
||
# 工程密码查询区域
|
||
pwd_query_frame = tk.Frame(left_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("<FocusIn>", self._on_vin_input_focus_in)
|
||
self.vin_input.bind("<FocusOut>", 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="",
|
||
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(left_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(left_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': 13
|
||
}
|
||
|
||
# 第一排按钮
|
||
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_unlock_install = tk.Button(row1_frame, text=self.t('btn_unlock_install'),
|
||
command=self.unlock_install_permission,
|
||
bg=self.colors['warning'],
|
||
**btn_params)
|
||
self.btn_unlock_install.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(left_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'])
|
||
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=20, 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'])
|
||
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=20, 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'])
|
||
self.auth_label.pack(side=tk.LEFT, padx=(5, 0))
|
||
|
||
# 刷新按钮
|
||
self.btn_refresh = tk.Button(status_bar_frame, text=self.t('btn_refresh'),
|
||
command=lambda: self.refresh_device_status(force=True),
|
||
font=('Microsoft YaHei', 8),
|
||
fg=self.colors['accent'],
|
||
bg=self.colors['bg_light'],
|
||
relief=tk.FLAT,
|
||
cursor='hand2')
|
||
self.btn_refresh.pack(side=tk.RIGHT, padx=10, pady=5)
|
||
|
||
# 解压进度条框架
|
||
progress_frame = tk.Frame(left_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(left_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(left_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.btn_lang_switch = tk.Button(bottom_status, text="EN",
|
||
command=self.toggle_lang,
|
||
font=('Microsoft YaHei', 8, 'bold'),
|
||
fg=self.colors['accent'],
|
||
bg=self.colors['bg_light'],
|
||
relief=tk.FLAT, cursor='hand2')
|
||
self.btn_lang_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)
|
||
|
||
# ========== 右侧提示面板 ==========
|
||
# 热点信息卡片
|
||
hotspot_card = tk.Frame(right_frame, bg=self.colors['bg_dark'], relief=tk.RAISED, bd=1)
|
||
hotspot_card.pack(fill=tk.X, padx=5, pady=(10, 5))
|
||
|
||
self.hotspot_title_label = tk.Label(hotspot_card, text=self.t('hotspot_title'),
|
||
font=('Microsoft YaHei', 11, 'bold'),
|
||
fg=self.colors['accent'],
|
||
bg=self.colors['bg_dark'])
|
||
self.hotspot_title_label.pack(pady=(8, 5))
|
||
|
||
self.hotspot_ssid_label = tk.Label(hotspot_card, text=self.t('hotspot_name_detecting'),
|
||
font=('Microsoft YaHei', 9),
|
||
fg=self.colors['text'],
|
||
bg=self.colors['bg_dark'])
|
||
self.hotspot_ssid_label.pack(anchor='w', padx=10, pady=2)
|
||
|
||
self.hotspot_pwd_label = tk.Label(hotspot_card, text=self.t('hotspot_pwd_default'),
|
||
font=('Microsoft YaHei', 9),
|
||
fg=self.colors['text'],
|
||
bg=self.colors['bg_dark'])
|
||
self.hotspot_pwd_label.pack(anchor='w', padx=10, pady=2)
|
||
|
||
self.hotspot_status_label = tk.Label(hotspot_card, text=self.t('hotspot_status_off'),
|
||
font=('Microsoft YaHei', 9),
|
||
fg=self.colors['warning'],
|
||
bg=self.colors['bg_dark'])
|
||
self.hotspot_status_label.pack(anchor='w', padx=10, pady=2)
|
||
|
||
self.btn_hotspot = tk.Button(hotspot_card, text=self.t('hotspot_start'),
|
||
command=self.start_hotspot_action,
|
||
font=('Microsoft YaHei', 8),
|
||
fg='white',
|
||
bg=self.colors['accent'],
|
||
relief=tk.FLAT,
|
||
cursor='hand2')
|
||
self.btn_hotspot.pack(pady=8, padx=10, fill=tk.X)
|
||
|
||
# 分隔线
|
||
tk.Frame(right_frame, bg=self.colors['border'], height=1).pack(fill=tk.X, padx=8, pady=5)
|
||
|
||
# 使用提示卡片
|
||
self.hint_card = tk.Frame(right_frame, bg=self.colors['bg_dark'], relief=tk.RAISED, bd=1)
|
||
self.hint_card.pack(fill=tk.X, padx=5, pady=5)
|
||
|
||
self.hint_title_label = tk.Label(self.hint_card, text=self.t('hint_title'),
|
||
font=('Microsoft YaHei', 11, 'bold'),
|
||
fg=self.colors['warning'],
|
||
bg=self.colors['bg_dark'])
|
||
self.hint_title_label.pack(pady=(8, 5))
|
||
|
||
self.hint_line_labels = []
|
||
self._render_hint_lines()
|
||
|
||
# 绑定悬停效果
|
||
self.bind_hover_effects()
|
||
|
||
def bind_hover_effects(self):
|
||
"""绑定按钮悬停效果"""
|
||
buttons = [self.btn_push, self.btn_unlock_install, self.btn_install_all,
|
||
self.btn_language, self.btn_timezone, self.btn_settings,
|
||
self.btn_reboot, self.btn_clear, self.btn_exit, self.btn_query_pwd,
|
||
self.btn_hotspot]
|
||
|
||
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 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):
|
||
return str(self.t(key)).format(**kwargs)
|
||
|
||
def is_placeholder_vin(self, text):
|
||
return text in (self.T['zh']['vin_placeholder'], self.T['en']['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 _render_hint_lines(self):
|
||
if not getattr(self, 'hint_card', None):
|
||
return
|
||
for label in getattr(self, 'hint_line_labels', []):
|
||
label.destroy()
|
||
self.hint_line_labels = []
|
||
for line in self.t('hint_lines'):
|
||
label = tk.Label(
|
||
self.hint_card,
|
||
text=line,
|
||
font=('Microsoft YaHei', 8),
|
||
fg=self.colors['text_secondary'],
|
||
bg=self.colors['bg_dark'],
|
||
justify=tk.LEFT,
|
||
anchor='w',
|
||
wraplength=205
|
||
)
|
||
label.pack(anchor='w', fill=tk.X, padx=10, pady=1)
|
||
self.hint_line_labels.append(label)
|
||
|
||
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):
|
||
t = self.t
|
||
self.root.title(t('window_title'))
|
||
widgets = [
|
||
(getattr(self, 'title_label', None), 'title', None),
|
||
(getattr(self, 'btn_unlock_install', None), 'btn_unlock_install', 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, 'btn_query_pwd', None), 'btn_query_pwd', 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),
|
||
(getattr(self, 'pwd_query_label', None), 'pwd_query_label', None),
|
||
(getattr(self, 'hotspot_title_label', None), 'hotspot_title', None),
|
||
(getattr(self, 'hotspot_ssid_label', None), 'hotspot_name_detecting', None),
|
||
(getattr(self, 'hotspot_pwd_label', None), 'hotspot_pwd_default', None),
|
||
(getattr(self, 'hotspot_status_label', None), 'hotspot_status_off', None),
|
||
(getattr(self, 'btn_hotspot', None), 'hotspot_start', None),
|
||
(getattr(self, 'hint_title_label', None), 'hint_title', None),
|
||
]
|
||
for w, key, _ in widgets:
|
||
if not w:
|
||
continue
|
||
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'))
|
||
self._render_hint_lines()
|
||
if self.vin:
|
||
self._update_device_status_impl(self.device_connected, self.vin,
|
||
getattr(self, '_last_authorized', False))
|
||
|
||
def _log_impl(self, message, level="INFO"):
|
||
"""日志写入的实际实现(必须在主线程调用)"""
|
||
if not self.debug_mode and level in ("INFO", "CMD"):
|
||
return
|
||
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("日志已清空", "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 unlock_install_permission(self):
|
||
"""解锁安装权限。"""
|
||
if not self.check_device_connection():
|
||
return
|
||
|
||
def worker():
|
||
ok, output = self.run_adb_shell('setprop vecentek.model 1')
|
||
if ok:
|
||
self.log(self.t('log_unlock_success'), "SUCCESS")
|
||
self.run_on_ui_thread(
|
||
messagebox.showinfo,
|
||
self.t('msg_success_title'),
|
||
self.t('msg_unlock_success')
|
||
)
|
||
else:
|
||
msg = output or self.t('msg_error_title')
|
||
self.log(self.tf('log_unlock_failed', output=msg), "ERROR")
|
||
self.run_on_ui_thread(
|
||
messagebox.showerror,
|
||
self.t('msg_error_title'),
|
||
self.tf('msg_unlock_failed', output=msg)
|
||
)
|
||
|
||
threading.Thread(target=worker, daemon=True).start()
|
||
|
||
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("设备已断开连接", "WARNING")
|
||
|
||
time.sleep(5)
|
||
except:
|
||
time.sleep(5)
|
||
|
||
threading.Thread(target=monitor, daemon=True).start()
|
||
|
||
# ============================================================
|
||
# 核心:adb shell 自动密码输入
|
||
# ============================================================
|
||
|
||
def run_adb_shell(self, shell_command):
|
||
"""执行 adb shell 命令,自动静默输入设备密码 adb36987。
|
||
静默执行,不显示 adb 原始输出,仅返回结果。"""
|
||
if self.debug_mode:
|
||
self.log(f"CMD: adb shell {shell_command}", "CMD")
|
||
try:
|
||
proc = subprocess.Popen(
|
||
f'{self._adb_cmd()} -d shell {shell_command}',
|
||
shell=True,
|
||
stdin=subprocess.PIPE,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE,
|
||
text=True
|
||
)
|
||
stdout, stderr = proc.communicate(input='adb36987\n', timeout=15)
|
||
|
||
# 过滤密码提示行
|
||
output_lines = []
|
||
for line in stdout.split('\n'):
|
||
stripped = line.strip()
|
||
if 'please input verify password' in stripped.lower():
|
||
continue
|
||
if stripped == 'verify success!':
|
||
continue
|
||
output_lines.append(line)
|
||
output = '\n'.join(output_lines).strip()
|
||
|
||
if proc.returncode == 0:
|
||
if self.debug_mode:
|
||
self.log(f"CMD OK: {output[:200]}", "CMD")
|
||
return True, output
|
||
else:
|
||
if self.debug_mode:
|
||
self.log(f"CMD FAIL: {stderr.strip()[:200]}", "CMD")
|
||
return False, stderr.strip()
|
||
|
||
except subprocess.TimeoutExpired:
|
||
proc.kill()
|
||
proc.communicate()
|
||
return False, "命令超时"
|
||
except Exception as e:
|
||
return False, str(e)
|
||
|
||
# ============================================================
|
||
# 原始 adb 命令(用于 adb push / adb install 等不需要 shell 的操作)
|
||
# ============================================================
|
||
|
||
def run_adb_command(self, command):
|
||
"""执行原始 adb 命令(adb push / adb install 等,无需 shell 密码)。
|
||
静默执行,不显示 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 result.returncode == 0:
|
||
if self.debug_mode:
|
||
self.log(f"CMD OK: {result.stdout.strip()[:200]}", "CMD")
|
||
return True, result.stdout.strip()
|
||
else:
|
||
if self.debug_mode:
|
||
self.log(f"CMD FAIL: {result.stderr.strip()[:200]}", "CMD")
|
||
return False, result.stderr.strip()
|
||
except Exception as e:
|
||
if self.debug_mode:
|
||
self.log(f"CMD ERROR: {str(e)}", "CMD")
|
||
return False, str(e)
|
||
|
||
def check_package_extracted(self):
|
||
"""检查语言包是否已解压"""
|
||
has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0
|
||
if has_app:
|
||
ok, reason = self._validate_extracted_apks()
|
||
if not ok:
|
||
self.log(f"已解压缓存无效: {reason}", "ERROR")
|
||
self._clear_extracted_cache()
|
||
return False
|
||
return has_app
|
||
|
||
def _validate_extracted_apks(self):
|
||
if not self.apps_dir or not self.apps_dir.exists():
|
||
return False, "缺少 apps 目录"
|
||
apks = list(self.apps_dir.glob("*.apk"))
|
||
if not apks:
|
||
return False, "apps 目录没有 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
|
||
|
||
def _format_extract_error(self, err_msg, return_code):
|
||
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, "Loading resources...")
|
||
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, "Loading resources...")
|
||
|
||
return_code = proc.wait()
|
||
decoded_output = self._decode_7z_output(bytes(output))
|
||
if return_code == 0:
|
||
self.update_progress(100, 100, "Resources loaded")
|
||
return True, decoded_output
|
||
return False, decoded_output
|
||
|
||
def extract_package_silent(self):
|
||
"""Extract package.bin silently with progress; app directory only."""
|
||
if not self.package_file.exists():
|
||
self.log(f"Error: package not found ({self.package_file})", "ERROR")
|
||
return False
|
||
|
||
if not self.extract_password:
|
||
self.log("Error: extract password is not set", "ERROR")
|
||
return False
|
||
|
||
if not os.path.exists(self.sz):
|
||
self.log(f"Error: 7za.exe not found ({self.sz})", "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_CS75Pro"
|
||
|
||
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("正在准备资源包", "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
|
||
app_candidates = list(self.temp_dir.rglob("apps")) or list(self.temp_dir.rglob("app"))
|
||
if app_candidates:
|
||
self.apps_dir = app_candidates[0]
|
||
|
||
if not self.apps_dir:
|
||
self.log("警告:未找到 apps 目录", "WARNING")
|
||
self._clear_extracted_cache()
|
||
return False
|
||
|
||
apk_count = len(list(self.apps_dir.glob("*.apk")))
|
||
ok, reason = self._validate_extracted_apks()
|
||
if not ok:
|
||
self.log(f"解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。", "ERROR")
|
||
self._clear_extracted_cache()
|
||
return False
|
||
self.log(f"资源准备完成 (app: {apk_count})", "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("资源准备失败,请检查网络连接后重试", "ERROR")
|
||
self._clear_extracted_cache()
|
||
return False
|
||
|
||
def check_environment(self):
|
||
"""检查环境"""
|
||
# 修改hosts文件
|
||
self.modify_hosts()
|
||
# 刷新热点显示
|
||
self.refresh_hotspot_display()
|
||
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("未找到资源包文件", "WARNING")
|
||
else:
|
||
self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR")
|
||
except FileNotFoundError:
|
||
self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR")
|
||
|
||
def refresh_device_status(self, force=False):
|
||
"""Refresh device status; Yidong uses ca.car.vin."""
|
||
if self._refreshing and not force:
|
||
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("Device connected", "SUCCESS")
|
||
|
||
success, vin_output = self.run_adb_shell('settings get system ca.car.vin')
|
||
vin = vin_output.strip() if success else ''
|
||
if vin == 'null':
|
||
vin = ''
|
||
|
||
if vin:
|
||
self.log(f"VIN: {vin}", "INFO")
|
||
authorized = self.check_authorization(vin)
|
||
self.update_device_status(True, vin, authorized)
|
||
else:
|
||
self.log("Unable to read VIN, please confirm factory mode", "WARNING")
|
||
self.update_device_status(True, None, False)
|
||
else:
|
||
if was_connected:
|
||
self.log("Device disconnected", "WARNING")
|
||
self.update_device_status(False)
|
||
except Exception as e:
|
||
self.log(f"Refresh device status failed: {str(e)}", "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("Debug mode: skip authorization", "WARNING")
|
||
return True
|
||
self.log("Checking authorization...", "INFO")
|
||
try:
|
||
authorized, vehicle_name, data = self.query_authorization_info(vin)
|
||
|
||
if authorized:
|
||
self.log("Authorization passed", "SUCCESS")
|
||
if vehicle_name:
|
||
self.log(f"Vehicle name: {vehicle_name}", "INFO")
|
||
return True
|
||
else:
|
||
self.log("Authorization failed", "ERROR")
|
||
return False
|
||
|
||
except Exception:
|
||
self.log("Authorization 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()
|
||
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("Please connect adb first", "ERROR")
|
||
return False
|
||
|
||
try:
|
||
# CS75Pro package-key vehicleName is fixed by requirement.
|
||
# It must not be sourced from auth-check, even if auth-check returns data.vehicleName.
|
||
vehicle_name = self.PACKAGE_KEY_VEHICLE_NAME
|
||
|
||
pwd_api_url = "https://api.changan.softwindy.cn/api/authorizations/package-key"
|
||
url = f"{pwd_api_url}?{urlencode({'vin': self.vin, 'vehicleName': vehicle_name})}"
|
||
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']
|
||
return True
|
||
else:
|
||
self.log(f"Data preparation failed: {data.get('message', 'unknown error')}", "ERROR")
|
||
return False
|
||
|
||
except Exception as e:
|
||
self.log(f"Data preparation failed: {str(e)}", "ERROR")
|
||
return False
|
||
|
||
def push_single_apk(self, apk_path, apk_name):
|
||
"""推送单个APK到设备并安装,返回 (成功, 错误信息)"""
|
||
temp_apk_path = f"/data/local/tmp/{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}"
|
||
|
||
ok, err = self.run_adb_shell(f'pm install -d -f -r {temp_apk_path}')
|
||
self.run_adb_shell(f'rm -f {temp_apk_path}')
|
||
if not ok:
|
||
return False, f"install失败: {err}"
|
||
|
||
return True, ""
|
||
|
||
def _push_and_install(self, apk_path, apk_name):
|
||
"""push → pm install → cleanup,供安装类方法复用"""
|
||
ok, _ = self.push_single_apk(apk_path, apk_name)
|
||
return ok
|
||
|
||
def cleanup_preinstalled_apps_for_language(self):
|
||
"""Disable and uninstall built-in apps before flashing language packages."""
|
||
packages = [
|
||
"com.wtcl.electronicdirections",
|
||
"com.tinnove.netease.music",
|
||
"com.incall.apps.softmanager",
|
||
"com.tencent.qqlive.audiobox",
|
||
]
|
||
failed = []
|
||
|
||
for package in packages:
|
||
disable_ok, disable_output = self.run_adb_shell(f'pm disable-user {package}')
|
||
uninstall_ok, uninstall_output = self.run_adb_shell(f'pm uninstall -k --user 0 {package}')
|
||
|
||
if self.debug_mode:
|
||
if disable_ok:
|
||
self.log(f"禁用完成: {package}", "CMD")
|
||
else:
|
||
self.log(f"禁用失败: {package} {disable_output}", "CMD")
|
||
if uninstall_ok:
|
||
self.log(f"卸载完成: {package}", "CMD")
|
||
else:
|
||
self.log(f"卸载失败: {package} {uninstall_output}", "CMD")
|
||
|
||
if not disable_ok or not uninstall_ok:
|
||
failed.append(package)
|
||
|
||
if failed:
|
||
if self.debug_mode:
|
||
self.log("预置应用清理部分失败: " + ", ".join(failed), "WARNING")
|
||
else:
|
||
self.log("预置应用清理部分失败,继续刷入语言包", "WARNING")
|
||
return False
|
||
|
||
self.log("预置应用清理完成", "SUCCESS")
|
||
return True
|
||
|
||
def push_all_apks(self):
|
||
"""推送APK并安装 —— 逸动版仅处理 app 目录,使用 pm install"""
|
||
# 检查设备连接(仅 UI 层检查在主线程,其余工作进后台线程)
|
||
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_push_all():
|
||
# 验证授权
|
||
if not self.check_authorization(self.vin):
|
||
self.run_on_ui_thread(
|
||
lambda: 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(
|
||
lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_data_prepare_failed'))
|
||
)
|
||
return
|
||
|
||
# 解压
|
||
if not self.check_package_extracted():
|
||
self.log("正在准备资源...", "INFO")
|
||
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(
|
||
lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_prepare_failed'))
|
||
)
|
||
return
|
||
self.show_progress(False, is_push=False)
|
||
|
||
if not self.apps_dir or not self.apps_dir.exists():
|
||
self.run_on_ui_thread(
|
||
lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_dir_missing'))
|
||
)
|
||
return
|
||
|
||
# 开始刷入
|
||
self.show_progress(True, is_push=True)
|
||
self.log("开始刷入语言包...", "INFO")
|
||
self.run_adb_shell('mkdir -p /data/local/tmp')
|
||
self.run_adb_shell('setprop vecentek.model 1')
|
||
self.cleanup_preinstalled_apps_for_language()
|
||
|
||
success_count = 0
|
||
try:
|
||
all_apks = list(self.apps_dir.glob("*.apk"))
|
||
if not all_apks:
|
||
self.log("未找到语言包文件", "WARNING")
|
||
return
|
||
|
||
total = len(all_apks)
|
||
for i, apk_path in enumerate(all_apks, 1):
|
||
apk_name = apk_path.stem
|
||
ok, _ = self.push_single_apk(apk_path, apk_name)
|
||
if ok:
|
||
if self.debug_mode:
|
||
self.log(f"安装成功: {apk_name}.apk", "SUCCESS")
|
||
success_count += 1
|
||
else:
|
||
if self.debug_mode:
|
||
self.log(f"安装失败: {apk_name}.apk", "ERROR")
|
||
else:
|
||
self.log(f"语言包刷入失败: {i}/{total}", "ERROR")
|
||
self.update_progress(i, total, "正在刷入", is_push=True)
|
||
|
||
self.update_progress(total, total, "刷入完成", is_push=True)
|
||
|
||
if success_count > 0:
|
||
self.log("语言包刷入完成,重启设备后生效", "SUCCESS")
|
||
else:
|
||
self.log("语言包刷入失败", "ERROR")
|
||
finally:
|
||
self.show_progress(False, is_push=True)
|
||
|
||
threading.Thread(target=do_push_all, daemon=True).start()
|
||
|
||
def install_all_apks(self):
|
||
"""批量安装APK — push → pm install → cleanup"""
|
||
if not self.check_device_connection():
|
||
return
|
||
if not self.vin:
|
||
messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码")
|
||
return
|
||
if not self.check_authorization(self.vin):
|
||
messagebox.showerror("授权失败", "设备未授权,无法执行此操作")
|
||
return
|
||
|
||
# 使用当前目录下的apks文件夹
|
||
apk_dir = self.base_dir / "apks"
|
||
if not apk_dir.exists():
|
||
apk_dir = find_resource("apks")
|
||
|
||
# 检查apk文件夹是否存在
|
||
if not apk_dir.exists():
|
||
messagebox.showerror("错误", "未找到apks文件夹!\n请在程序目录下创建apks文件夹并放入APK文件。")
|
||
self.log("未找到apks文件夹", "ERROR")
|
||
return
|
||
|
||
# 查找所有apk文件
|
||
apk_files = list(apk_dir.glob("*.apk"))
|
||
if not apk_files:
|
||
messagebox.showerror("错误", "apks文件夹中没有找到APK文件!")
|
||
self.log("apk文件夹中没有找到APK文件", "ERROR")
|
||
return
|
||
|
||
# 询问是否确认安装
|
||
result = messagebox.askyesno("确认安装",
|
||
f"找到 {len(apk_files)} 个APK文件\n\n是否开始批量安装?")
|
||
if not result:
|
||
return
|
||
|
||
def install():
|
||
self.show_progress(True, is_push=True)
|
||
total = len(apk_files)
|
||
self.log(f"开始批量安装 {total} 个APK...", "INFO")
|
||
|
||
success_count = 0
|
||
try:
|
||
self.run_adb_shell('setprop vecentek.model 1')
|
||
|
||
for i, apk_path in enumerate(apk_files, 1):
|
||
apk_name = apk_path.stem
|
||
self.update_progress(i, total, "安装中...", is_push=True)
|
||
if self._push_and_install(apk_path, apk_name):
|
||
self.log(f"安装成功: {apk_name}.apk", "SUCCESS")
|
||
success_count += 1
|
||
else:
|
||
self.log(f"安装失败: {apk_name}.apk", "ERROR")
|
||
|
||
self.update_progress(total, total, "安装完成", is_push=True)
|
||
|
||
if success_count == total:
|
||
self.run_on_ui_thread(messagebox.showinfo, "安装完成", f"成功安装 {total} 个APK!")
|
||
elif success_count > 0:
|
||
self.run_on_ui_thread(messagebox.showwarning, "部分成功", f"成功: {success_count}\n失败: {total - success_count}")
|
||
else:
|
||
self.log("安装失败", "ERROR")
|
||
self.run_on_ui_thread(messagebox.showerror, "安装失败", "所有APK安装失败!")
|
||
except Exception as e:
|
||
self.log(f"安装过程异常: {str(e)}", "ERROR")
|
||
self.run_on_ui_thread(messagebox.showerror, "安装失败", f"安装过程异常:{str(e)}")
|
||
finally:
|
||
self.show_progress(False, is_push=True)
|
||
|
||
threading.Thread(target=install, daemon=True).start()
|
||
|
||
def install_single_apk(self):
|
||
"""安装单个APK — push → pm install → cleanup"""
|
||
if not self.check_device_connection():
|
||
return
|
||
if not self.vin:
|
||
messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码")
|
||
return
|
||
if not self.check_authorization(self.vin):
|
||
messagebox.showerror("授权失败", "设备未授权,无法执行此操作")
|
||
return
|
||
|
||
file_path = filedialog.askopenfilename(
|
||
title="选择APK文件",
|
||
filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")]
|
||
)
|
||
|
||
if not file_path:
|
||
return
|
||
|
||
def install():
|
||
apk_name = Path(file_path).stem
|
||
self.show_progress(True, is_push=True)
|
||
self.update_progress(30, 100, "安装中", is_push=True)
|
||
|
||
try:
|
||
self.run_adb_shell('setprop vecentek.model 1')
|
||
success = self._push_and_install(file_path, apk_name)
|
||
self.update_progress(100, 100, "完成", is_push=True)
|
||
if success:
|
||
self.log("安装成功", "SUCCESS")
|
||
else:
|
||
self.log("安装失败", "ERROR")
|
||
except Exception as e:
|
||
self.log(f"安装过程异常: {str(e)}", "ERROR")
|
||
finally:
|
||
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_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)
|
||
locale_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'), locale_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():
|
||
success, output = self.run_adb_shell(
|
||
f'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:
|
||
self.log(self.tf('log_quick_lang_failed', output=output), "ERROR")
|
||
self.run_on_ui_thread(
|
||
messagebox.showerror,
|
||
self.t('quick_lang_failed_title'),
|
||
self.tf('quick_lang_failed', output=output)
|
||
)
|
||
|
||
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_shell('am start -a android.settings.TIMEZONE_SETTINGS')
|
||
|
||
def open_android_settings(self):
|
||
"""打开安卓原生设置"""
|
||
if not self.check_device_connection():
|
||
return
|
||
self.run_adb_shell('am start -a android.settings.SETTINGS')
|
||
|
||
def reboot_device(self):
|
||
"""重启设备"""
|
||
if not self.check_device_connection():
|
||
return
|
||
if messagebox.askyesno("确认重启", "确定要重启设备吗?"):
|
||
proc = subprocess.Popen(f'{self._adb_cmd()} -d shell reboot', shell=True,
|
||
stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||
try:
|
||
proc.stdin.write(b'adb36987\n')
|
||
proc.stdin.flush()
|
||
proc.stdin.close()
|
||
except:
|
||
pass
|
||
self.log("设备正在重启...", "INFO")
|
||
self.update_device_status(False)
|
||
|
||
def on_disable_upgrade(self):
|
||
"""禁用系统升级"""
|
||
# 检查设备连接
|
||
if not self.check_device_connection():
|
||
return
|
||
|
||
# 弹窗确认
|
||
result = messagebox.askyesno(
|
||
"确认禁用升级",
|
||
"⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n"
|
||
"是否确定要禁用系统升级应用?\n\n"
|
||
"禁用命令:\n"
|
||
"adb shell pm disable-user --user 0 com.incall.apps.softmanager"
|
||
)
|
||
|
||
if not result:
|
||
self.log("已取消禁用升级操作", "INFO")
|
||
return
|
||
|
||
def disable():
|
||
self.show_progress(True, is_push=False)
|
||
success, output = self.run_adb_shell(
|
||
'pm disable-user --user 0 com.incall.apps.softmanager')
|
||
|
||
if success:
|
||
self.log("系统升级已禁用", "SUCCESS")
|
||
self.run_on_ui_thread(messagebox.showinfo, "成功", "系统升级已成功禁用!")
|
||
else:
|
||
self.log("禁用系统升级失败", "ERROR")
|
||
self.run_on_ui_thread(messagebox.showerror, "错误", f"禁用失败:{output}")
|
||
|
||
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_warn_title'), self.t('msg_input_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.pwd_result_label.config(
|
||
text=f"密码: *#{pwd}#*",
|
||
fg=self.colors['success']
|
||
)
|
||
self.log(f"密码查询成功 VIN={vin} -> {pwd}", "SUCCESS")
|
||
else:
|
||
msg = data.get('message', '查询失败')
|
||
self.pwd_result_label.config(
|
||
text=f"失败: {msg}",
|
||
fg=self.colors['error']
|
||
)
|
||
self.log(f"密码查询失败: {msg}", "ERROR")
|
||
|
||
self.run_on_ui_thread(update_ui)
|
||
|
||
except Exception as e:
|
||
def update_ui_error():
|
||
self.pwd_result_label.config(
|
||
text=f"请求失败",
|
||
fg=self.colors['error']
|
||
)
|
||
self.log(f"密码查询请求失败: {str(e)}", "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("调试模式已关闭", "WARNING")
|
||
self.status_text.config(text="就绪")
|
||
self.refresh_device_status()
|
||
return
|
||
|
||
pwd = simpledialog.askstring("调试模式", "请输入调试密码:", show='*', parent=self.root)
|
||
if pwd == "zxch5200":
|
||
self.debug_mode = True
|
||
self.update_device_status(True, "", True)
|
||
self.log("🔧 调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志", "WARNING")
|
||
self.status_text.config(text="🔧 调试模式")
|
||
elif pwd is not None:
|
||
messagebox.showwarning("错误", "密码错误")
|
||
|
||
def install_apps(self):
|
||
"""安装App — 支持单选或多选APK文件"""
|
||
if not self.check_device_connection():
|
||
return
|
||
if not self.vin and not self.debug_mode:
|
||
messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码")
|
||
return
|
||
if not self.check_authorization(self.vin):
|
||
return
|
||
|
||
file_paths = filedialog.askopenfilenames(
|
||
title="选择APK文件",
|
||
filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")]
|
||
)
|
||
if not file_paths:
|
||
return
|
||
|
||
count = len(file_paths)
|
||
result = messagebox.askyesno("确认安装", f"已选择 {count} 个APK文件\n\n是否开始安装?")
|
||
if not result:
|
||
return
|
||
|
||
def install():
|
||
self.show_progress(True, is_push=True)
|
||
self.log(f"开始安装 {count} 个APK...", "INFO")
|
||
success_count = 0
|
||
try:
|
||
self.run_adb_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, f"安装中 ({apk_name})", is_push=True)
|
||
if self._push_and_install(file_path, apk_name):
|
||
self.log(f"✓ {apk_name}.apk", "SUCCESS")
|
||
success_count += 1
|
||
else:
|
||
self.log(f"✗ {apk_name}.apk", "ERROR")
|
||
|
||
self.update_progress(count, count, "安装完成", is_push=True)
|
||
|
||
if success_count == count:
|
||
self.log(f"安装完成:全部 {count} 个成功", "SUCCESS")
|
||
self.run_on_ui_thread(messagebox.showinfo, "安装完成", f"成功安装 {count} 个APK!")
|
||
elif success_count > 0:
|
||
self.log(f"安装完成:{success_count}/{count} 成功", "WARNING")
|
||
self.run_on_ui_thread(messagebox.showwarning, "部分成功", f"成功: {success_count}\n失败: {count - success_count}")
|
||
else:
|
||
self.log("安装失败", "ERROR")
|
||
self.run_on_ui_thread(messagebox.showerror, "安装失败", "所有APK安装失败!")
|
||
except Exception as e:
|
||
self.log(f"安装过程异常: {str(e)}", "ERROR")
|
||
self.run_on_ui_thread(messagebox.showerror, "安装失败", f"安装过程异常:{str(e)}")
|
||
finally:
|
||
self.show_progress(False, is_push=True)
|
||
|
||
threading.Thread(target=install, daemon=True).start()
|
||
|
||
# ============================================================
|
||
# hosts 文件修改
|
||
# ============================================================
|
||
|
||
def modify_hosts(self):
|
||
"""修改hosts文件,添加云端认证DNS映射"""
|
||
hosts_path = r"C:\Windows\System32\drivers\etc\hosts"
|
||
host_ip = "103.236.55.140"
|
||
host_name = "spm.auto-pai.com"
|
||
entry = f"{host_ip} {host_name}"
|
||
|
||
try:
|
||
try:
|
||
with open(hosts_path, 'r', encoding='utf-8') as f:
|
||
lines = f.readlines()
|
||
except UnicodeDecodeError:
|
||
with open(hosts_path, 'r', encoding='gbk', errors='replace') as f:
|
||
lines = f.readlines()
|
||
|
||
new_lines = []
|
||
changed = False
|
||
has_exact = False
|
||
for line in lines:
|
||
stripped = line.strip()
|
||
if not stripped or stripped.startswith('#'):
|
||
new_lines.append(line)
|
||
continue
|
||
|
||
body, _, _ = line.partition('#')
|
||
parts = body.split()
|
||
if len(parts) >= 2 and host_name.lower() in [p.lower() for p in parts[1:]]:
|
||
if parts[0] == host_ip and len(parts) == 2:
|
||
has_exact = True
|
||
new_lines.append(line)
|
||
else:
|
||
changed = True
|
||
continue
|
||
|
||
new_lines.append(line)
|
||
|
||
if has_exact and not changed:
|
||
return True
|
||
|
||
if not new_lines or (new_lines[-1] and not new_lines[-1].endswith(('\n', '\r'))):
|
||
new_lines.append('\n')
|
||
new_lines.append(f"{entry}\n")
|
||
|
||
with open(hosts_path, 'w', encoding='utf-8', newline='') as f:
|
||
f.writelines(new_lines)
|
||
return True
|
||
except PermissionError:
|
||
self.log("需要管理员权限,请以管理员身份运行", "WARNING")
|
||
return False
|
||
except Exception as e:
|
||
self.log(f"云端文件准备失败: {str(e)}", "WARNING")
|
||
return False
|
||
|
||
def _run_netsh(self, command):
|
||
"""执行 netsh 命令,返回 (returncode, output)"""
|
||
try:
|
||
for enc in ['utf-8', 'gbk']:
|
||
try:
|
||
r = subprocess.run(
|
||
command,
|
||
shell=True,
|
||
capture_output=True,
|
||
text=True,
|
||
encoding=enc,
|
||
errors='replace',
|
||
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||
)
|
||
output = (r.stdout or '') + (r.stderr or '')
|
||
return r.returncode, output.strip()
|
||
except (UnicodeDecodeError, LookupError):
|
||
continue
|
||
return -1, "解码失败"
|
||
except Exception as e:
|
||
return -1, str(e)
|
||
|
||
def start_hotspot_action(self):
|
||
"""打开热点设置并自动轮询检测"""
|
||
# 打开设置页面
|
||
try:
|
||
subprocess.Popen(
|
||
'start ms-settings:network-mobilehotspot',
|
||
shell=True,
|
||
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||
)
|
||
except:
|
||
pass
|
||
self.log("已打开热点设置,正在检测热点...", "INFO")
|
||
|
||
# 后台轮询,查到为止(最多 30 秒)
|
||
def poll():
|
||
for _ in range(15):
|
||
time.sleep(2)
|
||
ssid, pwd, status = self.get_hotspot_info()
|
||
self.refresh_hotspot_display()
|
||
if ssid and status == "已启动":
|
||
self.log(f"检测到热点: {ssid} / {pwd}", "SUCCESS")
|
||
return
|
||
self.log("未检测到热点,请确认已开启", "WARNING")
|
||
|
||
threading.Thread(target=poll, daemon=True).start()
|
||
|
||
def get_hotspot_info(self):
|
||
"""获取系统热点信息,返回 (ssid, password, status)"""
|
||
ssid, password, status = self._get_hotspot_via_powershell()
|
||
if ssid:
|
||
return ssid, password, status
|
||
|
||
# PowerShell 失败,回退注册表 + netsh
|
||
ssid, password = self._read_hotspot_registry()
|
||
status = "未启动"
|
||
try:
|
||
_, out = self._run_netsh('netsh wlan show hostednetwork')
|
||
for line in out.split('\n'):
|
||
s = line.strip()
|
||
if ('状态' in s or 'status' in s.lower()) and ('已启动' in s or 'started' in s.lower()):
|
||
status = "已启动"
|
||
if not ssid and 'ssid' in s.lower() and ':' in s:
|
||
val = s.split(':', 1)[-1].strip().strip('"')
|
||
if val and 'not set' not in val.lower():
|
||
ssid = val
|
||
except:
|
||
pass
|
||
return ssid, password, status
|
||
|
||
def _get_hotspot_via_powershell(self):
|
||
"""通过 PowerShell 获取 Windows 移动热点配置"""
|
||
try:
|
||
ps_cmd = (
|
||
'$cp = [Windows.Networking.Connectivity.NetworkInformation,'
|
||
'Windows.Networking.Connectivity,ContentType=WindowsRuntime]'
|
||
'::GetInternetConnectionProfile();'
|
||
'$tm = [Windows.Networking.NetworkOperators.NetworkOperatorTetheringManager,'
|
||
'Windows.Networking.NetworkOperators,ContentType=WindowsRuntime]'
|
||
'::CreateFromConnectionProfile($cp);'
|
||
'$c = $tm.GetCurrentAccessPointConfiguration();'
|
||
'Write-Output $c.Ssid; Write-Output $c.Passphrase;'
|
||
'Write-Output $tm.TetheringOperationalState'
|
||
)
|
||
r = subprocess.run(
|
||
['powershell', '-NoProfile', '-Command', ps_cmd],
|
||
capture_output=True, text=True, timeout=10,
|
||
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||
)
|
||
lines = [l.strip() for l in (r.stdout or '').split('\n') if l.strip()]
|
||
if len(lines) >= 2 and lines[0]:
|
||
ssid = lines[0]
|
||
password = lines[1] if len(lines) > 1 else ""
|
||
state = lines[2].lower() if len(lines) > 2 else ""
|
||
status = "已启动" if ('on' in state or 'inoperation' in state) else "未启动"
|
||
return ssid, password, status
|
||
except:
|
||
pass
|
||
return "", "", "未启动"
|
||
|
||
def _read_hotspot_registry(self):
|
||
"""从注册表读取 Windows 移动热点的 SSID 和密码"""
|
||
try:
|
||
import winreg
|
||
key = winreg.OpenKey(
|
||
winreg.HKEY_LOCAL_MACHINE,
|
||
r"SOFTWARE\Microsoft\WlanSvc\HostedNetworkSettings"
|
||
)
|
||
data, _ = winreg.QueryValueEx(key, "HostedNetworkSettings")
|
||
winreg.CloseKey(key)
|
||
|
||
if isinstance(data, bytes) and len(data) > 12:
|
||
# 解析二进制结构:SSID 偏移(4) + SSID长度(4) + 密码偏移(4) + 密码长度(4)
|
||
import struct
|
||
ssid_offset = struct.unpack_from('<I', data, 4)[0]
|
||
ssid_len = struct.unpack_from('<I', data, 8)[0]
|
||
pwd_offset = struct.unpack_from('<I', data, 12)[0]
|
||
pwd_len = struct.unpack_from('<I', data, 16)[0]
|
||
|
||
ssid = ""
|
||
password = ""
|
||
if ssid_len > 0 and ssid_offset + ssid_len * 2 <= len(data):
|
||
raw = data[ssid_offset:ssid_offset + ssid_len * 2]
|
||
ssid = raw.decode('utf-16-le', errors='replace').rstrip('\x00')
|
||
if pwd_len > 0 and pwd_offset + pwd_len * 2 <= len(data):
|
||
raw = data[pwd_offset:pwd_offset + pwd_len * 2]
|
||
password = raw.decode('utf-16-le', errors='replace').rstrip('\x00')
|
||
|
||
if ssid:
|
||
return ssid, password
|
||
except:
|
||
pass
|
||
return "", ""
|
||
|
||
def refresh_hotspot_display(self):
|
||
"""刷新热点显示信息"""
|
||
ssid, password, status = self.get_hotspot_info()
|
||
self.run_on_ui_thread(self._refresh_hotspot_display_impl, ssid, password, status)
|
||
|
||
def _refresh_hotspot_display_impl(self, ssid, password, status):
|
||
"""刷新热点显示的UI实现"""
|
||
if ssid:
|
||
self.hotspot_ssid_label.config(text=self.tf('hotspot_name_value', ssid=ssid))
|
||
else:
|
||
self.hotspot_ssid_label.config(text=self.t('hotspot_name_empty'))
|
||
if password:
|
||
self.hotspot_pwd_label.config(text=self.tf('hotspot_pwd_value', password=password))
|
||
else:
|
||
self.hotspot_pwd_label.config(text=self.t('hotspot_pwd_default'))
|
||
self.hotspot_status_label.config(
|
||
text=self.tf('hotspot_status_value', status=status),
|
||
fg=self.colors['success'] if '已启动' in status else self.colors['warning']
|
||
)
|
||
|
||
def _debug_test_extract(self, event=None):
|
||
"""Debug-only package extraction test."""
|
||
if not getattr(self, 'debug_mode', False):
|
||
messagebox.showwarning("Debug mode", "Press Ctrl+Shift+D to enable debug mode first")
|
||
return
|
||
|
||
pwd = simpledialog.askstring("Test extraction", "Enter package.bin password:", 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("Test extraction succeeded", "SUCCESS")
|
||
self.run_on_ui_thread(
|
||
messagebox.showinfo,
|
||
"Test extraction succeeded",
|
||
f"Resources extracted to:\n{self.temp_dir}"
|
||
)
|
||
else:
|
||
self.log("Test extraction failed", "ERROR")
|
||
self.run_on_ui_thread(messagebox.showerror, "Test extraction failed", "Check the 7za output in logs")
|
||
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或更高版本")
|
||
sys.exit(1)
|
||
|
||
try:
|
||
app = ADKAPKGUI()
|
||
app.run()
|
||
except Exception as e:
|
||
print(f"启动失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
messagebox.showerror("错误", f"程序启动失败: {e}")
|
||
|
||
if __name__ == "__main__":
|
||
main()
|