1399 lines
59 KiB
Python
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()
|