Files
language-installer/CS75Pro/CS75Pro_Installer.py
T
2026-07-07 14:37:04 +08:00

2233 lines
96 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()