commit bcde9712b075e0c71dcc22b08329e8c247b46075 Author: soft_windy <317709796@qq.com> Date: Fri May 15 19:47:31 2026 +0800 first commit diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..cea507e --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(python *)" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a1032a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +*.pyc +__pycache__/ +*.pyd +*.pyo + +# 打包产物 +build/ +dist/ +dist_cy/ +dist_obf/ + +# APK文件夹 +apks/ + +# 资源包 +package.bin + +# IDE +.vscode/ +.idea/ + +# 临时文件 +*.log +*.tmp diff --git a/.spec b/.spec new file mode 100644 index 0000000..9905e9b --- /dev/null +++ b/.spec @@ -0,0 +1,47 @@ +# -*- mode: python ; coding: utf-8 -*- +from PyInstaller.utils.hooks import collect_all + +datas = [('adb.exe', '.'), ('AdbWinApi.dll', '.'), ('AdbWinUsbApi.dll', '.'), ('7za.exe', '.')] +binaries = [] +hiddenimports = ['queue', 'threading', 'tkinter', 'zipfile', 'json', 'urllib'] +tmp_ret = collect_all('tkinter') +datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] + + +a = Analysis( + ['app-yidong.py'], + pathex=[], + binaries=binaries, + datas=datas, + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + uac_admin=True, + icon=['app.ico'], +) diff --git a/7za.exe b/7za.exe new file mode 100644 index 0000000..7f6bf86 Binary files /dev/null and b/7za.exe differ diff --git a/AdbWinApi.dll b/AdbWinApi.dll new file mode 100644 index 0000000..dd0d9a4 Binary files /dev/null and b/AdbWinApi.dll differ diff --git a/AdbWinUsbApi.dll b/AdbWinUsbApi.dll new file mode 100644 index 0000000..d791dbf Binary files /dev/null and b/AdbWinUsbApi.dll differ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..00dbd9c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,151 @@ +# 长安语言刷入工具 (Changan Language Flashing Tool) + +## Overview + +A Windows GUI tool suite (Python 3.6+ / tkinter) for flashing multi-language APKs onto Android-based vehicle infotainment systems. Built by 宜宾科宜科技有限公司. + +The suite contains **5 tool variants**, each targeting a different vehicle model. All share the same `ADKAPKGUI` class architecture with model-specific customizations. Authorization is VIN-based via a cloud API. + +| Tool file | Vehicle | Window title | Pack script | +|-----------|---------|-------------|-------------| +| `app.py` | 启源Q07 | 长安语言安装工具 | `pack_q07.bat` | +| `S05.py` | 深蓝S05 | 深蓝S05多语言安装 | `pack_s05.bat` | +| `X5plusTool.py` | X5plus | 适用于X5plus多语言安装 | `pack_x5plus.bat` | +| `app-install.py` | 长安逸动 (通用) | 长安语言刷入工具 | `pack_common.bat` | +| `app-yidong.py` | 长安逸动 | 长安逸动语言刷入工具 | `pack_yidong.bat` | + +## Project structure + +``` +├── app.py # 启源Q07 tool +├── S05.py # 深蓝S05 tool +├── X5plusTool.py # X5plus tool +├── app-install.py # 逸动通用 tool +├── app-yidong.py # 逸动 tool +├── pack_q07.bat # Q07 pack script +├── pack_s05.bat # S05 pack script +├── pack_x5plus.bat # X5plus pack script +├── pack_common.bat # 逸动通用 pack script +├── pack_yidong.bat # 逸动 pack script +├── test_extract.py # Standalone test for package.bin extraction +├── app.ico # Application icon +├── package.bin # Encrypted ZIP (not in repo) +├── adb.exe # Bundled ADB +├── AdbWinApi.dll # ADB dependency +├── AdbWinUsbApi.dll # ADB dependency +└── .vscode/ + └── settings.json +``` + +## Architecture + +### Single-file design + +Each tool is a single Python file under the `ADKAPKGUI` class. No MVC separation — the class handles GUI, business logic, ADB operations, network calls, and file extraction. + +### Threading model + +- **Main thread**: tkinter event loop (`root.mainloop()`) +- **Background threads**: All user-triggered operations spawn `threading.Thread(target=..., daemon=True)` +- **Thread-safety**: UI mutations via `self.run_on_ui_thread(func, ...)` → `self.root.after(0, func, ...)`. Internal `_impl` methods are the actual tkinter-touching implementations. + +### ADB operations + +- `run_adb_command(command)` — standard ADB commands; auto-substitutes `adb` with bundled `adb.exe` path when frozen +- `run_adb_shell(shell_command)` — only in app-install.py / app-yidong.py; shells into device with auto-password `adb36987` + +ADB path is portable: `self.adb` resolves to `sys._MEIPASS/adb.exe` when frozen (PyInstaller bundle), or `'adb'` in development. + +### Authentication flow + +1. ADB detects device → reads VIN (`ca_vin_info` or `VIN`; 逸动 uses `ca.car.vin`) +2. VIN sent to `https://api.changan.softwindy.cn/api/authorizations/auth-check?vin=...` +3. Package password from `https://api.changan.softwindy.cn/api/authorizations/package-key?vin=...` +4. Password used to decrypt `package.bin` + +### Package extraction + +- `package.bin` is a password-protected ZIP (AES via pyzipper, fallback to zipfile) +- Extracted to `%LOCALAPPDATA%\.cache\system\.android\apps_cache_{variant}` (hidden via `attrib +h`) +- Each tool has its own cache folder to avoid conflicts: + - app.py → `apps_cache_Q07` + - S05.py → `apps_cache_S05` + - X5plusTool.py → `apps_cache_X5plus` + - app-install.py → `apps_cache_common` + - app-yidong.py → `apps_cache_common` (default) +- Extracted APKs in `app/` pushed to `/system/app/`, `priv-app/` to `/system/priv-app/` + +### Device status monitoring + +Background daemon thread runs `adb devices` every 5 seconds to detect connect/disconnect events. + +## GUI layout (650x640 dark theme for most; 650x550 for 逸动) + +| Section | Contents | +|---------|----------| +| Title bar | Vehicle model + company subtitle | +| Password query | Hidden VIN→password lookup (S05, app-install only) | +| Button row 1 | 获取权限 (Q07/S05), 刷入语言包, 安装App, 语言设置 | +| Button row 2 | 时区设置, 安卓设置, 重启设备, 禁用升级 | +| Status bar | Connection dot, VIN, Auth status, Refresh button | +| Tips | Usage warnings | +| Progress | Extraction + push progress (hidden by default) | +| Log area | ScrolledText with tags (INFO/SUCCESS/ERROR/WARNING/CMD), 清空日志 button in title bar | + +## Key features + +### Install App (unified single/batch) +"安装App" button uses `filedialog.askopenfilenames` for multi-file selection, replacing old separate "单个安装" and "批量安装" buttons. Select 1 or more APKs and installs all in one pass (`adb install -r`). + +### Debug mode (hidden) +Press `Ctrl+Shift+D` → password `zxch5200` to enter debug mode: +- Bypasses device connection and authorization checks (UI shows "已连接" / "已授权") +- Logs all raw ADB commands and output to the log panel +- Exiting debug mode auto-refreshes real device state + +### Language quick-set +Popup with 8 one-click locale switches (zh-CN, en-US, ru-RU, fr-FR, es-ES, pt-BR, it-IT, ar-SA). Also links to native Android language settings. Changes take effect after reboot. + +### Model-specific behaviors + +| Feature | Q07 (app.py) | S05 (S05.py) | X5plus (X5plusTool.py) | 逸动 (app-install/yidong) | +|---------|-------------|-------------|------------------------|---------------------------| +| Root perm | Y | Y | N (no btn_root) | N | +| priv-app | Y | Y | Y | N (app only) | +| ADB shell | Standard | Standard | Standard | Auto-password `adb36987` | +| VIN key | ca_vin_info/VIN | ca_vin_info/VIN | ca_vin_info/VIN | ca.car.vin | +| Old app cleanup | N | N | Y (5 packages) | N | +| Font push | N | N | Y (FZLTHPro) | N | +| Overlay enable | N | N | N | N (removed) | +| Password query | N | Y | N | Y | +| Factory hints | N | Y | Y (dynamic pwd) | Y | + +## Build process + +Each `pack_*.bat` follows the same pipeline: +1. Install deps: `pyinstaller`, `cython`, `pyzipper` +2. Clean old build dirs +3. Cython compile: `{source}.py` → `_core.pyd` +4. Copy resources: `adb.exe`, DLLs, `app.ico` +5. PyInstaller: single `.exe` with `--uac-admin`, bundling `_core.pyd` + ADB + DLLs +6. Fallback to normal PyInstaller if Cython fails + +Output exe is self-contained — bundles `adb.exe` and DLLs via `sys._MEIPASS`, no external ADB needed. + +## Key behaviors to preserve + +1. **Thread safety**: Never call tkinter from background threads — always use `run_on_ui_thread` +2. **VIN-based auth**: Authorization required before push; flow must remain intact +3. **Silent extraction**: Auto-extract `package.bin` without user step +4. **ADB portability**: `self.adb` resolves to bundled exe path; all commands go through `run_adb_command` +5. **vecentek.model**: Set to 1 before install, 0 after +6. **Chinese encoding**: `# -*- coding: utf-8 -*-` throughout +7. **Debug mode**: `self.debug_mode` flag gates auth/connection bypass and verbose logging +8. **Separate cache dirs**: Each tool extracts to its own cache folder + +## Known issues + +- `test_extract.py` hardcodes password — should fetch from API +- `on_disable_upgrade` uses `findstr` (Windows-specific) +- Exception handling is minimal in many places (bare `except: pass`) +- 逸动 tools (app-install/yidong) use `subprocess.Popen` with stdin password injection — fragile diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc0ab5e --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# language_installer + +长安语言刷入工具 — 适用于启源Q07、深蓝S05、X5plus、长安逸动等多车型的Android车机多语言APK刷入工具套件。 diff --git a/S05.py b/S05.py new file mode 100644 index 0000000..ad77e37 --- /dev/null +++ b/S05.py @@ -0,0 +1,1348 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import subprocess +import json +import threading +import tkinter as tk +from tkinter import ttk, scrolledtext, filedialog, messagebox +from pathlib import Path +from urllib.request import urlopen, Request +from urllib.error import URLError, HTTPError +from datetime import datetime +import zipfile +try: + import pyzipper +except ImportError: + pyzipper = None +import shutil +import time + +class ADKAPKGUI: + def __init__(self): + self.root = tk.Tk() + self.root.title("长安语言安装工具") + self.root.geometry("650x640") + self.root.resizable(True, True) + + # 设置颜色主题 + self.colors = { + 'bg_dark': '#1e1e2e', + 'bg_light': '#2a2a3e', + 'accent': '#6c5ce7', + 'accent_hover': '#5b4bc4', + 'success': '#00b894', + 'error': '#d63031', + 'warning': '#fdcb6e', + 'info': '#0984e3', + 'text': '#dfe6e9', + 'text_secondary': '#b2bec3', + 'border': '#3d3d5e' + } + + # 从 exe/py 所在目录查找资源文件 + self.base_dir = Path(sys.executable).parent if getattr(sys, 'frozen', False) else Path(__file__).parent + if getattr(sys, 'frozen', False): + self.adb = str(Path(sys._MEIPASS) / 'adb.exe') + self.sz = str(Path(sys._MEIPASS) / '7za.exe') + else: + self.adb = 'adb' + self.sz = str(self.base_dir / '7za.exe') + self.package_file = self.base_dir / "package.bin" + self.extract_password = None + self.apps_dir = None + self.priv_apps_dir = None + self.temp_dir = None + self.api_url = "https://api.changan.softwindy.cn/api/authorizations/auth-check" + self.vin = None + self.device_connected = False + self._refreshing = False # 防止并发刷新 + self.debug_mode = False # 调试模式 + + # 设置样式 + self.setup_styles() + self.setup_ui() + self.center_window() + + # 检查环境 + self.check_environment() + + # 启动设备状态监控 + self.start_device_monitor() + + def setup_styles(self): + """设置自定义样式""" + style = ttk.Style() + style.theme_use('clam') + + # 配置主颜色 + style.configure('TFrame', background=self.colors['bg_dark']) + style.configure('TLabel', background=self.colors['bg_dark'], foreground=self.colors['text']) + style.configure('TLabelframe', background=self.colors['bg_dark'], foreground=self.colors['text']) + style.configure('TLabelframe.Label', background=self.colors['bg_dark'], foreground=self.colors['accent']) + + # 配置进度条 + style.configure('TProgressbar', + background=self.colors['accent'], + troughcolor=self.colors['bg_light'], + borderwidth=0) + + def setup_ui(self): + """设置UI界面""" + # 配置根窗口 + self.root.configure(bg=self.colors['bg_dark']) + + # 创建主框架 + main_frame = tk.Frame(self.root, bg=self.colors['bg_dark']) + main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 顶部标题栏 + title_frame = tk.Frame(main_frame, bg=self.colors['bg_dark'], height=65) + title_frame.pack(fill=tk.X, pady=(0, 10)) + title_frame.pack_propagate(False) + + # 标题 + title_label = tk.Label(title_frame, + text="🚀 深蓝S05多语言安装", + font=('Microsoft YaHei', 18, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + title_label.pack() + + subtitle_label = tk.Label(title_frame, + text="宜宾科宜科技有限公司 - 智能设备管理平台", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + subtitle_label.pack() + + # 工程密码查询区域 + pwd_query_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + pwd_query_frame.pack(fill=tk.X, pady=(0, 5), padx=5) + + tk.Label(pwd_query_frame, text="工程密码查询:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT, padx=(10, 5), pady=5) + + self.vin_input = tk.Entry(pwd_query_frame, + font=('Consolas', 9), + bg='#2d2d3d', + fg='#636e72', + insertbackground='white', + relief=tk.FLAT, + width=20) + self.vin_input.insert(0, "请输入VIN") + self.vin_input.bind("", self._on_vin_input_focus_in) + self.vin_input.bind("", self._on_vin_input_focus_out) + self.vin_input.pack(side=tk.LEFT, padx=5, pady=5) + + self.btn_query_pwd = tk.Button(pwd_query_frame, text="查询密码", + command=self.query_password_by_vin, + font=('Microsoft YaHei', 8), + fg='white', + bg=self.colors['accent'], + relief=tk.FLAT, + cursor='hand2') + self.btn_query_pwd.pack(side=tk.LEFT, padx=5, pady=5) + + self.pwd_result_label = tk.Label(pwd_query_frame, text="", + font=('Microsoft YaHei', 9, 'bold'), + fg=self.colors['success'], + bg=self.colors['bg_light']) + self.pwd_result_label.pack(side=tk.LEFT, padx=10, pady=5) + + # 工厂模式提示 + factory_hint_frame = tk.Frame(main_frame, bg=self.colors['bg_dark']) + factory_hint_frame.pack(fill=tk.X, pady=(0, 3)) + tk.Label(factory_hint_frame, text="🔧 关闭车辆WI-FI和4G网络,拨号获取的密码进入工程模式", + font=('Microsoft YaHei', 8), + fg=self.colors['warning'], + bg=self.colors['bg_dark']).pack(side=tk.LEFT, padx=2) + + + # 按钮区域(两排,每排5个) + button_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + button_frame.pack(fill=tk.X, pady=(0, 10), padx=5) + + # 按钮样式参数 + btn_params = { + 'font': ('Microsoft YaHei', 9), + 'fg': 'white', + 'relief': tk.FLAT, + 'cursor': 'hand2', + 'height': 1, + 'width': 14 + } + + # 第一排按钮 + row1_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + row1_frame.pack(pady=(8, 4)) + + self.btn_root = tk.Button(row1_frame, text="🔓 获取权限", + command=self.get_root_permission, + bg=self.colors['success'], + **btn_params) + self.btn_root.pack(side=tk.LEFT, padx=4) + + self.btn_push = tk.Button(row1_frame, text="📦 刷入语言包", + command=self.push_all_apks, + bg=self.colors['accent'], + **btn_params) + self.btn_push.pack(side=tk.LEFT, padx=4) + + self.btn_install_all = tk.Button(row1_frame, text="📱 安装App", + command=self.install_apps, + bg=self.colors['accent'], + **btn_params) + self.btn_install_all.pack(side=tk.LEFT, padx=4) + + self.btn_language = tk.Button(row1_frame, text="🌐 语言设置", + command=self.open_language_quick_set, + bg=self.colors['accent'], + **btn_params) + self.btn_language.pack(side=tk.LEFT, padx=4) + + # 第二排按钮 + row2_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + row2_frame.pack(pady=(4, 8)) + + self.btn_timezone = tk.Button(row2_frame, text="⏰ 时区设置", + command=self.open_timezone_settings, + bg=self.colors['accent'], + **btn_params) + self.btn_timezone.pack(side=tk.LEFT, padx=4) + + self.btn_settings = tk.Button(row2_frame, text="⚙️ 安卓设置", + command=self.open_android_settings, + bg=self.colors['accent'], + **btn_params) + self.btn_settings.pack(side=tk.LEFT, padx=4) + + self.btn_reboot = tk.Button(row2_frame, text="🔄 重启设备", + command=self.reboot_device, + bg=self.colors['warning'], + **btn_params) + self.btn_reboot.pack(side=tk.LEFT, padx=4) + + self.btn_exit = tk.Button(row2_frame, text="❌ 禁用升级", + command=self.on_disable_upgrade, + bg=self.colors['error'], + **btn_params) + self.btn_exit.pack(side=tk.LEFT, padx=4) + + # 设备状态栏(横条) + status_bar_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + status_bar_frame.pack(fill=tk.X, pady=(0, 5)) + + # 状态指示器 + status_indicator_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + status_indicator_frame.pack(side=tk.LEFT, padx=10, pady=5) + + self.status_indicator = tk.Canvas(status_indicator_frame, width=10, height=10, + bg=self.colors['bg_light'], highlightthickness=0) + self.status_indicator.pack(side=tk.LEFT) + self.status_dot = self.status_indicator.create_oval(2, 2, 8, 8, fill='#636e72') + + tk.Label(status_indicator_frame, text="设备:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT, padx=(5, 3)) + + self.device_status_label = tk.Label(status_indicator_frame, text="未检测", + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + anchor='w', width=4) + self.device_status_label.pack(side=tk.LEFT) + + # VIN信息 + vin_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + vin_frame.pack(side=tk.LEFT, padx=20, pady=5) + tk.Label(vin_frame, text="VIN码:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT) + self.vin_label = tk.Label(vin_frame, text="未获取", + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + anchor='w', width=17) + self.vin_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 授权状态 + auth_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + auth_frame.pack(side=tk.LEFT, padx=20, pady=5) + tk.Label(auth_frame, text="授权:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT) + self.auth_label = tk.Label(auth_frame, text="未验证", + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + anchor='w', width=4) + self.auth_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 刷新按钮 + refresh_btn = tk.Button(status_bar_frame, text="🔄 检查", + command=self.refresh_device_status, + font=('Microsoft YaHei', 8), + fg=self.colors['accent'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + refresh_btn.pack(side=tk.RIGHT, padx=10, pady=5) + + # 提示信息区域(设备状态下方) + tips_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + tips_frame.pack(fill=tk.X, pady=(5, 5), padx=5) + + tips = [ + "1. 安装语言过程中请保持车辆和电脑的电量充足,不可中途停止。", + "2. 获取权限以后,车辆自动重启以后再进入语言刷入。", + "3. 部分语言需要重启后生效,可以一切工作完成以后再重启。", + ] + + for i, tip in enumerate(tips): + tip_row = tk.Frame(tips_frame, bg=self.colors['bg_light']) + tip_row.pack(fill=tk.X, padx=10, pady=(5 if i == 0 else 0, 5 if i == len(tips) - 1 else 0)) + tk.Label(tip_row, text=tip, + font=('Microsoft YaHei', 9), + fg=self.colors['warning'], + bg=self.colors['bg_light'], + wraplength=600, + justify=tk.LEFT).pack(side=tk.LEFT) + + # 解压进度条框架 + progress_frame = tk.Frame(main_frame, bg=self.colors['bg_dark']) + progress_frame.pack(fill=tk.X, pady=(5, 5)) + + self.progress_label = tk.Label(progress_frame, text="", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + self.progress_label.pack() + + self.progress = ttk.Progressbar(progress_frame, mode='determinate', style='TProgressbar') + self.progress.pack(fill=tk.X, pady=(2, 0)) + + # 推送进度条 + self.push_progress_label = tk.Label(progress_frame, text="", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + + self.push_progress = ttk.Progressbar(progress_frame, mode='determinate', style='TProgressbar') + + # 日志区域(下方) + log_card = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + log_card.pack(fill=tk.BOTH, expand=True, pady=(5, 0)) + + # 日志标题栏 + log_title_frame = tk.Frame(log_card, bg=self.colors['bg_dark'], height=30) + log_title_frame.pack(fill=tk.X) + log_title_frame.pack_propagate(False) + + tk.Label(log_title_frame, text="📋 运行日志", + font=('Microsoft YaHei', 10, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']).pack(side=tk.LEFT, padx=10) + + self.btn_clear = tk.Button(log_title_frame, text="🗑 清空日志", + command=self.clear_log, + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark'], + relief=tk.FLAT, + cursor='hand2') + self.btn_clear.pack(side=tk.RIGHT, padx=10) + + # 日志文本框 + text_frame = tk.Frame(log_card, bg=self.colors['bg_light']) + text_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + self.log_text = scrolledtext.ScrolledText(text_frame, + height=12, + wrap=tk.WORD, + font=('Consolas', 9), + bg='#2d2d3d', + fg='#e0e0e0', + insertbackground='white', + relief=tk.FLAT, + borderwidth=0) + self.log_text.pack(fill=tk.BOTH, expand=True) + + # 配置日志颜色标签 + self.log_text.tag_config('INFO', foreground='#74b9ff') + self.log_text.tag_config('SUCCESS', foreground='#55efc4') + self.log_text.tag_config('ERROR', foreground='#ff7675') + self.log_text.tag_config('WARNING', foreground='#ffeaa7') + self.log_text.tag_config('CMD', foreground='#a29bfe') + + # 底部状态栏 + bottom_status = tk.Frame(main_frame, bg=self.colors['bg_light'], height=22) + bottom_status.pack(fill=tk.X, pady=(5, 0)) + bottom_status.pack_propagate(False) + + self.status_text = tk.Label(bottom_status, text="就绪", + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light']) + self.status_text.pack(side=tk.LEFT, padx=10) + + # 调试模式快捷键 + self.root.bind('', self._toggle_debug) + + # 绑定悬停效果 + self.bind_hover_effects() + + def bind_hover_effects(self): + """绑定按钮悬停效果""" + buttons = [self.btn_root, self.btn_push, self.btn_install_all, + self.btn_language, self.btn_timezone, self.btn_settings, + self.btn_reboot, self.btn_clear, self.btn_exit] + + for btn in buttons: + original_bg = btn.cget('bg') + def on_enter(e, btn=btn, bg=original_bg): + btn.config(bg=self.lighten_color(bg)) + def on_leave(e, btn=btn, bg=original_bg): + btn.config(bg=bg) + btn.bind('', on_enter) + btn.bind('', on_leave) + + def lighten_color(self, color): + """调亮颜色""" + if color == self.colors['accent']: + return self.colors['accent_hover'] + elif color == self.colors['warning']: + return '#feca57' + elif color == self.colors['info']: + return '#0984e3' + elif color == self.colors['error']: + return '#e17055' + elif color == self.colors['success']: + return '#00a884' + return color + + def center_window(self): + """将窗口居中显示在屏幕上""" + self.root.update_idletasks() + screen_w = self.root.winfo_screenwidth() + screen_h = self.root.winfo_screenheight() + win_w = self.root.winfo_reqwidth() + win_h = self.root.winfo_reqheight() + x = (screen_w - win_w) // 2 + y = (screen_h - win_h) // 2 + self.root.geometry(f"+{x}+{y}") + + def run_on_ui_thread(self, func, *args, **kwargs): + """将函数调度到主线程执行,确保线程安全""" + self.root.after(0, func, *args, **kwargs) + + def _log_impl(self, message, level="INFO"): + """日志写入的实际实现(必须在主线程调用)""" + timestamp = datetime.now().strftime("%H:%M:%S") + log_entry = f"[{timestamp}] [{level}] {message}\n" + self.log_text.insert(tk.END, log_entry, level) + self.log_text.see(tk.END) + + def log(self, message, level="INFO"): + """添加日志(线程安全)""" + self.run_on_ui_thread(self._log_impl, message, level) + + def clear_log(self): + """清空日志""" + self.log_text.delete(1.0, tk.END) + self.log("日志已清空", "INFO") + + def _show_progress_impl(self, show=True, is_push=False): + """显示/隐藏进度条的实际实现(必须在主线程调用)""" + if is_push: + if show: + self.push_progress_label.pack() + self.push_progress.pack(fill=tk.X, pady=(2, 0)) + self.push_progress['value'] = 0 + else: + self.push_progress_label.pack_forget() + self.push_progress.pack_forget() + else: + if show: + self.progress_label.pack() + self.progress.pack(fill=tk.X, pady=(2, 0)) + self.progress['value'] = 0 + else: + self.progress_label.pack_forget() + self.progress.pack_forget() + + def show_progress(self, show=True, is_push=False): + """显示/隐藏进度条(线程安全)""" + self.run_on_ui_thread(self._show_progress_impl, show, is_push) + + def _update_progress_impl(self, value, max_value=100, label="", is_push=False): + """更新进度条的实际实现(必须在主线程调用)""" + if is_push: + percent = (value / max_value) * 100 + self.push_progress['value'] = percent + self.push_progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)") + else: + percent = (value / max_value) * 100 + self.progress['value'] = percent + self.progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)") + self.root.update_idletasks() + + def update_progress(self, value, max_value=100, label="", is_push=False): + """更新进度条(线程安全)""" + self.run_on_ui_thread(self._update_progress_impl, value, max_value, label, is_push) + + def update_device_status(self, connected, vin=None, authorized=False): + """更新设备状态显示(线程安全:立即设状态变量,UI走主线程)""" + self.device_connected = connected + if vin is not None: + self.vin = vin + self.run_on_ui_thread(self._update_device_status_impl, connected, vin, authorized) + + def _update_device_status_impl(self, connected, vin, authorized): + """设备状态UI更新的实际实现(必须在主线程调用)""" + if connected: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['success']) + self.device_status_label.config(text="已连接", fg=self.colors['success']) + if vin: + self.vin_label.config(text=vin, fg=self.colors['success']) + if authorized: + self.auth_label.config(text="已授权", fg=self.colors['success']) + else: + self.auth_label.config(text="未授权", fg=self.colors['error']) + else: + self.vin_label.config(text="未获取", fg=self.colors['error']) + self.auth_label.config(text="未验证", fg=self.colors['error']) + else: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['error']) + self.device_status_label.config(text="未连接", fg=self.colors['error']) + self.vin_label.config(text="未获取", fg=self.colors['error']) + self.auth_label.config(text="未验证", fg=self.colors['error']) + + def check_device_connection(self): + """检查设备是否连接""" + if self.debug_mode: + return True + if not self.device_connected: + messagebox.showwarning("设备未连接", "请先连接设备并点击「检查」按钮刷新状态!") + return False + return True + + def start_device_monitor(self): + """启动设备状态监控(每5秒检查一次)""" + def monitor(): + while True: + try: + result = subprocess.run(f'{self.adb} -d devices', shell=True, capture_output=True, text=True) + lines = result.stdout.strip().split('\n') + devices = [line for line in lines[1:] if line.strip() and 'device' in line and 'offline' not in line] + + if devices and not self.device_connected and not self._refreshing: + # 设备新连接,刷新状态 + self.refresh_device_status() + elif not devices and self.device_connected: + # 设备断开连接 + self.update_device_status(False) + self.log("设备已断开连接", "WARNING") + + time.sleep(5) + except: + time.sleep(5) + + threading.Thread(target=monitor, daemon=True).start() + + def get_root_permission(self): + """获取 root 权限,首次获取需重启""" + if not self.check_device_connection(): + return + + def get_root(): + self.show_progress(True, is_push=False) + + # 执行 adb -d root + ok_root, out_root = self.run_adb_command('adb -d root') + if not ok_root: + self.log("获取 root 失败", "ERROR") + self.show_progress(False, is_push=False) + return + + time.sleep(1) + + # 执行 adb -d remount(需同时捕获 stdout 和 stderr) + remount_result = subprocess.run(f'{self.adb} -d remount', shell=True, + capture_output=True, text=True) + if remount_result.returncode != 0: + self.log("获取权限失败", "ERROR") + self.show_progress(False, is_push=False) + return + + # 判断是否需要重启:首次 remount 返回 "Now reboot your device for settings to take effect" + combined_output = (remount_result.stdout + remount_result.stderr).lower() + if 'now reboot your device' in combined_output: + self.log("首次获取权限,需要重启设备...", "INFO") + ok, _ = self.run_adb_command('adb -d shell reboot') + if ok: + self.log("设备即将重启,重启后权限生效", "INFO") + self.update_device_status(False) + else: + self.log("重启失败", "ERROR") + else: + self.log("已获取权限", "SUCCESS") + + self.show_progress(False, is_push=False) + + threading.Thread(target=get_root, daemon=True).start() + + def check_package_extracted(self): + """检查语言包是否已解压""" + has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0 + has_priv = self.priv_apps_dir and self.priv_apps_dir.exists() and len(list(self.priv_apps_dir.glob("*.apk"))) > 0 + return has_app or has_priv + + def extract_package_silent(self): + """静默解压语言包(带进度)""" + if not self.package_file.exists(): + self.log(f"未找到资源包 ({self.package_file.name})", "ERROR") + return False + + try: + # 使用用户目录,无需管理员权限 + local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) + hidden_path = Path(local_appdata) / ".cache" / "system" / ".android" + hidden_path.mkdir(parents=True, exist_ok=True) + + self.temp_dir = hidden_path / "apps_cache_S05" + + # 如果已存在,先清理 + if self.temp_dir.exists(): + shutil.rmtree(self.temp_dir, ignore_errors=True) + time.sleep(0.5) + + self.temp_dir.mkdir(parents=True, exist_ok=True) + + # 设置隐藏属性(Windows) + if sys.platform == 'win32': + subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True) + subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True) + + + # 使用 7za 高速解压 + self.update_progress(0, 1, "资源加载中...") + result = subprocess.run( + [self.sz, 'x', str(self.package_file), f'-p{self.extract_password}', f'-o{self.temp_dir}', '-y'], + capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + if result.returncode != 0: + stderr = result.stderr.strip() + if stderr: + self.log(f"解压失败: {stderr[:200]}", "ERROR") + else: + self.log("解压失败,请检查密码是否正确", "ERROR") + return False + self.update_progress(1, 1, "资源加载完成") + + # 查找app和priv-app目录 + self.apps_dir = None + self.priv_apps_dir = None + + app_candidates = list(self.temp_dir.rglob("app")) or list(self.temp_dir.rglob("apps")) + if app_candidates: + self.apps_dir = app_candidates[0] + + priv_app_candidates = list(self.temp_dir.rglob("priv-app")) or list(self.temp_dir.rglob("priv-apps")) + if priv_app_candidates: + self.priv_apps_dir = priv_app_candidates[0] + + if not self.apps_dir and not self.priv_apps_dir: + self.log("未找到 app/priv-app 目录", "WARNING") + return False + + return True + + except Exception as e: + self.log(f"资源准备失败", "ERROR") + return False + + def check_environment(self): + """检查环境""" + try: + result = subprocess.run(f'{self.adb} version', shell=True, capture_output=True, text=True) + if result.returncode == 0: + self.refresh_device_status() + if not self.package_file.exists(): + self.log("未找到资源包文件", "WARNING") + else: + self._try_reuse_extracted() + else: + self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") + except FileNotFoundError: + self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") + + def _try_reuse_extracted(self): + """检查磁盘上是否已有解压好的资源,有则直接复用""" + local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) + cache_dir = Path(local_appdata) / ".cache" / "system" / ".android" / "apps_cache_S05" + if not cache_dir.exists(): + return + + app_candidates = list(cache_dir.rglob("app")) or list(cache_dir.rglob("apps")) + priv_candidates = list(cache_dir.rglob("priv-app")) or list(cache_dir.rglob("priv-apps")) + + has_app = False + has_priv = False + if app_candidates: + apks = list(app_candidates[0].glob("*.apk")) + has_app = len(apks) > 0 + if priv_candidates: + apks = list(priv_candidates[0].glob("*.apk")) + has_priv = len(apks) > 0 + + if has_app or has_priv: + if has_app: + self.apps_dir = app_candidates[0] + if has_priv: + self.priv_apps_dir = priv_candidates[0] + self.temp_dir = cache_dir + # self.log("已复用缓存的资源文件", "INFO") + + def refresh_device_status(self): + """刷新设备状态""" + # 防止并发刷新 + if self._refreshing: + return + self._refreshing = True + + def refresh(): + was_connected = self.device_connected + + # 检查设备连接 + result = subprocess.run(f'{self.adb} -d devices', shell=True, capture_output=True, text=True) + lines = result.stdout.strip().split('\n') + devices = [line for line in lines[1:] if line.strip() and 'device' in line and 'offline' not in line] + + if devices: + # 只在首次连接时打日志 + if not was_connected: + self.log("设备已连接", "SUCCESS") + + # 获取VIN — 兼容两种 key,过滤 Android null 返回值 + vin = '' + for key in ('ca_vin_info', 'VIN'): + vin_result = subprocess.run( + f'{self.adb} -d shell settings get system {key}', + shell=True, capture_output=True, text=True) + vin = vin_result.stdout.strip() + if vin and vin != 'null': + break + vin = '' + if vin: + self.log(f"当前车辆VIN: {vin}", "INFO") + + # 验证授权 + authorized = self.check_authorization(vin) + self.update_device_status(True, vin, authorized) + else: + self.log("无法获取VIN", "WARNING") + self.update_device_status(True, None, False) + else: + if was_connected: + self.log("设备未连接", "WARNING") + self.update_device_status(False) + + self._refreshing = False + + threading.Thread(target=refresh, daemon=True).start() + + def check_authorization(self, vin): + """检查授权""" + if self.debug_mode: + self.log("调试模式: 跳过授权验证", "WARNING") + return True + self.log("正在验证授权...", "INFO") + try: + url = f"{self.api_url}?vin={vin}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + if data.get('authorized') == True: + self.log("✅ 授权验证通过!", "SUCCESS") + if 'data' in data and 'vehicleName' in data['data']: + self.log(f"车辆名称: {data['data']['vehicleName']}", "INFO") + return True + else: + self.log(f"❌ 授权验证失败", "ERROR") + return False + + except Exception as e: + self.log(f"❌ 授权验证失败", "ERROR") + return False + + def fetch_package_password(self): + """从服务端获取资源包解压密码""" + if not self.vin: + self.log("请先连接adb!", "ERROR") + return False + + try: + pwd_api_url = "https://api.changan.softwindy.cn/api/authorizations/package-key" + url = f"{pwd_api_url}?vin={self.vin}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + if data.get('success') and 'data' in data and 'password' in data['data']: + self.extract_password = data['data']['password'] + return True + else: + self.log(f"数据准备失败: {data.get('message', '未知错误')}", "ERROR") + return False + + except Exception as e: + self.log(f"数据准备失败: {str(e)}", "ERROR") + return False + + def run_adb_command(self, command): + """执行 adb 命令,静默执行,仅返回结果""" + command = command.replace('adb', self.adb, 1) + if self.debug_mode: + self.log(f"CMD: {command}", "CMD") + try: + result = subprocess.run(command, shell=True, capture_output=True, text=True, encoding='utf-8') + if self.debug_mode: + out = result.stdout.strip() + err = result.stderr.strip() + if out: + self.log(f" -> {out[:300]}", "CMD") + if err: + self.log(f" !! {err[:300]}", "ERROR") + if result.returncode == 0: + return True, result.stdout.strip() + else: + return False, result.stderr.strip() + except Exception as e: + return False, str(e) + + def push_single_apk(self, apk_path, apk_name, target_type="app"): + """推送单个APK到系统分区,返回 (成功, 错误信息)""" + temp_apk_path = f"/data/local/tmp/{apk_name}.apk" + target_dir = f"/system/priv-app/{apk_name}" if target_type == "priv-app" else f"/system/app/{apk_name}" + target_apk_path = f"{target_dir}/{apk_name}.apk" + + ok, err = self.run_adb_command(f'adb -d push "{apk_path}" {temp_apk_path}') + if not ok: + return False, f"push失败: {err}" + + self.run_adb_command(f'adb -d shell mkdir -p {target_dir}') + ok, err = self.run_adb_command(f'adb -d shell cp {temp_apk_path} {target_apk_path}') + self.run_adb_command(f'adb -d shell rm -f {temp_apk_path}') + if not ok: + return False, f"cp失败: {err}" + + return True, "" + + def push_all_apks(self): + """推送APK到系统分区(支持app和priv-app)""" + if not self.check_device_connection(): + return + if not self.vin: + messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码") + return + + messagebox.showwarning("⚠️ 重要提示", + "刷入过程中请勿:\n" + " ● 重启车机\n" + " ● 退出本程序\n" + " ● 关闭电脑\n\n" + "否则可能导致车机系统损坏!") + + def do_push_all(): + if not self.check_authorization(self.vin): + self.run_on_ui_thread(lambda: messagebox.showerror("授权失败", "设备未授权")) + return + if not self.extract_password: + if not self.fetch_package_password(): + self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源准备失败!")) + return + if not self.check_package_extracted(): + self.show_progress(True, is_push=False) + if not self.extract_package_silent(): + self.show_progress(False, is_push=False) + self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源准备失败!")) + return + self.show_progress(False, is_push=False) + + if (not self.apps_dir or not self.apps_dir.exists()) and \ + (not self.priv_apps_dir or not self.priv_apps_dir.exists()): + self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源目录未找到")) + return + + self.show_progress(True, is_push=True) + self.run_adb_command('adb -d shell mkdir -p /data/local/tmp') + + all_apks = [] + if self.apps_dir and self.apps_dir.exists(): + for apk in self.apps_dir.glob("*.apk"): + all_apks.append((apk, "app")) + if self.priv_apps_dir and self.priv_apps_dir.exists(): + for apk in self.priv_apps_dir.glob("*.apk"): + all_apks.append((apk, "priv-app")) + + if not all_apks: + # 缓存可能过期,强制重新解压 + self.apps_dir = None + self.priv_apps_dir = None + self.temp_dir = None + if not self.fetch_package_password() or not self.extract_package_silent(): + self.log("未找到语言包文件", "WARNING") + self.show_progress(False, is_push=True) + return + # 重新收集 + all_apks = [] + if self.apps_dir and self.apps_dir.exists(): + for apk in self.apps_dir.glob("*.apk"): + all_apks.append((apk, "app")) + if self.priv_apps_dir and self.priv_apps_dir.exists(): + for apk in self.priv_apps_dir.glob("*.apk"): + all_apks.append((apk, "priv-app")) + if not all_apks: + self.log("未找到语言包文件", "WARNING") + self.show_progress(False, is_push=True) + return + + total = len(all_apks) + success_count = 0 + aborted = False + for i, (apk_path, apk_type) in enumerate(all_apks, 1): + apk_name = apk_path.stem + ok, err = self.push_single_apk(apk_path, apk_name, apk_type) + if ok: + success_count += 1 + else: + if "Read-only file system" in err: + self.log("请先点击「获取权限」获取权限后再试", "ERROR") + aborted = True + break + self.update_progress(i, total, "正在刷入...", is_push=True) + + self.update_progress(total, total, "刷入完成" if not aborted else "已终止", is_push=True) + + if success_count == total: + self.log(f"刷入完成,共 {total} 个语言包", "SUCCESS") + self.log("语言包已刷入完成,重启设备后生效,您可在适当时候重启", "WARNING") + elif success_count > 0: + self.log(f"部分刷入成功({success_count}/{total})", "WARNING") + if not aborted: + self.log("语言包已刷入完成,重启设备后生效,您可在适当时候重启", "WARNING") + + self.show_progress(False, is_push=True) + + threading.Thread(target=do_push_all, daemon=True).start() + + def install_all_apks(self): + """批量安装APK — 手动选择文件夹""" + if not self.check_device_connection(): + return + + apk_dir = filedialog.askdirectory(title="选择包含APK文件的文件夹") + if not apk_dir: + return + + apk_files = list(Path(apk_dir).glob("*.apk")) + if not apk_files: + messagebox.showerror("错误", "所选文件夹中没有APK文件!") + return + + result = messagebox.askyesno("确认安装", + f"找到 {len(apk_files)} 个APK文件\n\n是否开始批量安装?") + if not result: + return + + def install(): + self.show_progress(True, is_push=True) + total = len(apk_files) + self.log(f"开始批量安装 {total} 个APK...", "INFO") + self.run_adb_command('adb -d shell setprop vecentek.model 1') + + success_count = 0 + for i, apk_path in enumerate(apk_files, 1): + self.update_progress(i, total, "安装中...", is_push=True) + success, _ = self.run_adb_command(f'adb -d install -r "{apk_path}"') + if success: + success_count += 1 + + self.run_adb_command('adb -d shell setprop vecentek.model 0') + self.update_progress(total, total, "安装完成", is_push=True) + self.show_progress(False, is_push=True) + + if success_count == total: + self.log(f"安装完成:全部 {total} 个成功", "SUCCESS") + messagebox.showinfo("安装完成", f"成功安装 {total} 个APK!") + elif success_count > 0: + self.log(f"安装完成:{success_count}/{total} 成功", "WARNING") + messagebox.showwarning("部分成功", f"成功: {success_count}\n失败: {total - success_count}") + else: + self.log("安装失败", "ERROR") + messagebox.showerror("安装失败", "所有APK安装失败!") + + self.show_progress(False, is_push=True) + + threading.Thread(target=install, daemon=True).start() + + def install_single_apk(self): + """安装单个APK""" + # 检查设备连接 + if not self.check_device_connection(): + return + + file_path = filedialog.askopenfilename( + title="选择APK文件", + filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + ) + + if not file_path: + return + + def install(): + self.show_progress(True, is_push=True) + self.update_progress(50, 100, f"安装中", is_push=True) + self.run_adb_command('adb -d shell setprop vecentek.model 1') + success, _ = self.run_adb_command(f'adb -d install -r "{file_path}"') + self.run_adb_command('adb -d shell setprop vecentek.model 0') + self.update_progress(100, 100, f"完成", is_push=True) + if success: + self.log("✓ 安装成功", "SUCCESS") + else: + self.log("✗ 安装失败", "ERROR") + self.show_progress(False, is_push=True) + + threading.Thread(target=install, daemon=True).start() + + def open_language_settings(self): + """打开系统语言设置""" + if not self.check_device_connection(): + return + self.run_adb_command('adb -d shell am start -a android.settings.LOCALE_SETTINGS') + + def open_language_quick_set(self): + """打开快捷语言设置弹窗""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 创建弹窗 + popup = tk.Toplevel(self.root) + popup.title("快捷语言设置") + popup.geometry("520x320") + popup.configure(bg=self.colors['bg_dark']) + popup.resizable(False, False) + + # 居中显示 + popup.update_idletasks() + x = self.root.winfo_x() + (self.root.winfo_width() - 520) // 2 + y = self.root.winfo_y() + (self.root.winfo_height() - 320) // 2 + popup.geometry(f"+{x}+{y}") + popup.transient(self.root) + popup.grab_set() + + # 标题 + header = tk.Label(popup, text="选择目标语言", + font=('Microsoft YaHei', 13, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + header.pack(pady=(15, 10)) + + hint = tk.Label(popup, text="点击按钮即可将系统语言切换为对应语言,重启后生效", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + hint.pack(pady=(0, 12)) + + # 语言列表:(显示名, locale_code) + languages = [ + ("🇨🇳 中文", "zh-CN"), + ("英 English", "en-US"), + ("俄 Русский", "ru-RU"), + ("法 Français", "fr-FR"), + ("西 Español", "es-ES"), + ("葡 Português", "pt-BR"), + ("意 Italiano", "it-IT"), + ("阿 العربية", "ar-SA"), + ] + + # 创建按钮容器 + btn_frame = tk.Frame(popup, bg=self.colors['bg_dark']) + btn_frame.pack(pady=(0, 10)) + + btn_colors = [ + self.colors['accent'], self.colors['info'], + self.colors['success'], self.colors['warning'], + '#e17055', '#00b894', + '#6c5ce7', '#0984e3', + ] + + for i, (label, locale) in enumerate(languages): + row = i // 4 + col = i % 4 + + def make_cmd(loc=locale, lbl=label): + return lambda: self._quick_set_language(loc, lbl, popup) + + btn = tk.Button(btn_frame, text=label, + command=make_cmd(), + font=('Microsoft YaHei', 10), + fg='white', + bg=btn_colors[i], + relief=tk.FLAT, + cursor='hand2', + width=12, height=2) + btn.grid(row=row, column=col, padx=5, pady=5) + + # 底部分隔 + 打开系统设置入口 + sep = tk.Frame(popup, bg=self.colors['border'], height=1) + sep.pack(fill=tk.X, padx=20, pady=(8, 6)) + + sys_btn = tk.Button(popup, text="⚙️ 打开系统语言设置(手动选择)", + command=lambda: self._open_sys_and_close(popup), + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + sys_btn.pack(pady=(0, 10)) + + def _quick_set_language(self, locale_code, language_name, popup): + """执行快捷语言设置""" + popup.destroy() + + def do_set(): + self.log(f"正在设置系统语言为: {language_name} ({locale_code})", "INFO") + success, output = self.run_adb_command( + f'adb -d shell settings put system system_locales {locale_code}' + ) + + if success: + self.log(f"✓ 语言已设置为 {language_name}", "SUCCESS") + messagebox.showinfo( + "设置成功", + f"系统语言已设置为 {language_name}\n\n⚠️ 请重启设备使其生效。" + ) + else: + self.log(f"✗ 语言设置失败: {output}", "ERROR") + messagebox.showerror("设置失败", f"语言设置失败!\n\n{output}") + + threading.Thread(target=do_set, daemon=True).start() + + def _open_sys_and_close(self, popup): + """关闭弹窗并打开系统语言设置""" + popup.destroy() + self.open_language_settings() + + def open_timezone_settings(self): + """打开时区设置""" + if not self.check_device_connection(): + return + self.run_adb_command('adb -d shell am start -a android.settings.TIMEZONE_SETTINGS') + + def open_android_settings(self): + """打开安卓原生设置""" + if not self.check_device_connection(): + return + self.run_adb_command('adb -d shell am start -a android.settings.SETTINGS') + + def reboot_device(self): + """重启设备""" + if not self.check_device_connection(): + return + if messagebox.askyesno("确认重启", "确定要重启设备吗?"): + subprocess.Popen(f'{self.adb} -d shell reboot', shell=True, + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + self.log("设备正在重启...", "INFO") + self.update_device_status(False) + + def on_disable_upgrade(self): + """禁用系统升级""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 弹窗确认 + result = messagebox.askyesno( + "确认禁用升级", + "⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n" + "是否确定要禁用系统升级应用?\n\n" + "禁用命令:\n" + "adb -d shell pm disable-user --user 0 com.incall.apps.softmanager" + ) + + if not result: + self.log("已取消禁用升级操作", "INFO") + return + + def disable(): + self.show_progress(True, is_push=False) + success, output = self.run_adb_command( + 'adb -d shell pm disable-user --user 0 com.incall.apps.softmanager') + if success: + self.log("系统升级已禁用", "SUCCESS") + messagebox.showinfo("成功", "系统升级已成功禁用!") + else: + self.log("禁用系统升级失败", "ERROR") + messagebox.showerror("错误", f"禁用失败:{output}") + self.show_progress(False, is_push=False) + + threading.Thread(target=disable, daemon=True).start() + + def _on_vin_input_focus_in(self, event): + """输入框获得焦点时清除占位符""" + if self.vin_input.get() == "请输入VIN": + self.vin_input.delete(0, tk.END) + self.vin_input.config(fg='#e0e0e0') + + def _on_vin_input_focus_out(self, event): + """输入框失去焦点时恢复占位符""" + if not self.vin_input.get(): + self.vin_input.insert(0, "请输入VIN") + self.vin_input.config(fg='#636e72') + + def query_password_by_vin(self): + """通过VIN查询密码""" + vin = self.vin_input.get().strip() + if not vin: + messagebox.showwarning("提示", "请输入VIN码") + return + + def do_query(): + try: + api_url = "https://api.changan.softwindy.cn/api/authorizations/generate-password-by-vin" + url = f"{api_url}?vin={vin}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + def update_ui(): + if data.get('success'): + pwd = data.get('data', {}).get('devicePassword', '未知') + self.pwd_result_label.config( + text=f"密码: *#{pwd}#*", + fg=self.colors['success'] + ) + self.log(f"密码查询成功 VIN={vin} -> {pwd}", "SUCCESS") + else: + msg = data.get('message', '查询失败') + self.pwd_result_label.config( + text=f"失败: {msg}", + fg=self.colors['error'] + ) + self.log(f"密码查询失败: {msg}", "ERROR") + + self.run_on_ui_thread(update_ui) + + except Exception as e: + def update_ui_error(): + self.pwd_result_label.config( + text=f"请求失败", + fg=self.colors['error'] + ) + self.log(f"密码查询请求失败: {str(e)}", "ERROR") + self.run_on_ui_thread(update_ui_error) + + threading.Thread(target=do_query, daemon=True).start() + + def _toggle_debug(self, event=None): + """切换调试模式(隐藏入口,Ctrl+Shift+D)""" + if self.debug_mode: + self.debug_mode = False + self.log("调试模式已关闭", "WARNING") + self.status_text.config(text="就绪") + self.refresh_device_status() + return + + pwd = tk.simpledialog.askstring("调试模式", "请输入调试密码:", show='*', parent=self.root) + if pwd == "zxch5200": + self.debug_mode = True + self.update_device_status(True, "", True) + self.log("🔧 调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志", "WARNING") + self.status_text.config(text="🔧 调试模式") + elif pwd is not None: + messagebox.showwarning("错误", "密码错误") + + def install_apps(self): + """安装App — 支持单选或多选APK文件""" + if not self.check_device_connection(): + return + + file_paths = filedialog.askopenfilenames( + title="选择APK文件", + filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + ) + if not file_paths: + return + + count = len(file_paths) + result = messagebox.askyesno("确认安装", f"已选择 {count} 个APK文件\n\n是否开始安装?") + if not result: + return + + def install(): + self.show_progress(True, is_push=True) + self.log(f"开始安装 {count} 个APK...", "INFO") + self.run_adb_command('adb -d shell setprop vecentek.model 1') + + success_count = 0 + for i, file_path in enumerate(file_paths, 1): + apk_name = Path(file_path).stem + self.update_progress(i, count, f"安装中 ({apk_name})", is_push=True) + success, _ = self.run_adb_command(f'adb -d install -r "{file_path}"') + if success: + self.log(f"✓ {apk_name}.apk", "SUCCESS") + success_count += 1 + else: + self.log(f"✗ {apk_name}.apk", "ERROR") + + self.run_adb_command('adb -d shell setprop vecentek.model 0') + self.update_progress(count, count, "安装完成", is_push=True) + self.show_progress(False, is_push=True) + + if success_count == count: + self.log(f"安装完成:全部 {count} 个成功", "SUCCESS") + messagebox.showinfo("安装完成", f"成功安装 {count} 个APK!") + elif success_count > 0: + self.log(f"安装完成:{success_count}/{count} 成功", "WARNING") + messagebox.showwarning("部分成功", f"成功: {success_count}\n失败: {count - success_count}") + else: + self.log("安装失败", "ERROR") + messagebox.showerror("安装失败", "所有APK安装失败!") + + threading.Thread(target=install, daemon=True).start() + + def run(self): + """运行程序""" + self.root.mainloop() + +def main(): + """主函数""" + if sys.version_info < (3, 6): + print("错误:需要Python 3.6或更高版本") + sys.exit(1) + + try: + app = ADKAPKGUI() + app.run() + except Exception as e: + print(f"启动失败: {e}") + import traceback + traceback.print_exc() + messagebox.showerror("错误", f"程序启动失败: {e}") + +if __name__ == "__main__": + main() diff --git a/X5plusTool.py b/X5plusTool.py new file mode 100644 index 0000000..4f07849 --- /dev/null +++ b/X5plusTool.py @@ -0,0 +1,1243 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import subprocess +import json +import threading +import tkinter as tk +from tkinter import ttk, scrolledtext, filedialog, messagebox +from pathlib import Path +from urllib.request import urlopen, Request +from urllib.error import URLError, HTTPError +from datetime import datetime +import zipfile +try: + import pyzipper +except ImportError: + pyzipper = None +import shutil +import time + +class ADKAPKGUI: + def __init__(self): + self.root = tk.Tk() + self.root.title("长安语言安装工具") + self.root.geometry("650x640") + self.root.resizable(True, True) + + # 设置颜色主题 + self.colors = { + 'bg_dark': '#1e1e2e', + 'bg_light': '#2a2a3e', + 'accent': '#6c5ce7', + 'accent_hover': '#5b4bc4', + 'success': '#00b894', + 'error': '#d63031', + 'warning': '#fdcb6e', + 'info': '#0984e3', + 'text': '#dfe6e9', + 'text_secondary': '#b2bec3', + 'border': '#3d3d5e' + } + + # 从 exe/py 所在目录查找资源文件 + self.base_dir = Path(sys.executable).parent if getattr(sys, 'frozen', False) else Path(__file__).parent + if getattr(sys, 'frozen', False): + self.adb = str(Path(sys._MEIPASS) / 'adb.exe') + self.sz = str(Path(sys._MEIPASS) / '7za.exe') + else: + self.adb = 'adb' + self.sz = str(self.base_dir / '7za.exe') + self.package_file = self.base_dir / "package.bin" + self.extract_password = None + self.apps_dir = None + self.priv_apps_dir = None + self.temp_dir = None + self.api_url = "https://api.changan.softwindy.cn/api/authorizations/auth-check" + self.vin = None + self.device_connected = False + self._refreshing = False # 防止并发刷新 + self.debug_mode = False # 调试模式 + + # 设置样式 + self.setup_styles() + self.setup_ui() + self.center_window() + + # 检查环境 + self.check_environment() + + # 启动设备状态监控 + self.start_device_monitor() + + def setup_styles(self): + """设置自定义样式""" + style = ttk.Style() + style.theme_use('clam') + + # 配置主颜色 + style.configure('TFrame', background=self.colors['bg_dark']) + style.configure('TLabel', background=self.colors['bg_dark'], foreground=self.colors['text']) + style.configure('TLabelframe', background=self.colors['bg_dark'], foreground=self.colors['text']) + style.configure('TLabelframe.Label', background=self.colors['bg_dark'], foreground=self.colors['accent']) + + # 配置进度条 + style.configure('TProgressbar', + background=self.colors['accent'], + troughcolor=self.colors['bg_light'], + borderwidth=0) + + def setup_ui(self): + """设置UI界面""" + # 配置根窗口 + self.root.configure(bg=self.colors['bg_dark']) + + # 创建主框架 + main_frame = tk.Frame(self.root, bg=self.colors['bg_dark']) + main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 顶部标题栏 + title_frame = tk.Frame(main_frame, bg=self.colors['bg_dark'], height=65) + title_frame.pack(fill=tk.X, pady=(0, 10)) + title_frame.pack_propagate(False) + + # 标题 + title_label = tk.Label(title_frame, + text="🚀 适用于X5plus多语言安装", + font=('Microsoft YaHei', 18, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + title_label.pack() + + subtitle_label = tk.Label(title_frame, + text="宜宾科宜科技有限公司 - 智能设备管理平台", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + subtitle_label.pack() + + # 工程模式提示 + eng_tips_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + eng_tips_frame.pack(fill=tk.X, pady=(0, 8), padx=5) + + self.eng_tip_label = tk.Label(eng_tips_frame, text="", + font=('Microsoft YaHei', 9), + fg=self.colors['info'], + bg=self.colors['bg_light'], + wraplength=620, + justify=tk.LEFT) + self.eng_tip_label.pack(padx=10, pady=8) + self._update_fac_pwd_display() + + # 按钮区域(两排,每排5个) + button_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + button_frame.pack(fill=tk.X, pady=(0, 10), padx=5) + + # 按钮样式参数 + btn_params = { + 'font': ('Microsoft YaHei', 9), + 'fg': 'white', + 'relief': tk.FLAT, + 'cursor': 'hand2', + 'height': 1, + 'width': 14 + } + + # 第一排按钮 + row1_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + row1_frame.pack(pady=(8, 4)) + + self.btn_push = tk.Button(row1_frame, text="📦 刷入语言包", + command=self.push_all_apks, + bg=self.colors['accent'], + **btn_params) + self.btn_push.pack(side=tk.LEFT, padx=4) + + self.btn_install_all = tk.Button(row1_frame, text="📱 安装App", + command=self.install_apps, + bg=self.colors['accent'], + **btn_params) + self.btn_install_all.pack(side=tk.LEFT, padx=4) + + self.btn_language = tk.Button(row1_frame, text="🌐 语言设置", + command=self.open_language_quick_set, + bg=self.colors['accent'], + **btn_params) + self.btn_language.pack(side=tk.LEFT, padx=4) + + # 第二排按钮 + row2_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + row2_frame.pack(pady=(4, 8)) + + self.btn_timezone = tk.Button(row2_frame, text="⏰ 时区设置", + command=self.open_timezone_settings, + bg=self.colors['accent'], + **btn_params) + self.btn_timezone.pack(side=tk.LEFT, padx=4) + + self.btn_settings = tk.Button(row2_frame, text="⚙️ 安卓设置", + command=self.open_android_settings, + bg=self.colors['accent'], + **btn_params) + self.btn_settings.pack(side=tk.LEFT, padx=4) + + self.btn_reboot = tk.Button(row2_frame, text="🔄 重启设备", + command=self.reboot_device, + bg=self.colors['warning'], + **btn_params) + self.btn_reboot.pack(side=tk.LEFT, padx=4) + + self.btn_exit = tk.Button(row2_frame, text="❌ 禁用升级", + command=self.on_disable_upgrade, + bg=self.colors['error'], + **btn_params) + self.btn_exit.pack(side=tk.LEFT, padx=4) + + # 设备状态栏(横条) + status_bar_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + status_bar_frame.pack(fill=tk.X, pady=(0, 5)) + + # 状态指示器 + status_indicator_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + status_indicator_frame.pack(side=tk.LEFT, padx=10, pady=5) + + self.status_indicator = tk.Canvas(status_indicator_frame, width=10, height=10, + bg=self.colors['bg_light'], highlightthickness=0) + self.status_indicator.pack(side=tk.LEFT) + self.status_dot = self.status_indicator.create_oval(2, 2, 8, 8, fill='#636e72') + + tk.Label(status_indicator_frame, text="设备:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT, padx=(5, 3)) + + self.device_status_label = tk.Label(status_indicator_frame, text="未检测", + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + anchor='w', width=4) + self.device_status_label.pack(side=tk.LEFT) + + # VIN信息 + vin_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + vin_frame.pack(side=tk.LEFT, padx=20, pady=5) + tk.Label(vin_frame, text="VIN码:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT) + self.vin_label = tk.Label(vin_frame, text="未获取", + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + anchor='w', width=17) + self.vin_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 授权状态 + auth_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + auth_frame.pack(side=tk.LEFT, padx=20, pady=5) + tk.Label(auth_frame, text="授权:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT) + self.auth_label = tk.Label(auth_frame, text="未验证", + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + anchor='w', width=4) + self.auth_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 刷新按钮 + refresh_btn = tk.Button(status_bar_frame, text="🔄 检查", + command=self.refresh_device_status, + font=('Microsoft YaHei', 8), + fg=self.colors['accent'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + refresh_btn.pack(side=tk.RIGHT, padx=10, pady=5) + + # 提示信息区域(设备状态下方) + tips_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + tips_frame.pack(fill=tk.X, pady=(5, 5), padx=5) + + tips = [ + "1. 安装语言过程中请保持车辆和电脑的电量充足,不可中途停止。", + "2. 获取权限以后,车辆自动重启以后再进入语言刷入。", + "3. 部分语言需要重启后生效,可以一切工作完成以后再重启。", + ] + + for i, tip in enumerate(tips): + tip_row = tk.Frame(tips_frame, bg=self.colors['bg_light']) + tip_row.pack(fill=tk.X, padx=10, pady=(5 if i == 0 else 0, 5 if i == len(tips) - 1 else 0)) + tk.Label(tip_row, text=tip, + font=('Microsoft YaHei', 9), + fg=self.colors['warning'], + bg=self.colors['bg_light'], + wraplength=600, + justify=tk.LEFT).pack(side=tk.LEFT) + + # 解压进度条框架 + progress_frame = tk.Frame(main_frame, bg=self.colors['bg_dark']) + progress_frame.pack(fill=tk.X, pady=(5, 5)) + + self.progress_label = tk.Label(progress_frame, text="", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + self.progress_label.pack() + + self.progress = ttk.Progressbar(progress_frame, mode='determinate', style='TProgressbar') + self.progress.pack(fill=tk.X, pady=(2, 0)) + + # 推送进度条 + self.push_progress_label = tk.Label(progress_frame, text="", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + + self.push_progress = ttk.Progressbar(progress_frame, mode='determinate', style='TProgressbar') + + # 日志区域(下方) + log_card = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + log_card.pack(fill=tk.BOTH, expand=True, pady=(5, 0)) + + # 日志标题栏 + log_title_frame = tk.Frame(log_card, bg=self.colors['bg_dark'], height=30) + log_title_frame.pack(fill=tk.X) + log_title_frame.pack_propagate(False) + + tk.Label(log_title_frame, text="📋 运行日志", + font=('Microsoft YaHei', 10, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']).pack(side=tk.LEFT, padx=10) + + self.btn_clear = tk.Button(log_title_frame, text="🗑 清空日志", + command=self.clear_log, + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark'], + relief=tk.FLAT, + cursor='hand2') + self.btn_clear.pack(side=tk.RIGHT, padx=10) + + # 日志文本框 + text_frame = tk.Frame(log_card, bg=self.colors['bg_light']) + text_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + self.log_text = scrolledtext.ScrolledText(text_frame, + height=12, + wrap=tk.WORD, + font=('Consolas', 9), + bg='#2d2d3d', + fg='#e0e0e0', + insertbackground='white', + relief=tk.FLAT, + borderwidth=0) + self.log_text.pack(fill=tk.BOTH, expand=True) + + # 配置日志颜色标签 + self.log_text.tag_config('INFO', foreground='#74b9ff') + self.log_text.tag_config('SUCCESS', foreground='#55efc4') + self.log_text.tag_config('ERROR', foreground='#ff7675') + self.log_text.tag_config('WARNING', foreground='#ffeaa7') + self.log_text.tag_config('CMD', foreground='#a29bfe') + + # 底部状态栏 + bottom_status = tk.Frame(main_frame, bg=self.colors['bg_light'], height=22) + bottom_status.pack(fill=tk.X, pady=(5, 0)) + bottom_status.pack_propagate(False) + + self.status_text = tk.Label(bottom_status, text="就绪", + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light']) + self.status_text.pack(side=tk.LEFT, padx=10) + + # 调试模式快捷键 + self.root.bind('', self._toggle_debug) + + # 绑定悬停效果 + self.bind_hover_effects() + + def bind_hover_effects(self): + """绑定按钮悬停效果""" + buttons = [self.btn_push, self.btn_install_all, + self.btn_language, self.btn_timezone, self.btn_settings, + self.btn_reboot, self.btn_clear, self.btn_exit] + + for btn in buttons: + original_bg = btn.cget('bg') + def on_enter(e, btn=btn, bg=original_bg): + btn.config(bg=self.lighten_color(bg)) + def on_leave(e, btn=btn, bg=original_bg): + btn.config(bg=bg) + btn.bind('', on_enter) + btn.bind('', on_leave) + + def lighten_color(self, color): + """调亮颜色""" + if color == self.colors['accent']: + return self.colors['accent_hover'] + elif color == self.colors['warning']: + return '#feca57' + elif color == self.colors['info']: + return '#0984e3' + elif color == self.colors['error']: + return '#e17055' + elif color == self.colors['success']: + return '#00a884' + return color + + def center_window(self): + """将窗口居中显示在屏幕上""" + self.root.update_idletasks() + screen_w = self.root.winfo_screenwidth() + screen_h = self.root.winfo_screenheight() + win_w = self.root.winfo_reqwidth() + win_h = self.root.winfo_reqheight() + x = (screen_w - win_w) // 2 + y = (screen_h - win_h) // 2 + self.root.geometry(f"+{x}+{y}") + + def _calc_fac_pwd(self): + """计算当前工程密码:分钟十位数 + 0 + 小时(如 14:58 → 5014)""" + now = datetime.now() + hour = now.hour # 如 14 + minute_tens = now.minute // 10 # 如 58 → 5 + return f"{minute_tens}0{hour}" + + def _update_fac_pwd_display(self): + """更新工程密码显示,每30秒刷新一次""" + pwd = self._calc_fac_pwd() + text = ( + f"工程模式:拨号 *#*#888 ," + f"工程密码:{pwd}," + f"调试密码:3821" + ) + self.eng_tip_label.config(text=text) + # 每30秒刷新(密码每分钟可能变化) + self.root.after(30000, self._update_fac_pwd_display) + + def run_on_ui_thread(self, func, *args, **kwargs): + """将函数调度到主线程执行,确保线程安全""" + self.root.after(0, func, *args, **kwargs) + + def _log_impl(self, message, level="INFO"): + """日志写入的实际实现(必须在主线程调用)""" + timestamp = datetime.now().strftime("%H:%M:%S") + log_entry = f"[{timestamp}] [{level}] {message}\n" + self.log_text.insert(tk.END, log_entry, level) + self.log_text.see(tk.END) + + def log(self, message, level="INFO"): + """添加日志(线程安全)""" + self.run_on_ui_thread(self._log_impl, message, level) + + def clear_log(self): + """清空日志""" + self.log_text.delete(1.0, tk.END) + self.log("日志已清空", "INFO") + + def _show_progress_impl(self, show=True, is_push=False): + """显示/隐藏进度条的实际实现(必须在主线程调用)""" + if is_push: + if show: + self.push_progress_label.pack() + self.push_progress.pack(fill=tk.X, pady=(2, 0)) + self.push_progress['value'] = 0 + else: + self.push_progress_label.pack_forget() + self.push_progress.pack_forget() + else: + if show: + self.progress_label.pack() + self.progress.pack(fill=tk.X, pady=(2, 0)) + self.progress['value'] = 0 + else: + self.progress_label.pack_forget() + self.progress.pack_forget() + + def show_progress(self, show=True, is_push=False): + """显示/隐藏进度条(线程安全)""" + self.run_on_ui_thread(self._show_progress_impl, show, is_push) + + def _update_progress_impl(self, value, max_value=100, label="", is_push=False): + """更新进度条的实际实现(必须在主线程调用)""" + if is_push: + percent = (value / max_value) * 100 + self.push_progress['value'] = percent + self.push_progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)") + else: + percent = (value / max_value) * 100 + self.progress['value'] = percent + self.progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)") + self.root.update_idletasks() + + def update_progress(self, value, max_value=100, label="", is_push=False): + """更新进度条(线程安全)""" + self.run_on_ui_thread(self._update_progress_impl, value, max_value, label, is_push) + + def update_device_status(self, connected, vin=None, authorized=False): + """更新设备状态显示(线程安全:立即设状态变量,UI走主线程)""" + self.device_connected = connected + if vin is not None: + self.vin = vin + self.run_on_ui_thread(self._update_device_status_impl, connected, vin, authorized) + + def _update_device_status_impl(self, connected, vin, authorized): + """设备状态UI更新的实际实现(必须在主线程调用)""" + if connected: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['success']) + self.device_status_label.config(text="已连接", fg=self.colors['success']) + if vin: + self.vin_label.config(text=vin, fg=self.colors['success']) + if authorized: + self.auth_label.config(text="已授权", fg=self.colors['success']) + else: + self.auth_label.config(text="未授权", fg=self.colors['error']) + else: + self.vin_label.config(text="未获取", fg=self.colors['error']) + self.auth_label.config(text="未验证", fg=self.colors['error']) + else: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['error']) + self.device_status_label.config(text="未连接", fg=self.colors['error']) + self.vin_label.config(text="未获取", fg=self.colors['error']) + self.auth_label.config(text="未验证", fg=self.colors['error']) + + def check_device_connection(self): + """检查设备是否连接""" + if self.debug_mode: + return True + if not self.device_connected: + messagebox.showwarning("设备未连接", "请先连接设备并点击「检查」按钮刷新状态!") + return False + return True + + def start_device_monitor(self): + """启动设备状态监控(每5秒检查一次)""" + def monitor(): + while True: + try: + result = subprocess.run(f'{self.adb} -d devices', shell=True, capture_output=True, text=True) + lines = result.stdout.strip().split('\n') + devices = [line for line in lines[1:] if line.strip() and 'device' in line and 'offline' not in line] + + if devices and not self.device_connected and not self._refreshing: + # 设备新连接,刷新状态 + self.refresh_device_status() + elif not devices and self.device_connected: + # 设备断开连接 + self.update_device_status(False) + self.log("设备已断开连接", "WARNING") + + time.sleep(5) + except: + time.sleep(5) + + threading.Thread(target=monitor, daemon=True).start() + + def check_package_extracted(self): + """检查语言包是否已解压""" + has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0 + has_priv = self.priv_apps_dir and self.priv_apps_dir.exists() and len(list(self.priv_apps_dir.glob("*.apk"))) > 0 + return has_app or has_priv + + def extract_package_silent(self): + """静默解压语言包(带进度)""" + if not self.package_file.exists(): + self.log(f"未找到资源包 ({self.package_file.name})", "ERROR") + return False + + try: + # 使用用户目录,无需管理员权限 + local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) + hidden_path = Path(local_appdata) / ".cache" / "system" / ".android" + hidden_path.mkdir(parents=True, exist_ok=True) + + self.temp_dir = hidden_path / "apps_cache_X5plus" + + # 如果已存在,先清理 + if self.temp_dir.exists(): + shutil.rmtree(self.temp_dir, ignore_errors=True) + time.sleep(0.5) + + self.temp_dir.mkdir(parents=True, exist_ok=True) + + # 设置隐藏属性(Windows) + if sys.platform == 'win32': + subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True) + subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True) + + + # 使用 7za 高速解压 + self.update_progress(0, 1, "资源加载中...") + result = subprocess.run( + [self.sz, 'x', str(self.package_file), f'-p{self.extract_password}', f'-o{self.temp_dir}', '-y'], + capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + if result.returncode != 0: + stderr = result.stderr.strip() + if stderr: + self.log(f"解压失败: {stderr[:200]}", "ERROR") + else: + self.log("解压失败,请检查密码是否正确", "ERROR") + return False + self.update_progress(1, 1, "资源加载完成") + + # 查找app和priv-app目录 + self.apps_dir = None + self.priv_apps_dir = None + + app_candidates = list(self.temp_dir.rglob("app")) or list(self.temp_dir.rglob("apps")) + if app_candidates: + self.apps_dir = app_candidates[0] + + priv_app_candidates = list(self.temp_dir.rglob("priv-app")) or list(self.temp_dir.rglob("priv-apps")) + if priv_app_candidates: + self.priv_apps_dir = priv_app_candidates[0] + + if not self.apps_dir and not self.priv_apps_dir: + self.log("未找到 app/priv-app 目录", "WARNING") + return False + + return True + + except Exception as e: + self.log(f"资源准备失败", "ERROR") + return False + + def check_environment(self): + """检查环境""" + try: + result = subprocess.run(f'{self.adb} version', shell=True, capture_output=True, text=True) + if result.returncode == 0: + self.refresh_device_status() + if not self.package_file.exists(): + self.log("未找到资源包文件", "WARNING") + else: + self._try_reuse_extracted() + else: + self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") + except FileNotFoundError: + self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") + + def _try_reuse_extracted(self): + """检查磁盘上是否已有解压好的资源,有则直接复用""" + local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) + cache_dir = Path(local_appdata) / ".cache" / "system" / ".android" / "apps_cache_X5plus" + if not cache_dir.exists(): + return + + app_candidates = list(cache_dir.rglob("app")) or list(cache_dir.rglob("apps")) + priv_candidates = list(cache_dir.rglob("priv-app")) or list(cache_dir.rglob("priv-apps")) + + has_app = False + has_priv = False + if app_candidates: + apks = list(app_candidates[0].glob("*.apk")) + has_app = len(apks) > 0 + if priv_candidates: + apks = list(priv_candidates[0].glob("*.apk")) + has_priv = len(apks) > 0 + + if has_app or has_priv: + if has_app: + self.apps_dir = app_candidates[0] + if has_priv: + self.priv_apps_dir = priv_candidates[0] + self.temp_dir = cache_dir + # self.log("已复用缓存的资源文件", "INFO") + + def refresh_device_status(self): + """刷新设备状态""" + # 防止并发刷新 + if self._refreshing: + return + self._refreshing = True + + def refresh(): + was_connected = self.device_connected + + # 检查设备连接 + result = subprocess.run(f'{self.adb} -d devices', shell=True, capture_output=True, text=True) + lines = result.stdout.strip().split('\n') + devices = [line for line in lines[1:] if line.strip() and 'device' in line and 'offline' not in line] + + if devices: + # 只在首次连接时打日志 + if not was_connected: + self.log("设备已连接", "SUCCESS") + + # 获取VIN — 兼容两种 key,过滤 Android null 返回值 + vin = '' + for key in ('ca_vin_info', 'VIN'): + vin_result = subprocess.run( + f'{self.adb} -d shell settings get system {key}', + shell=True, capture_output=True, text=True) + vin = vin_result.stdout.strip() + if vin and vin != 'null': + break + vin = '' + if vin: + self.log(f"当前车辆VIN: {vin}", "INFO") + + # 验证授权 + authorized = self.check_authorization(vin) + self.update_device_status(True, vin, authorized) + else: + self.log("无法获取VIN", "WARNING") + self.update_device_status(True, None, False) + else: + if was_connected: + self.log("设备未连接", "WARNING") + self.update_device_status(False) + + self._refreshing = False + + threading.Thread(target=refresh, daemon=True).start() + + def check_authorization(self, vin): + """检查授权""" + if self.debug_mode: + self.log("调试模式: 跳过授权验证", "WARNING") + return True + self.log("正在验证授权...", "INFO") + try: + url = f"{self.api_url}?vin={vin}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + if data.get('authorized') == True: + self.log("✅ 授权验证通过!", "SUCCESS") + if 'data' in data and 'vehicleName' in data['data']: + self.log(f"车辆名称: {data['data']['vehicleName']}", "INFO") + return True + else: + self.log(f"❌ 授权验证失败", "ERROR") + return False + + except Exception as e: + self.log(f"❌ 授权验证失败", "ERROR") + return False + + def fetch_package_password(self): + """从服务端获取资源包解压密码""" + if not self.vin: + self.log("请先连接adb!", "ERROR") + return False + + try: + pwd_api_url = "https://api.changan.softwindy.cn/api/authorizations/package-key" + url = f"{pwd_api_url}?vin={self.vin}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + if data.get('success') and 'data' in data and 'password' in data['data']: + self.extract_password = data['data']['password'] + return True + else: + self.log(f"数据准备失败: {data.get('message', '未知错误')}", "ERROR") + return False + + except Exception as e: + self.log(f"数据准备失败: {str(e)}", "ERROR") + return False + + def run_adb_command(self, command): + """执行 adb 命令,静默执行,仅返回结果""" + command = command.replace('adb', self.adb, 1) + if self.debug_mode: + self.log(f"CMD: {command}", "CMD") + try: + result = subprocess.run(command, shell=True, capture_output=True, text=True, encoding='utf-8') + if self.debug_mode: + out = result.stdout.strip() + err = result.stderr.strip() + if out: + self.log(f" -> {out[:300]}", "CMD") + if err: + self.log(f" !! {err[:300]}", "ERROR") + if result.returncode == 0: + return True, result.stdout.strip() + else: + return False, result.stderr.strip() + except Exception as e: + return False, str(e) + + def push_single_apk(self, apk_path, apk_name, target_type="app"): + """推送单个APK到系统分区,返回 (成功, 错误信息)""" + temp_apk_path = f"/data/local/tmp/{apk_name}.apk" + target_dir = f"/system/priv-app/{apk_name}" if target_type == "priv-app" else f"/system/app/{apk_name}" + target_apk_path = f"{target_dir}/{apk_name}.apk" + + ok, err = self.run_adb_command(f'adb -d push "{apk_path}" {temp_apk_path}') + if not ok: + return False, f"push失败: {err}" + + self.run_adb_command(f'adb -d shell mkdir -p {target_dir}') + ok, err = self.run_adb_command(f'adb -d shell cp {temp_apk_path} {target_apk_path}') + self.run_adb_command(f'adb -d shell rm -f {temp_apk_path}') + if not ok: + return False, f"cp失败: {err}" + + return True, "" + + def push_all_apks(self): + """推送APK到系统分区(支持app和priv-app)""" + if not self.check_device_connection(): + return + if not self.vin: + messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码") + return + + messagebox.showwarning("⚠️ 重要提示", + "刷入过程中请勿:\n" + " ● 重启车机\n" + " ● 退出本程序\n" + " ● 关闭电脑\n\n" + "否则可能导致车机系统损坏!") + + def do_push_all(): + if not self.check_authorization(self.vin): + self.run_on_ui_thread(lambda: messagebox.showerror("授权失败", "设备未授权")) + return + if not self.extract_password: + if not self.fetch_package_password(): + self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源准备失败!")) + return + if not self.check_package_extracted(): + self.show_progress(True, is_push=False) + if not self.extract_package_silent(): + self.show_progress(False, is_push=False) + self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源准备失败!")) + return + self.show_progress(False, is_push=False) + + if (not self.apps_dir or not self.apps_dir.exists()) and \ + (not self.priv_apps_dir or not self.priv_apps_dir.exists()): + self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源目录未找到")) + return + + self.show_progress(True, is_push=True) + self.run_adb_command('adb -d shell mkdir -p /data/local/tmp') + + all_apks = [] + if self.apps_dir and self.apps_dir.exists(): + for apk in self.apps_dir.glob("*.apk"): + all_apks.append((apk, "app")) + if self.priv_apps_dir and self.priv_apps_dir.exists(): + for apk in self.priv_apps_dir.glob("*.apk"): + all_apks.append((apk, "priv-app")) + + if not all_apks: + # 缓存可能过期,强制重新解压 + self.apps_dir = None + self.priv_apps_dir = None + self.temp_dir = None + if not self.fetch_package_password() or not self.extract_package_silent(): + self.log("未找到语言包文件", "WARNING") + self.show_progress(False, is_push=True) + return + # 重新收集 + all_apks = [] + if self.apps_dir and self.apps_dir.exists(): + for apk in self.apps_dir.glob("*.apk"): + all_apks.append((apk, "app")) + if self.priv_apps_dir and self.priv_apps_dir.exists(): + for apk in self.priv_apps_dir.glob("*.apk"): + all_apks.append((apk, "priv-app")) + if not all_apks: + self.log("未找到语言包文件", "WARNING") + self.show_progress(False, is_push=True) + return + + total = len(all_apks) + success_count = 0 + for i, (apk_path, apk_type) in enumerate(all_apks, 1): + apk_name = apk_path.stem + ok, err = self.push_single_apk(apk_path, apk_name, apk_type) + if ok: + success_count += 1 + self.update_progress(i, total, "正在刷入...", is_push=True) + + self.update_progress(total, total, "刷入完成", is_push=True) + + # 删除旧应用 + self.log("正在清理旧应用...", "INFO") + for pkg in [ + "/system/app/B561_OSWeather", + "/system/app/B561_WeChatSendCar", + "/system/app/B561_VoiceCamera", + "/system/app/B561_KugouCC", + "/system/app/AiQiYi", + ]: + self.run_adb_command(f'adb -d shell rm -r {pkg}') + + # 推送字体文件 + font_file = self.temp_dir / "FZLTHPro_GB18030.ttf" if self.temp_dir else None + if font_file and font_file.exists(): + self.log("正在推送字体文件...", "INFO") + self.run_adb_command(f'adb -d push "{font_file}" /data/local/tmp/FZLTHPro_GB18030.ttf') + self.run_adb_command('adb -d shell mkdir -p /system/fonts') + self.run_adb_command('adb -d shell cp /data/local/tmp/FZLTHPro_GB18030.ttf /system/fonts/FZLTHPro_GB18030.ttf') + self.run_adb_command('adb -d shell rm -f /data/local/tmp/FZLTHPro_GB18030.ttf') + + if success_count == total: + self.log(f"刷入完成,共 {total} 个语言包", "SUCCESS") + self.log("语言包已刷入完成,重启设备后生效,您可在适当时候重启", "WARNING") + elif success_count > 0: + self.log(f"部分刷入成功({success_count}/{total})", "WARNING") + self.log("语言包已刷入完成,重启设备后生效,您可在适当时候重启", "WARNING") + + self.show_progress(False, is_push=True) + + threading.Thread(target=do_push_all, daemon=True).start() + + def install_all_apks(self): + """批量安装APK — 手动选择文件夹""" + if not self.check_device_connection(): + return + + apk_dir = filedialog.askdirectory(title="选择包含APK文件的文件夹") + if not apk_dir: + return + + apk_files = list(Path(apk_dir).glob("*.apk")) + if not apk_files: + messagebox.showerror("错误", "所选文件夹中没有APK文件!") + return + + result = messagebox.askyesno("确认安装", + f"找到 {len(apk_files)} 个APK文件\n\n是否开始批量安装?") + if not result: + return + + def install(): + self.show_progress(True, is_push=True) + total = len(apk_files) + self.log(f"开始批量安装 {total} 个APK...", "INFO") + self.run_adb_command('adb -d shell setprop vecentek.model 1') + + success_count = 0 + for i, apk_path in enumerate(apk_files, 1): + self.update_progress(i, total, "安装中...", is_push=True) + success, _ = self.run_adb_command(f'adb -d install -r "{apk_path}"') + if success: + success_count += 1 + + self.run_adb_command('adb -d shell setprop vecentek.model 0') + self.update_progress(total, total, "安装完成", is_push=True) + self.show_progress(False, is_push=True) + + if success_count == total: + self.log(f"安装完成:全部 {total} 个成功", "SUCCESS") + messagebox.showinfo("安装完成", f"成功安装 {total} 个APK!") + elif success_count > 0: + self.log(f"安装完成:{success_count}/{total} 成功", "WARNING") + messagebox.showwarning("部分成功", f"成功: {success_count}\n失败: {total - success_count}") + else: + self.log("安装失败", "ERROR") + messagebox.showerror("安装失败", "所有APK安装失败!") + + self.show_progress(False, is_push=True) + + threading.Thread(target=install, daemon=True).start() + + def install_single_apk(self): + """安装单个APK""" + # 检查设备连接 + if not self.check_device_connection(): + return + + file_path = filedialog.askopenfilename( + title="选择APK文件", + filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + ) + + if not file_path: + return + + def install(): + self.show_progress(True, is_push=True) + self.update_progress(50, 100, f"安装中", is_push=True) + self.run_adb_command('adb -d shell setprop vecentek.model 1') + success, _ = self.run_adb_command(f'adb -d install -r "{file_path}"') + self.run_adb_command('adb -d shell setprop vecentek.model 0') + self.update_progress(100, 100, f"完成", is_push=True) + if success: + self.log("✓ 安装成功", "SUCCESS") + else: + self.log("✗ 安装失败", "ERROR") + self.show_progress(False, is_push=True) + + threading.Thread(target=install, daemon=True).start() + + def open_language_settings(self): + """打开系统语言设置""" + if not self.check_device_connection(): + return + self.run_adb_command('adb -d shell am start -a android.settings.LOCALE_SETTINGS') + + def open_language_quick_set(self): + """打开快捷语言设置弹窗""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 创建弹窗 + popup = tk.Toplevel(self.root) + popup.title("快捷语言设置") + popup.geometry("520x320") + popup.configure(bg=self.colors['bg_dark']) + popup.resizable(False, False) + + # 居中显示 + popup.update_idletasks() + x = self.root.winfo_x() + (self.root.winfo_width() - 520) // 2 + y = self.root.winfo_y() + (self.root.winfo_height() - 320) // 2 + popup.geometry(f"+{x}+{y}") + popup.transient(self.root) + popup.grab_set() + + # 标题 + header = tk.Label(popup, text="选择目标语言", + font=('Microsoft YaHei', 13, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + header.pack(pady=(15, 10)) + + hint = tk.Label(popup, text="点击按钮即可将系统语言切换为对应语言,重启后生效", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + hint.pack(pady=(0, 12)) + + # 语言列表:(显示名, locale_code) + languages = [ + ("🇨🇳 中文", "zh-CN"), + ("英 English", "en-US"), + ("俄 Русский", "ru-RU"), + ("法 Français", "fr-FR"), + ("西 Español", "es-ES"), + ("葡 Português", "pt-BR"), + ("意 Italiano", "it-IT"), + ("阿 العربية", "ar-SA"), + ] + + # 创建按钮容器 + btn_frame = tk.Frame(popup, bg=self.colors['bg_dark']) + btn_frame.pack(pady=(0, 10)) + + btn_colors = [ + self.colors['accent'], self.colors['info'], + self.colors['success'], self.colors['warning'], + '#e17055', '#00b894', + '#6c5ce7', '#0984e3', + ] + + for i, (label, locale) in enumerate(languages): + row = i // 4 + col = i % 4 + + def make_cmd(loc=locale, lbl=label): + return lambda: self._quick_set_language(loc, lbl, popup) + + btn = tk.Button(btn_frame, text=label, + command=make_cmd(), + font=('Microsoft YaHei', 10), + fg='white', + bg=btn_colors[i], + relief=tk.FLAT, + cursor='hand2', + width=12, height=2) + btn.grid(row=row, column=col, padx=5, pady=5) + + # 底部分隔 + 打开系统设置入口 + sep = tk.Frame(popup, bg=self.colors['border'], height=1) + sep.pack(fill=tk.X, padx=20, pady=(8, 6)) + + sys_btn = tk.Button(popup, text="⚙️ 打开系统语言设置(手动选择)", + command=lambda: self._open_sys_and_close(popup), + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + sys_btn.pack(pady=(0, 10)) + + def _quick_set_language(self, locale_code, language_name, popup): + """执行快捷语言设置""" + popup.destroy() + + def do_set(): + self.log(f"正在设置系统语言为: {language_name} ({locale_code})", "INFO") + success, output = self.run_adb_command( + f'adb -d shell settings put system system_locales {locale_code}' + ) + + if success: + self.log(f"✓ 语言已设置为 {language_name}", "SUCCESS") + messagebox.showinfo( + "设置成功", + f"系统语言已设置为 {language_name}\n\n⚠️ 请重启设备使其生效。" + ) + else: + self.log(f"✗ 语言设置失败: {output}", "ERROR") + messagebox.showerror("设置失败", f"语言设置失败!\n\n{output}") + + threading.Thread(target=do_set, daemon=True).start() + + def _open_sys_and_close(self, popup): + """关闭弹窗并打开系统语言设置""" + popup.destroy() + self.open_language_settings() + + def open_timezone_settings(self): + """打开时区设置""" + if not self.check_device_connection(): + return + self.run_adb_command('adb -d shell am start -a android.settings.TIMEZONE_SETTINGS') + + def open_android_settings(self): + """打开安卓原生设置""" + if not self.check_device_connection(): + return + self.run_adb_command('adb -d shell am start -a android.settings.SETTINGS') + + def reboot_device(self): + """重启设备""" + if not self.check_device_connection(): + return + if messagebox.askyesno("确认重启", "确定要重启设备吗?"): + subprocess.Popen(f'{self.adb} -d shell reboot', shell=True, + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + self.log("设备正在重启...", "INFO") + self.update_device_status(False) + + def on_disable_upgrade(self): + """禁用系统升级""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 弹窗确认 + result = messagebox.askyesno( + "确认禁用升级", + "⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n" + "是否确定要禁用系统升级应用?\n\n" + "禁用命令:\n" + "adb -d shell pm disable-user --user 0 com.incall.apps.softmanager" + ) + + if not result: + self.log("已取消禁用升级操作", "INFO") + return + + def disable(): + self.show_progress(True, is_push=False) + success, output = self.run_adb_command( + 'adb -d shell pm disable-user --user 0 com.incall.apps.softmanager') + if success: + self.log("系统升级已禁用", "SUCCESS") + messagebox.showinfo("成功", "系统升级已成功禁用!") + else: + self.log("禁用系统升级失败", "ERROR") + messagebox.showerror("错误", f"禁用失败:{output}") + self.show_progress(False, is_push=False) + + threading.Thread(target=disable, daemon=True).start() + + def _toggle_debug(self, event=None): + """切换调试模式(隐藏入口,Ctrl+Shift+D)""" + if self.debug_mode: + self.debug_mode = False + self.log("调试模式已关闭", "WARNING") + self.status_text.config(text="就绪") + self.refresh_device_status() + return + + pwd = tk.simpledialog.askstring("调试模式", "请输入调试密码:", show='*', parent=self.root) + if pwd == "zxch5200": + self.debug_mode = True + self.update_device_status(True, "", True) + self.log("🔧 调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志", "WARNING") + self.status_text.config(text="🔧 调试模式") + elif pwd is not None: + messagebox.showwarning("错误", "密码错误") + + def install_apps(self): + """安装App — 支持单选或多选APK文件""" + if not self.check_device_connection(): + return + + file_paths = filedialog.askopenfilenames( + title="选择APK文件", + filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + ) + if not file_paths: + return + + count = len(file_paths) + result = messagebox.askyesno("确认安装", f"已选择 {count} 个APK文件\n\n是否开始安装?") + if not result: + return + + def install(): + self.show_progress(True, is_push=True) + self.log(f"开始安装 {count} 个APK...", "INFO") + self.run_adb_command('adb -d shell setprop vecentek.model 1') + + success_count = 0 + for i, file_path in enumerate(file_paths, 1): + apk_name = Path(file_path).stem + self.update_progress(i, count, f"安装中 ({apk_name})", is_push=True) + success, _ = self.run_adb_command(f'adb -d install -r "{file_path}"') + if success: + self.log(f"✓ {apk_name}.apk", "SUCCESS") + success_count += 1 + else: + self.log(f"✗ {apk_name}.apk", "ERROR") + + self.run_adb_command('adb -d shell setprop vecentek.model 0') + self.update_progress(count, count, "安装完成", is_push=True) + self.show_progress(False, is_push=True) + + if success_count == count: + self.log(f"安装完成:全部 {count} 个成功", "SUCCESS") + messagebox.showinfo("安装完成", f"成功安装 {count} 个APK!") + elif success_count > 0: + self.log(f"安装完成:{success_count}/{count} 成功", "WARNING") + messagebox.showwarning("部分成功", f"成功: {success_count}\n失败: {count - success_count}") + else: + self.log("安装失败", "ERROR") + messagebox.showerror("安装失败", "所有APK安装失败!") + + threading.Thread(target=install, daemon=True).start() + + def run(self): + """运行程序""" + self.root.mainloop() + +def main(): + """主函数""" + if sys.version_info < (3, 6): + print("错误:需要Python 3.6或更高版本") + sys.exit(1) + + try: + app = ADKAPKGUI() + app.run() + except Exception as e: + print(f"启动失败: {e}") + import traceback + traceback.print_exc() + messagebox.showerror("错误", f"程序启动失败: {e}") + +if __name__ == "__main__": + main() diff --git a/adb.exe b/adb.exe new file mode 100644 index 0000000..55f65f9 Binary files /dev/null and b/adb.exe differ diff --git a/app-install.py b/app-install.py new file mode 100644 index 0000000..262e796 --- /dev/null +++ b/app-install.py @@ -0,0 +1,1296 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import subprocess +import json +import threading +import tkinter as tk +from tkinter import ttk, scrolledtext, filedialog, messagebox +from pathlib import Path +from urllib.request import urlopen, Request +from urllib.error import URLError, HTTPError +from datetime import datetime +import zipfile +try: + import pyzipper +except ImportError: + pyzipper = None +import shutil +import time + +class ADKAPKGUI: + def __init__(self): + self.root = tk.Tk() + self.root.title("长安语言刷入工具") + self.root.geometry("650x550") + self.root.resizable(True, True) + + # 设置颜色主题 + self.colors = { + 'bg_dark': '#1e1e2e', + 'bg_light': '#2a2a3e', + 'accent': '#6c5ce7', + 'accent_hover': '#5b4bc4', + 'success': '#00b894', + 'error': '#d63031', + 'warning': '#fdcb6e', + 'info': '#0984e3', + 'text': '#dfe6e9', + 'text_secondary': '#b2bec3', + 'border': '#3d3d5e' + } + + self.base_dir = Path(sys.executable).parent if getattr(sys, 'frozen', False) else Path(__file__).parent + if getattr(sys, 'frozen', False): + self.adb = str(Path(sys._MEIPASS) / 'adb.exe') + self.sz = str(Path(sys._MEIPASS) / '7za.exe') + else: + self.adb = 'adb' + self.sz = str(self.base_dir / '7za.exe') + self.package_file = self.base_dir / "package.bin" + self.extract_password = None + self.apps_dir = None + self.temp_dir = None + self.api_url = "https://api.changan.softwindy.cn/api/authorizations/auth-check" + self.vin = None + self.device_connected = False + self._refreshing = False # 防止并发刷新 + self.debug_mode = False # 调试模式 + + # 设置样式 + self.setup_styles() + self.setup_ui() + self.center_window() + + # 检查环境 + self.check_environment() + + # 启动设备状态监控 + self.start_device_monitor() + + def setup_styles(self): + """设置自定义样式""" + style = ttk.Style() + style.theme_use('clam') + + # 配置主颜色 + style.configure('TFrame', background=self.colors['bg_dark']) + style.configure('TLabel', background=self.colors['bg_dark'], foreground=self.colors['text']) + style.configure('TLabelframe', background=self.colors['bg_dark'], foreground=self.colors['text']) + style.configure('TLabelframe.Label', background=self.colors['bg_dark'], foreground=self.colors['accent']) + + # 配置进度条 + style.configure('TProgressbar', + background=self.colors['accent'], + troughcolor=self.colors['bg_light'], + borderwidth=0) + + def setup_ui(self): + """设置UI界面""" + # 配置根窗口 + self.root.configure(bg=self.colors['bg_dark']) + + # 创建主框架 + main_frame = tk.Frame(self.root, bg=self.colors['bg_dark']) + main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 顶部标题栏 + title_frame = tk.Frame(main_frame, bg=self.colors['bg_dark'], height=65) + title_frame.pack(fill=tk.X, pady=(0, 10)) + title_frame.pack_propagate(False) + + # 标题 + title_label = tk.Label(title_frame, + text="🚀 CS55Plus/Q05", + font=('Microsoft YaHei', 18, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + title_label.pack() + + subtitle_label = tk.Label(title_frame, + text="@宜宾科宜科技有限公司 - 出口改装一站式服务", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + subtitle_label.pack() + + # 工程密码查询区域 + pwd_query_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + pwd_query_frame.pack(fill=tk.X, pady=(0, 5), padx=5) + + tk.Label(pwd_query_frame, text="工程密码查询:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT, padx=(10, 5), pady=5) + + self.vin_input = tk.Entry(pwd_query_frame, + font=('Consolas', 9), + bg='#2d2d3d', + fg='#636e72', + insertbackground='white', + relief=tk.FLAT, + width=20) + self.vin_input.insert(0, "请输入VIN") + self.vin_input.bind("", self._on_vin_input_focus_in) + self.vin_input.bind("", self._on_vin_input_focus_out) + self.vin_input.pack(side=tk.LEFT, padx=5, pady=5) + + self.btn_query_pwd = tk.Button(pwd_query_frame, text="查询密码", + command=self.query_password_by_vin, + font=('Microsoft YaHei', 8), + fg='white', + bg=self.colors['accent'], + relief=tk.FLAT, + cursor='hand2') + self.btn_query_pwd.pack(side=tk.LEFT, padx=5, pady=5) + + self.pwd_result_label = tk.Label(pwd_query_frame, text="", + font=('Microsoft YaHei', 9, 'bold'), + fg=self.colors['success'], + bg=self.colors['bg_light']) + self.pwd_result_label.pack(side=tk.LEFT, padx=10, pady=5) + + + # 工厂模式提示 + factory_hint_frame = tk.Frame(main_frame, bg=self.colors['bg_dark']) + factory_hint_frame.pack(fill=tk.X, pady=(0, 3)) + tk.Label(factory_hint_frame, text="🔧 关闭车辆WI-FI和4G网络,拨号获取的密码进入工程模式", + font=('Microsoft YaHei', 8), + fg=self.colors['warning'], + bg=self.colors['bg_dark']).pack(side=tk.LEFT, padx=2) + + # 按钮区域(两排,每排5个) + button_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + button_frame.pack(fill=tk.X, pady=(0, 10), padx=5) + + # 按钮样式参数 + btn_params = { + 'font': ('Microsoft YaHei', 9), + 'fg': 'white', + 'relief': tk.FLAT, + 'cursor': 'hand2', + 'height': 1, + 'width': 14 + } + + # 第一排按钮 + row1_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + row1_frame.pack(pady=(8, 4)) + + self.btn_push = tk.Button(row1_frame, text="📦 刷入语言包", + command=self.push_all_apks, + bg=self.colors['accent'], + **btn_params) + self.btn_push.pack(side=tk.LEFT, padx=4) + + self.btn_install_all = tk.Button(row1_frame, text="📱 安装App", + command=self.install_apps, + bg=self.colors['accent'], + **btn_params) + self.btn_install_all.pack(side=tk.LEFT, padx=4) + + self.btn_language = tk.Button(row1_frame, text="🌐 语言设置", + command=self.open_language_quick_set, + bg=self.colors['accent'], + **btn_params) + self.btn_language.pack(side=tk.LEFT, padx=4) + + # 第二排按钮 + row2_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + row2_frame.pack(pady=(4, 8)) + + self.btn_timezone = tk.Button(row2_frame, text="⏰ 时区设置", + command=self.open_timezone_settings, + bg=self.colors['accent'], + **btn_params) + self.btn_timezone.pack(side=tk.LEFT, padx=4) + + self.btn_settings = tk.Button(row2_frame, text="⚙️ 安卓设置", + command=self.open_android_settings, + bg=self.colors['accent'], + **btn_params) + self.btn_settings.pack(side=tk.LEFT, padx=4) + + self.btn_reboot = tk.Button(row2_frame, text="🔄 重启设备", + command=self.reboot_device, + bg=self.colors['warning'], + **btn_params) + self.btn_reboot.pack(side=tk.LEFT, padx=4) + + self.btn_exit = tk.Button(row2_frame, text="❌ 禁用升级", + command=self.on_disable_upgrade, + bg=self.colors['error'], + **btn_params) + self.btn_exit.pack(side=tk.LEFT, padx=4) + + # 设备状态栏(横条) + status_bar_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + status_bar_frame.pack(fill=tk.X, pady=(0, 5)) + + # 状态指示器 + status_indicator_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + status_indicator_frame.pack(side=tk.LEFT, padx=10, pady=5) + + self.status_indicator = tk.Canvas(status_indicator_frame, width=10, height=10, + bg=self.colors['bg_light'], highlightthickness=0) + self.status_indicator.pack(side=tk.LEFT) + self.status_dot = self.status_indicator.create_oval(2, 2, 8, 8, fill='#636e72') + + tk.Label(status_indicator_frame, text="设备:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT, padx=(5, 3)) + + self.device_status_label = tk.Label(status_indicator_frame, text="未检测", + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light']) + self.device_status_label.pack(side=tk.LEFT) + + # VIN信息 + vin_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + vin_frame.pack(side=tk.LEFT, padx=20, pady=5) + tk.Label(vin_frame, text="VIN码:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT) + self.vin_label = tk.Label(vin_frame, text="未获取", + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light']) + self.vin_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 授权状态 + auth_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + auth_frame.pack(side=tk.LEFT, padx=20, pady=5) + tk.Label(auth_frame, text="授权:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT) + self.auth_label = tk.Label(auth_frame, text="未验证", + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light']) + self.auth_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 刷新按钮 + refresh_btn = tk.Button(status_bar_frame, text="🔄 检查", + command=lambda: self.refresh_device_status(force=True), + font=('Microsoft YaHei', 8), + fg=self.colors['accent'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + refresh_btn.pack(side=tk.RIGHT, padx=10, pady=5) + + # 解压进度条框架 + progress_frame = tk.Frame(main_frame, bg=self.colors['bg_dark']) + progress_frame.pack(fill=tk.X, pady=(5, 5)) + + self.progress_label = tk.Label(progress_frame, text="", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + self.progress_label.pack() + + self.progress = ttk.Progressbar(progress_frame, mode='determinate', style='TProgressbar') + self.progress.pack(fill=tk.X, pady=(2, 0)) + + # 推送进度条 + self.push_progress_label = tk.Label(progress_frame, text="", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + + self.push_progress = ttk.Progressbar(progress_frame, mode='determinate', style='TProgressbar') + + # 日志区域(下方) + log_card = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + log_card.pack(fill=tk.BOTH, expand=True, pady=(5, 0)) + + # 日志标题栏 + log_title_frame = tk.Frame(log_card, bg=self.colors['bg_dark'], height=30) + log_title_frame.pack(fill=tk.X) + log_title_frame.pack_propagate(False) + + tk.Label(log_title_frame, text="📋 运行日志", + font=('Microsoft YaHei', 10, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']).pack(side=tk.LEFT, padx=10) + + self.btn_clear = tk.Button(log_title_frame, text="🗑 清空日志", + command=self.clear_log, + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark'], + relief=tk.FLAT, + cursor='hand2') + self.btn_clear.pack(side=tk.RIGHT, padx=10) + + # 日志文本框 + text_frame = tk.Frame(log_card, bg=self.colors['bg_light']) + text_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + self.log_text = scrolledtext.ScrolledText(text_frame, + height=12, + wrap=tk.WORD, + font=('Consolas', 9), + bg='#2d2d3d', + fg='#e0e0e0', + insertbackground='white', + relief=tk.FLAT, + borderwidth=0) + self.log_text.pack(fill=tk.BOTH, expand=True) + + # 配置日志颜色标签 + self.log_text.tag_config('INFO', foreground='#74b9ff') + self.log_text.tag_config('SUCCESS', foreground='#55efc4') + self.log_text.tag_config('ERROR', foreground='#ff7675') + self.log_text.tag_config('WARNING', foreground='#ffeaa7') + self.log_text.tag_config('CMD', foreground='#a29bfe') + + # 底部状态栏 + bottom_status = tk.Frame(main_frame, bg=self.colors['bg_light'], height=22) + bottom_status.pack(fill=tk.X, pady=(5, 0)) + bottom_status.pack_propagate(False) + + self.status_text = tk.Label(bottom_status, text="就绪", + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light']) + self.status_text.pack(side=tk.LEFT, padx=10) + + # 调试模式快捷键 + self.root.bind('', self._toggle_debug) + + # 绑定悬停效果 + self.bind_hover_effects() + + def bind_hover_effects(self): + """绑定按钮悬停效果""" + buttons = [self.btn_push, self.btn_install_all, + self.btn_language, self.btn_timezone, self.btn_settings, + self.btn_reboot, self.btn_clear, self.btn_exit, self.btn_query_pwd] + + for btn in buttons: + original_bg = btn.cget('bg') + def on_enter(e, btn=btn, bg=original_bg): + btn.config(bg=self.lighten_color(bg)) + def on_leave(e, btn=btn, bg=original_bg): + btn.config(bg=bg) + btn.bind('', on_enter) + btn.bind('', on_leave) + + def lighten_color(self, color): + """调亮颜色""" + if color == self.colors['accent']: + return self.colors['accent_hover'] + elif color == self.colors['warning']: + return '#feca57' + elif color == self.colors['info']: + return '#0984e3' + elif color == self.colors['error']: + return '#e17055' + elif color == self.colors['success']: + return '#00a884' + return color + + def center_window(self): + """将窗口居中显示在屏幕上""" + self.root.update_idletasks() + screen_w = self.root.winfo_screenwidth() + screen_h = self.root.winfo_screenheight() + win_w = self.root.winfo_reqwidth() + win_h = self.root.winfo_reqheight() + x = (screen_w - win_w) // 2 + y = (screen_h - win_h) // 2 + self.root.geometry(f"+{x}+{y}") + + def run_on_ui_thread(self, func, *args, **kwargs): + """将函数调度到主线程执行,确保线程安全""" + self.root.after(0, func, *args, **kwargs) + + def _log_impl(self, message, level="INFO"): + """日志写入的实际实现(必须在主线程调用)""" + timestamp = datetime.now().strftime("%H:%M:%S") + log_entry = f"[{timestamp}] [{level}] {message}\n" + self.log_text.insert(tk.END, log_entry, level) + self.log_text.see(tk.END) + + def log(self, message, level="INFO"): + """添加日志(线程安全)""" + self.run_on_ui_thread(self._log_impl, message, level) + + def clear_log(self): + """清空日志""" + self.log_text.delete(1.0, tk.END) + self.log("日志已清空", "INFO") + + def _show_progress_impl(self, show=True, is_push=False): + """显示/隐藏进度条的实际实现(必须在主线程调用)""" + if is_push: + if show: + self.push_progress_label.pack() + self.push_progress.pack(fill=tk.X, pady=(2, 0)) + self.push_progress['value'] = 0 + else: + self.push_progress_label.pack_forget() + self.push_progress.pack_forget() + else: + if show: + self.progress_label.pack() + self.progress.pack(fill=tk.X, pady=(2, 0)) + self.progress['value'] = 0 + else: + self.progress_label.pack_forget() + self.progress.pack_forget() + + def show_progress(self, show=True, is_push=False): + """显示/隐藏进度条(线程安全)""" + self.run_on_ui_thread(self._show_progress_impl, show, is_push) + + def _update_progress_impl(self, value, max_value=100, label="", is_push=False): + """更新进度条的实际实现(必须在主线程调用)""" + if is_push: + percent = (value / max_value) * 100 + self.push_progress['value'] = percent + self.push_progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)") + else: + percent = (value / max_value) * 100 + self.progress['value'] = percent + self.progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)") + self.root.update_idletasks() + + def update_progress(self, value, max_value=100, label="", is_push=False): + """更新进度条(线程安全)""" + self.run_on_ui_thread(self._update_progress_impl, value, max_value, label, is_push) + + def update_device_status(self, connected, vin=None, authorized=False): + """更新设备状态显示(线程安全:立即设状态变量,UI走主线程)""" + self.device_connected = connected + if vin is not None: + self.vin = vin + self.run_on_ui_thread(self._update_device_status_impl, connected, vin, authorized) + + def _update_device_status_impl(self, connected, vin, authorized): + """设备状态UI更新的实际实现(必须在主线程调用)""" + if connected: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['success']) + self.device_status_label.config(text="已连接", fg=self.colors['success']) + if vin: + self.vin_label.config(text=vin, fg=self.colors['success']) + if authorized: + self.auth_label.config(text="已授权", fg=self.colors['success']) + else: + self.auth_label.config(text="未授权", fg=self.colors['error']) + else: + self.vin_label.config(text="未获取", fg=self.colors['error']) + self.auth_label.config(text="未验证", fg=self.colors['error']) + else: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['error']) + self.device_status_label.config(text="未连接", fg=self.colors['error']) + self.vin_label.config(text="未获取", fg=self.colors['error']) + self.auth_label.config(text="未验证", fg=self.colors['error']) + + def check_device_connection(self): + """检查设备是否连接""" + if self.debug_mode: + return True + if not self.device_connected: + messagebox.showwarning("设备未连接", "请先连接设备并点击「检查」按钮刷新状态!") + return False + return True + + def start_device_monitor(self): + """启动设备状态监控(每5秒检查一次)""" + def monitor(): + while True: + try: + result = subprocess.run(f'{self.adb} -d devices', shell=True, capture_output=True, text=True) + lines = result.stdout.strip().split('\n') + devices = [line for line in lines[1:] if line.strip() and 'device' in line and 'offline' not in line] + + if devices and not self.device_connected and not self._refreshing: + # 设备新连接,刷新状态 + self.refresh_device_status() + elif not devices and self.device_connected: + # 设备断开连接 + self.update_device_status(False) + self.log("设备已断开连接", "WARNING") + + time.sleep(5) + except: + time.sleep(5) + + threading.Thread(target=monitor, daemon=True).start() + + # ============================================================ + # 核心:adb shell 自动密码输入 + # ============================================================ + + def run_adb_shell(self, shell_command): + """执行 adb shell 命令,自动静默输入设备密码 adb36987。 + 静默执行,不显示 adb 原始输出,仅返回结果。""" + if self.debug_mode: + self.log(f"CMD: adb shell {shell_command}", "CMD") + try: + proc = subprocess.Popen( + f'{self.adb} -d shell {shell_command}', + shell=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + stdout, stderr = proc.communicate(input='adb36987\n', timeout=15) + + # 过滤密码提示行 + output_lines = [] + for line in stdout.split('\n'): + stripped = line.strip() + if 'please input verify password' in stripped.lower(): + continue + if stripped == 'verify success!': + continue + output_lines.append(line) + output = '\n'.join(output_lines).strip() + + if proc.returncode == 0: + if self.debug_mode: + self.log(f"CMD OK: {output[:200]}", "CMD") + return True, output + else: + if self.debug_mode: + self.log(f"CMD FAIL: {stderr.strip()[:200]}", "CMD") + return False, stderr.strip() + + except subprocess.TimeoutExpired: + proc.kill() + proc.communicate() + return False, "命令超时" + except Exception as e: + return False, str(e) + + # ============================================================ + # 原始 adb 命令(用于 adb push / adb install 等不需要 shell 的操作) + # ============================================================ + + def run_adb_command(self, command): + """执行原始 adb 命令(adb push / adb install 等,无需 shell 密码)。 + 静默执行,不显示 adb 原始输出,仅返回结果。""" + command = command.replace('adb', self.adb, 1) + if self.debug_mode: + self.log(f"CMD: {command}", "CMD") + try: + result = subprocess.run(command, shell=True, capture_output=True, text=True, encoding='utf-8') + if result.returncode == 0: + if self.debug_mode: + self.log(f"CMD OK: {result.stdout.strip()[:200]}", "CMD") + return True, result.stdout.strip() + else: + if self.debug_mode: + self.log(f"CMD FAIL: {result.stderr.strip()[:200]}", "CMD") + return False, result.stderr.strip() + except Exception as e: + if self.debug_mode: + self.log(f"CMD ERROR: {str(e)}", "CMD") + return False, str(e) + + def check_package_extracted(self): + """检查语言包是否已解压""" + has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0 + return has_app + + def extract_package_silent(self): + """静默解压语言包(带进度)—— 逸动版仅处理 app 目录""" + if not self.package_file.exists(): + self.log(f"错误:未找到资源包", "ERROR") + return False + + try: + # 使用用户目录,无需管理员权限 + local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) + hidden_path = Path(local_appdata) / ".cache" / "system" / ".android" + hidden_path.mkdir(parents=True, exist_ok=True) + + self.temp_dir = hidden_path / "apps_cache_common" + + # 如果已存在,先清理 + if self.temp_dir.exists(): + shutil.rmtree(self.temp_dir, ignore_errors=True) + time.sleep(0.5) + + self.temp_dir.mkdir(parents=True, exist_ok=True) + + # 设置隐藏属性(Windows) + if sys.platform == 'win32': + subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True) + subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True) + + self.log(f"正在准备资源,耗时较长,请耐心等待...", "INFO") + + # 使用 7za 高速解压 + self.update_progress(0, 1, "资源加载中...") + result = subprocess.run( + [self.sz, 'x', str(self.package_file), f'-p{self.extract_password}', f'-o{self.temp_dir}', '-y'], + capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + if result.returncode != 0: + stderr = result.stderr.strip() + if stderr: + self.log(f"解压失败: {stderr[:200]}", "ERROR") + else: + self.log("解压失败,请检查密码是否正确", "ERROR") + return False + self.update_progress(1, 1, "资源加载完成") + + # 查找 app 目录(逸动无 priv-app) + self.apps_dir = None + + app_candidates = list(self.temp_dir.rglob("apps")) + if app_candidates: + self.apps_dir = app_candidates[0] + + if not self.apps_dir: + self.log("警告:未找到 apps 目录", "WARNING") + return False + + return True + + except Exception as e: + self.log(f"资源准备失败", "ERROR") + return False + + def check_environment(self): + """检查环境""" + try: + result = subprocess.run(f'{self.adb} version', shell=True, capture_output=True, text=True) + if result.returncode == 0: + self.refresh_device_status() + if not self.package_file.exists(): + self.log("未找到资源包文件", "WARNING") + else: + self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") + except FileNotFoundError: + self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") + + def refresh_device_status(self, force=False): + """刷新设备状态 —— 逸动版使用 ca.car.vin 获取 VIN""" + # 防止并发刷新(手动点击「检查」时强制忽略锁) + if self._refreshing and not force: + return + self._refreshing = True + + def refresh(): + was_connected = self.device_connected + + # 检查设备连接 + result = subprocess.run(f'{self.adb} -d devices', shell=True, capture_output=True, text=True) + lines = result.stdout.strip().split('\n') + devices = [line for line in lines[1:] if line.strip() and 'device' in line and 'offline' not in line] + + if devices: + if not was_connected: + self.log("设备已连接", "SUCCESS") + + # 获取VIN —— 逸动车型使用 ca.car.vin + success, vin_output = self.run_adb_shell( + 'settings get system ca.car.vin') + vin = vin_output.strip() if success else '' + + if vin: + self.log(f"VIN: {vin}", "INFO") + authorized = self.check_authorization(vin) + self.update_device_status(True, vin, authorized) + else: + self.log("无法获取VIN,请确认设备已进入工厂模式", "WARNING") + self.update_device_status(True, None, False) + else: + if was_connected: + self.log("设备未连接", "WARNING") + self.update_device_status(False) + + self._refreshing = False + + threading.Thread(target=refresh, daemon=True).start() + + def check_authorization(self, vin): + """检查授权""" + if self.debug_mode: + self.log("调试模式: 跳过授权验证", "WARNING") + return True + self.log("正在验证授权状态...", "INFO") + + try: + url = f"{self.api_url}?vin={vin}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + if data.get('authorized') == True: + self.log("✅ 授权验证通过!", "SUCCESS") + if 'data' in data and 'vehicleName' in data['data']: + self.log(f"车辆名称: {data['data']['vehicleName']}", "INFO") + return True + else: + self.log(f"❌ 授权验证失败", "ERROR") + return False + + except Exception as e: + self.log(f"❌ 授权验证失败", "ERROR") + return False + + def fetch_package_password(self): + """从服务端获取资源包解压密码""" + if not self.vin: + self.log("请先连接adb!", "ERROR") + return False + + try: + pwd_api_url = "https://api.changan.softwindy.cn/api/authorizations/package-key" + url = f"{pwd_api_url}?vin={self.vin}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + if data.get('success') and 'data' in data and 'password' in data['data']: + self.extract_password = data['data']['password'] + return True + else: + self.log(f"数据准备失败: {data.get('message', '未知错误')}", "ERROR") + return False + + except Exception as e: + self.log(f"数据准备失败: {str(e)}", "ERROR") + return False + + def push_single_apk(self, apk_path, apk_name): + """推送单个APK到设备并安装,返回 (成功, 错误信息)""" + temp_apk_path = f"/data/local/tmp/{apk_name}.apk" + + ok, err = self.run_adb_command(f'adb -d push "{apk_path}" {temp_apk_path}') + if not ok: + return False, f"push失败: {err}" + + ok, err = self.run_adb_shell(f'pm install -r {temp_apk_path}') + self.run_adb_shell(f'rm -f {temp_apk_path}') + if not ok: + return False, f"install失败: {err}" + + return True, "" + + def _push_and_install(self, apk_path, apk_name): + """push → pm install → cleanup,供安装类方法复用""" + ok, _ = self.push_single_apk(apk_path, apk_name) + return ok + + def push_all_apks(self): + """推送APK并安装 —— 逸动版仅处理 app 目录,使用 pm install""" + # 检查设备连接(仅 UI 层检查在主线程,其余工作进后台线程) + if not self.check_device_connection(): + return + + if not self.vin: + messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码") + return + + def do_push_all(): + # 验证授权 + if not self.check_authorization(self.vin): + self.run_on_ui_thread(lambda: messagebox.showerror("授权失败", "设备未授权")) + return + + # 获取解压密码 + if not self.extract_password: + if not self.fetch_package_password(): + self.run_on_ui_thread(lambda: messagebox.showerror("错误", "数据准备失败!")) + return + + # 解压 + if not self.check_package_extracted(): + self.log("正在准备资源...", "INFO") + self.show_progress(True, is_push=False) + if not self.extract_package_silent(): + self.show_progress(False, is_push=False) + self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源准备失败!")) + return + self.show_progress(False, is_push=False) + + if not self.apps_dir or not self.apps_dir.exists(): + self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源目录未找到")) + return + + # 开始刷入 + self.show_progress(True, is_push=True) + self.log("开始刷入语言包...", "INFO") + self.run_adb_shell('mkdir -p /data/local/tmp') + self.run_adb_shell('setprop vecentek.model 1') + + all_apks = list(self.apps_dir.glob("*.apk")) + if not all_apks: + self.log("未找到语言包文件", "WARNING") + self.show_progress(False, is_push=True) + return + + total = len(all_apks) + success_count = 0 + for i, apk_path in enumerate(all_apks, 1): + apk_name = apk_path.stem + ok, _ = self.push_single_apk(apk_path, apk_name) + if ok: + self.log(f"安装成功: {apk_name}.apk", "SUCCESS") + success_count += 1 + else: + self.log(f"安装失败: {apk_name}.apk", "ERROR") + self.update_progress(i, total, "正在刷入...", is_push=True) + + self.update_progress(total, total, "刷入完成", is_push=True) + self.run_adb_shell('setprop vecentek.model 0') + + if success_count > 0: + self.log("语言包刷入完成,重启设备后生效", "SUCCESS") + else: + self.log("语言包刷入失败", "ERROR") + + self.show_progress(False, is_push=True) + + threading.Thread(target=do_push_all, daemon=True).start() + + def install_all_apks(self): + """批量安装APK — push → pm install → cleanup""" + if not self.check_device_connection(): + return + if not self.vin: + messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码") + return + if not self.check_authorization(self.vin): + messagebox.showerror("授权失败", "设备未授权,无法执行此操作") + return + + # 使用当前目录下的apks文件夹 + apk_dir = self.base_dir / "apks" + + # 检查apk文件夹是否存在 + if not apk_dir.exists(): + messagebox.showerror("错误", "未找到apks文件夹!\n请在程序目录下创建apks文件夹并放入APK文件。") + self.log("未找到apks文件夹", "ERROR") + return + + # 查找所有apk文件 + apk_files = list(apk_dir.glob("*.apk")) + if not apk_files: + messagebox.showerror("错误", "apks文件夹中没有找到APK文件!") + self.log("apk文件夹中没有找到APK文件", "ERROR") + return + + # 询问是否确认安装 + result = messagebox.askyesno("确认安装", + f"找到 {len(apk_files)} 个APK文件\n\n是否开始批量安装?") + if not result: + return + + def install(): + self.show_progress(True, is_push=True) + total = len(apk_files) + self.log(f"开始批量安装 {total} 个APK...", "INFO") + + self.run_adb_shell('setprop vecentek.model 1') + + success_count = 0 + for i, apk_path in enumerate(apk_files, 1): + apk_name = apk_path.stem + self.update_progress(i, total, "安装中...", is_push=True) + if self._push_and_install(apk_path, apk_name): + self.log(f"安装成功: {apk_name}.apk", "SUCCESS") + success_count += 1 + else: + self.log(f"安装失败: {apk_name}.apk", "ERROR") + + self.run_adb_shell('setprop vecentek.model 0') + self.update_progress(total, total, "安装完成", is_push=True) + self.show_progress(False, is_push=True) + + if success_count == total: + messagebox.showinfo("安装完成", f"成功安装 {total} 个APK!") + elif success_count > 0: + messagebox.showwarning("部分成功", f"成功: {success_count}\n失败: {total - success_count}") + messagebox.showwarning("部分成功", f"成功: {success_count}\n失败: {total - success_count}") + else: + self.log("安装失败", "ERROR") + messagebox.showerror("安装失败", "所有APK安装失败!") + + threading.Thread(target=install, daemon=True).start() + + def install_single_apk(self): + """安装单个APK — push → pm install → cleanup""" + if not self.check_device_connection(): + return + if not self.vin: + messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码") + return + if not self.check_authorization(self.vin): + messagebox.showerror("授权失败", "设备未授权,无法执行此操作") + return + + file_path = filedialog.askopenfilename( + title="选择APK文件", + filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + ) + + if not file_path: + return + + def install(): + apk_name = Path(file_path).stem + self.show_progress(True, is_push=True) + self.update_progress(30, 100, "安装中", is_push=True) + + self.run_adb_shell('setprop vecentek.model 1') + success = self._push_and_install(file_path, apk_name) + self.run_adb_shell('setprop vecentek.model 0') + + self.update_progress(100, 100, "完成", is_push=True) + if success: + self.log("安装成功", "SUCCESS") + else: + self.log("安装失败", "ERROR") + self.show_progress(False, is_push=True) + + threading.Thread(target=install, daemon=True).start() + + def open_language_settings(self): + """打开系统语言设置""" + if not self.check_device_connection(): + return + self.run_adb_shell('am start -a android.settings.LOCALE_SETTINGS') + + def open_language_quick_set(self): + """打开快捷语言设置弹窗""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 创建弹窗 + popup = tk.Toplevel(self.root) + popup.title("快捷语言设置") + popup.geometry("520x320") + popup.configure(bg=self.colors['bg_dark']) + popup.resizable(False, False) + + # 居中显示 + popup.update_idletasks() + x = self.root.winfo_x() + (self.root.winfo_width() - 520) // 2 + y = self.root.winfo_y() + (self.root.winfo_height() - 320) // 2 + popup.geometry(f"+{x}+{y}") + popup.transient(self.root) + popup.grab_set() + + # 标题 + header = tk.Label(popup, text="选择目标语言", + font=('Microsoft YaHei', 13, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + header.pack(pady=(15, 10)) + + hint = tk.Label(popup, text="点击按钮即可将系统语言切换为对应语言,重启后生效", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + hint.pack(pady=(0, 12)) + + # 语言列表:(显示名, locale_code) + languages = [ + ("🇨🇳 中文", "zh-CN"), + ("英 English", "en-US"), + ("俄 Русский", "ru-RU"), + ("法 Français", "fr-FR"), + ("西 Español", "es-ES"), + ("葡 Português", "pt-BR"), + ("意 Italiano", "it-IT"), + ("阿 العربية", "ar-SA"), + ] + + # 创建按钮容器 + btn_frame = tk.Frame(popup, bg=self.colors['bg_dark']) + btn_frame.pack(pady=(0, 10)) + + btn_colors = [ + self.colors['accent'], self.colors['info'], + self.colors['success'], self.colors['warning'], + '#e17055', '#00b894', + '#6c5ce7', '#0984e3', + ] + + for i, (label, locale) in enumerate(languages): + row = i // 4 + col = i % 4 + + def make_cmd(loc=locale, lbl=label): + return lambda: self._quick_set_language(loc, lbl, popup) + + btn = tk.Button(btn_frame, text=label, + command=make_cmd(), + font=('Microsoft YaHei', 10), + fg='white', + bg=btn_colors[i], + relief=tk.FLAT, + cursor='hand2', + width=12, height=2) + btn.grid(row=row, column=col, padx=5, pady=5) + + # 底部分隔 + 打开系统设置入口 + sep = tk.Frame(popup, bg=self.colors['border'], height=1) + sep.pack(fill=tk.X, padx=20, pady=(8, 6)) + + sys_btn = tk.Button(popup, text="⚙️ 打开系统语言设置(手动选择)", + command=lambda: self._open_sys_and_close(popup), + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + sys_btn.pack(pady=(0, 10)) + + def _quick_set_language(self, locale_code, language_name, popup): + """执行快捷语言设置""" + popup.destroy() + + def do_set(): + self.log(f"正在设置系统语言为: {language_name} ({locale_code})", "INFO") + success, output = self.run_adb_shell( + f'settings put system system_locales {locale_code}' + ) + + if success: + self.log(f"✓ 语言已设置为 {language_name}", "SUCCESS") + messagebox.showinfo( + "设置成功", + f"系统语言已设置为 {language_name}\n\n⚠️ 请重启设备使其生效。" + ) + else: + self.log(f"✗ 语言设置失败: {output}", "ERROR") + messagebox.showerror("设置失败", f"语言设置失败!\n\n{output}") + + threading.Thread(target=do_set, daemon=True).start() + + def _open_sys_and_close(self, popup): + """关闭弹窗并打开系统语言设置""" + popup.destroy() + self.open_language_settings() + + def open_timezone_settings(self): + """打开时区设置""" + if not self.check_device_connection(): + return + self.run_adb_shell('am start -a android.settings.TIMEZONE_SETTINGS') + + def open_android_settings(self): + """打开安卓原生设置""" + if not self.check_device_connection(): + return + self.run_adb_shell('am start -a android.settings.SETTINGS') + + def reboot_device(self): + """重启设备""" + if not self.check_device_connection(): + return + if messagebox.askyesno("确认重启", "确定要重启设备吗?"): + proc = subprocess.Popen(f'{self.adb} -d shell reboot', shell=True, + stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + try: + proc.stdin.write(b'adb36987\n') + proc.stdin.flush() + proc.stdin.close() + except: + pass + self.log("设备正在重启...", "INFO") + self.update_device_status(False) + + def on_disable_upgrade(self): + """禁用系统升级""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 弹窗确认 + result = messagebox.askyesno( + "确认禁用升级", + "⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n" + "是否确定要禁用系统升级应用?\n\n" + "禁用命令:\n" + "adb shell pm disable-user --user 0 com.incall.apps.softmanager" + ) + + if not result: + self.log("已取消禁用升级操作", "INFO") + return + + def disable(): + self.show_progress(True, is_push=False) + success, output = self.run_adb_shell( + 'pm disable-user --user 0 com.incall.apps.softmanager') + + if success: + self.log("系统升级已禁用", "SUCCESS") + messagebox.showinfo("成功", "系统升级已成功禁用!") + else: + self.log("禁用系统升级失败", "ERROR") + messagebox.showerror("错误", f"禁用失败:{output}") + + self.show_progress(False, is_push=False) + + threading.Thread(target=disable, daemon=True).start() + + def _on_vin_input_focus_in(self, event): + """输入框获得焦点时清除占位符""" + if self.vin_input.get() == "请输入VIN": + self.vin_input.delete(0, tk.END) + self.vin_input.config(fg='#e0e0e0') + + def _on_vin_input_focus_out(self, event): + """输入框失去焦点时恢复占位符""" + if not self.vin_input.get(): + self.vin_input.insert(0, "请输入VIN") + self.vin_input.config(fg='#636e72') + + def query_password_by_vin(self): + """通过VIN查询密码""" + vin = self.vin_input.get().strip() + if not vin: + messagebox.showwarning("提示", "请输入VIN码") + return + + def do_query(): + try: + api_url = "https://api.changan.softwindy.cn/api/authorizations/generate-password-by-vin" + url = f"{api_url}?vin={vin}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + def update_ui(): + if data.get('success'): + pwd = data.get('data', {}).get('devicePassword', '未知') + self.pwd_result_label.config( + text=f"密码: *#{pwd}#*", + fg=self.colors['success'] + ) + self.log(f"密码查询成功 VIN={vin} -> {pwd}", "SUCCESS") + else: + msg = data.get('message', '查询失败') + self.pwd_result_label.config( + text=f"失败: {msg}", + fg=self.colors['error'] + ) + self.log(f"密码查询失败: {msg}", "ERROR") + + self.run_on_ui_thread(update_ui) + + except Exception as e: + def update_ui_error(): + self.pwd_result_label.config( + text=f"请求失败", + fg=self.colors['error'] + ) + self.log(f"密码查询请求失败: {str(e)}", "ERROR") + self.run_on_ui_thread(update_ui_error) + + threading.Thread(target=do_query, daemon=True).start() + + def _toggle_debug(self, event=None): + """切换调试模式(隐藏入口,Ctrl+Shift+D)""" + if self.debug_mode: + self.debug_mode = False + self.log("调试模式已关闭", "WARNING") + self.status_text.config(text="就绪") + self.refresh_device_status() + return + + pwd = tk.simpledialog.askstring("调试模式", "请输入调试密码:", show='*', parent=self.root) + if pwd == "zxch5200": + self.debug_mode = True + self.update_device_status(True, "", True) + self.log("🔧 调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志", "WARNING") + self.status_text.config(text="🔧 调试模式") + elif pwd is not None: + messagebox.showwarning("错误", "密码错误") + + def install_apps(self): + """安装App — 支持单选或多选APK文件""" + if not self.check_device_connection(): + return + if not self.vin and not self.debug_mode: + messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码") + return + if not self.check_authorization(self.vin): + return + + file_paths = filedialog.askopenfilenames( + title="选择APK文件", + filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + ) + if not file_paths: + return + + count = len(file_paths) + result = messagebox.askyesno("确认安装", f"已选择 {count} 个APK文件\n\n是否开始安装?") + if not result: + return + + def install(): + self.show_progress(True, is_push=True) + self.log(f"开始安装 {count} 个APK...", "INFO") + self.run_adb_shell('setprop vecentek.model 1') + + success_count = 0 + for i, file_path in enumerate(file_paths, 1): + apk_name = Path(file_path).stem + self.update_progress(i, count, f"安装中 ({apk_name})", is_push=True) + if self._push_and_install(file_path, apk_name): + self.log(f"✓ {apk_name}.apk", "SUCCESS") + success_count += 1 + else: + self.log(f"✗ {apk_name}.apk", "ERROR") + + self.run_adb_shell('setprop vecentek.model 0') + self.update_progress(count, count, "安装完成", is_push=True) + self.show_progress(False, is_push=True) + + if success_count == count: + self.log(f"安装完成:全部 {count} 个成功", "SUCCESS") + messagebox.showinfo("安装完成", f"成功安装 {count} 个APK!") + elif success_count > 0: + self.log(f"安装完成:{success_count}/{count} 成功", "WARNING") + messagebox.showwarning("部分成功", f"成功: {success_count}\n失败: {count - success_count}") + else: + self.log("安装失败", "ERROR") + messagebox.showerror("安装失败", "所有APK安装失败!") + + threading.Thread(target=install, daemon=True).start() + + def run(self): + """运行程序""" + self.root.mainloop() + +def main(): + """主函数""" + if sys.version_info < (3, 6): + print("错误:需要Python 3.6或更高版本") + sys.exit(1) + + try: + app = ADKAPKGUI() + app.run() + except Exception as e: + print(f"启动失败: {e}") + import traceback + traceback.print_exc() + messagebox.showerror("错误", f"程序启动失败: {e}") + +if __name__ == "__main__": + main() diff --git a/app-yidong.py b/app-yidong.py new file mode 100644 index 0000000..4d86cf4 --- /dev/null +++ b/app-yidong.py @@ -0,0 +1,1123 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import subprocess +import json +import threading +import tkinter as tk +from tkinter import ttk, scrolledtext, filedialog, messagebox +from pathlib import Path +from urllib.request import urlopen, Request +from urllib.error import URLError, HTTPError +from datetime import datetime +import zipfile +try: + import pyzipper +except ImportError: + pyzipper = None +import shutil +import time + +class ADKAPKGUI: + def __init__(self): + self.root = tk.Tk() + self.root.title("长安逸动语言刷入工具") + self.root.geometry("650x550") + self.root.resizable(True, True) + + # 设置颜色主题 + self.colors = { + 'bg_dark': '#1e1e2e', + 'bg_light': '#2a2a3e', + 'accent': '#6c5ce7', + 'accent_hover': '#5b4bc4', + 'success': '#00b894', + 'error': '#d63031', + 'warning': '#fdcb6e', + 'info': '#0984e3', + 'text': '#dfe6e9', + 'text_secondary': '#b2bec3', + 'border': '#3d3d5e' + } + + self.base_dir = Path(sys.executable).parent if getattr(sys, 'frozen', False) else Path(__file__).parent + if getattr(sys, 'frozen', False): + self.adb = str(Path(sys._MEIPASS) / 'adb.exe') + self.sz = str(Path(sys._MEIPASS) / '7za.exe') + else: + self.adb = 'adb' + self.sz = str(self.base_dir / '7za.exe') + self.package_file = self.base_dir / "package.bin" + self.extract_password = None + self.apps_dir = None + self.temp_dir = None + self.api_url = "https://api.changan.softwindy.cn/api/authorizations/auth-check" + self.vin = None + self.device_connected = False + self._refreshing = False # 防止并发刷新 + + # 设置样式 + self.setup_styles() + self.setup_ui() + self.center_window() + + # 检查环境 + self.check_environment() + + # 启动设备状态监控 + self.start_device_monitor() + + def setup_styles(self): + """设置自定义样式""" + style = ttk.Style() + style.theme_use('clam') + + # 配置主颜色 + style.configure('TFrame', background=self.colors['bg_dark']) + style.configure('TLabel', background=self.colors['bg_dark'], foreground=self.colors['text']) + style.configure('TLabelframe', background=self.colors['bg_dark'], foreground=self.colors['text']) + style.configure('TLabelframe.Label', background=self.colors['bg_dark'], foreground=self.colors['accent']) + + # 配置进度条 + style.configure('TProgressbar', + background=self.colors['accent'], + troughcolor=self.colors['bg_light'], + borderwidth=0) + + def setup_ui(self): + """设置UI界面""" + # 配置根窗口 + self.root.configure(bg=self.colors['bg_dark']) + + # 创建主框架 + main_frame = tk.Frame(self.root, bg=self.colors['bg_dark']) + main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 顶部标题栏 + title_frame = tk.Frame(main_frame, bg=self.colors['bg_dark'], height=65) + title_frame.pack(fill=tk.X, pady=(0, 10)) + title_frame.pack_propagate(False) + + # 标题 + title_label = tk.Label(title_frame, + text="🚀 长安逸动多语言刷机", + font=('Microsoft YaHei', 18, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + title_label.pack() + + subtitle_label = tk.Label(title_frame, + text="@港泊智行 - 出口改装一站式服务", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + subtitle_label.pack() + + # 按钮区域(两排,每排5个) + button_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + button_frame.pack(fill=tk.X, pady=(0, 10), padx=5) + + # 按钮样式参数 + btn_params = { + 'font': ('Microsoft YaHei', 9), + 'fg': 'white', + 'relief': tk.FLAT, + 'cursor': 'hand2', + 'height': 1, + 'width': 14 + } + + # 第一排按钮 + row1_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + row1_frame.pack(pady=(8, 4)) + + self.btn_push = tk.Button(row1_frame, text="📦 刷入语言包", + command=self.push_all_apks, + bg=self.colors['accent'], + **btn_params) + self.btn_push.pack(side=tk.LEFT, padx=4) + + self.btn_install_all = tk.Button(row1_frame, text="📱 批量安装", + command=self.install_all_apks, + bg=self.colors['accent'], + **btn_params) + self.btn_install_all.pack(side=tk.LEFT, padx=4) + + self.btn_install_single = tk.Button(row1_frame, text="🎯 单个安装", + command=self.install_single_apk, + bg=self.colors['accent'], + **btn_params) + self.btn_install_single.pack(side=tk.LEFT, padx=4) + + self.btn_language = tk.Button(row1_frame, text="🌐 语言设置", + command=self.open_language_quick_set, + bg=self.colors['accent'], + **btn_params) + self.btn_language.pack(side=tk.LEFT, padx=4) + + self.btn_settings = tk.Button(row1_frame, text="⚙️ 安卓设置", + command=self.open_android_settings, + bg=self.colors['accent'], + **btn_params) + self.btn_settings.pack(side=tk.LEFT, padx=4) + + # 第二排按钮 + row2_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + row2_frame.pack(pady=(4, 8)) + + self.btn_timezone = tk.Button(row2_frame, text="⏰ 时区设置", + command=self.open_timezone_settings, + bg=self.colors['accent'], + **btn_params) + self.btn_timezone.pack(side=tk.LEFT, padx=4) + + self.btn_reboot = tk.Button(row2_frame, text="🔄 重启设备", + command=self.reboot_device, + bg=self.colors['warning'], + **btn_params) + self.btn_reboot.pack(side=tk.LEFT, padx=4) + + self.btn_clear = tk.Button(row2_frame, text="🗑 清空日志", + command=self.clear_log, + bg=self.colors['info'], + **btn_params) + self.btn_clear.pack(side=tk.LEFT, padx=4) + + self.btn_exit = tk.Button(row2_frame, text="❌ 禁用升级", + command=self.on_disable_upgrade, + bg=self.colors['error'], + **btn_params) + self.btn_exit.pack(side=tk.LEFT, padx=4) + + # 工厂模式提示 + factory_hint_frame = tk.Frame(main_frame, bg=self.colors['bg_dark']) + factory_hint_frame.pack(fill=tk.X, pady=(0, 3)) + tk.Label(factory_hint_frame, text="🔧 工厂模式:拨号 *#*#888 ,密码:369875", + font=('Microsoft YaHei', 8), + fg=self.colors['warning'], + bg=self.colors['bg_dark']).pack(side=tk.LEFT, padx=2) + + # 设备状态栏(横条) + status_bar_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + status_bar_frame.pack(fill=tk.X, pady=(0, 5)) + + # 状态指示器 + status_indicator_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + status_indicator_frame.pack(side=tk.LEFT, padx=10, pady=5) + + self.status_indicator = tk.Canvas(status_indicator_frame, width=10, height=10, + bg=self.colors['bg_light'], highlightthickness=0) + self.status_indicator.pack(side=tk.LEFT) + self.status_dot = self.status_indicator.create_oval(2, 2, 8, 8, fill='#636e72') + + tk.Label(status_indicator_frame, text="设备:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT, padx=(5, 3)) + + self.device_status_label = tk.Label(status_indicator_frame, text="未检测", + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light']) + self.device_status_label.pack(side=tk.LEFT) + + # VIN信息 + vin_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + vin_frame.pack(side=tk.LEFT, padx=20, pady=5) + tk.Label(vin_frame, text="VIN码:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT) + self.vin_label = tk.Label(vin_frame, text="未获取", + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light']) + self.vin_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 授权状态 + auth_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + auth_frame.pack(side=tk.LEFT, padx=20, pady=5) + tk.Label(auth_frame, text="授权:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT) + self.auth_label = tk.Label(auth_frame, text="未验证", + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light']) + self.auth_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 刷新按钮 + refresh_btn = tk.Button(status_bar_frame, text="🔄 检查", + command=lambda: self.refresh_device_status(force=True), + font=('Microsoft YaHei', 8), + fg=self.colors['accent'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + refresh_btn.pack(side=tk.RIGHT, padx=10, pady=5) + + # 解压进度条框架 + progress_frame = tk.Frame(main_frame, bg=self.colors['bg_dark']) + progress_frame.pack(fill=tk.X, pady=(5, 5)) + + self.progress_label = tk.Label(progress_frame, text="", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + self.progress_label.pack() + + self.progress = ttk.Progressbar(progress_frame, mode='determinate', style='TProgressbar') + self.progress.pack(fill=tk.X, pady=(2, 0)) + + # 推送进度条 + self.push_progress_label = tk.Label(progress_frame, text="", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + + self.push_progress = ttk.Progressbar(progress_frame, mode='determinate', style='TProgressbar') + + # 日志区域(下方) + log_card = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + log_card.pack(fill=tk.BOTH, expand=True, pady=(5, 0)) + + # 日志标题栏 + log_title_frame = tk.Frame(log_card, bg=self.colors['bg_dark'], height=30) + log_title_frame.pack(fill=tk.X) + log_title_frame.pack_propagate(False) + + tk.Label(log_title_frame, text="📋 运行日志", + font=('Microsoft YaHei', 10, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']).pack(side=tk.LEFT, padx=10) + + # 日志文本框 + text_frame = tk.Frame(log_card, bg=self.colors['bg_light']) + text_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + self.log_text = scrolledtext.ScrolledText(text_frame, + height=12, + wrap=tk.WORD, + font=('Consolas', 9), + bg='#2d2d3d', + fg='#e0e0e0', + insertbackground='white', + relief=tk.FLAT, + borderwidth=0) + self.log_text.pack(fill=tk.BOTH, expand=True) + + # 配置日志颜色标签 + self.log_text.tag_config('INFO', foreground='#74b9ff') + self.log_text.tag_config('SUCCESS', foreground='#55efc4') + self.log_text.tag_config('ERROR', foreground='#ff7675') + self.log_text.tag_config('WARNING', foreground='#ffeaa7') + self.log_text.tag_config('CMD', foreground='#a29bfe') + + # 底部状态栏 + bottom_status = tk.Frame(main_frame, bg=self.colors['bg_light'], height=22) + bottom_status.pack(fill=tk.X, pady=(5, 0)) + bottom_status.pack_propagate(False) + + self.status_text = tk.Label(bottom_status, text="就绪", + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light']) + self.status_text.pack(side=tk.LEFT, padx=10) + + # 绑定悬停效果 + self.bind_hover_effects() + + def bind_hover_effects(self): + """绑定按钮悬停效果""" + buttons = [self.btn_push, self.btn_install_all, self.btn_install_single, + self.btn_language, self.btn_timezone, self.btn_settings, + self.btn_reboot, self.btn_clear, self.btn_exit] + + for btn in buttons: + original_bg = btn.cget('bg') + def on_enter(e, btn=btn, bg=original_bg): + btn.config(bg=self.lighten_color(bg)) + def on_leave(e, btn=btn, bg=original_bg): + btn.config(bg=bg) + btn.bind('', on_enter) + btn.bind('', on_leave) + + def center_window(self): + """将窗口居中显示在屏幕上""" + self.root.update_idletasks() + screen_w = self.root.winfo_screenwidth() + screen_h = self.root.winfo_screenheight() + win_w = self.root.winfo_reqwidth() + win_h = self.root.winfo_reqheight() + x = (screen_w - win_w) // 2 + y = (screen_h - win_h) // 2 + self.root.geometry(f"+{x}+{y}") + + def lighten_color(self, color): + """调亮颜色""" + if color == self.colors['accent']: + return self.colors['accent_hover'] + elif color == self.colors['warning']: + return '#feca57' + elif color == self.colors['info']: + return '#0984e3' + elif color == self.colors['error']: + return '#e17055' + elif color == self.colors['success']: + return '#00a884' + return color + + def run_on_ui_thread(self, func, *args, **kwargs): + """将函数调度到主线程执行,确保线程安全""" + self.root.after(0, func, *args, **kwargs) + + def _log_impl(self, message, level="INFO"): + """日志写入的实际实现(必须在主线程调用)""" + timestamp = datetime.now().strftime("%H:%M:%S") + log_entry = f"[{timestamp}] [{level}] {message}\n" + self.log_text.insert(tk.END, log_entry, level) + self.log_text.see(tk.END) + + def log(self, message, level="INFO"): + """添加日志(线程安全)""" + self.run_on_ui_thread(self._log_impl, message, level) + + def clear_log(self): + """清空日志""" + self.log_text.delete(1.0, tk.END) + self.log("日志已清空", "INFO") + + def _show_progress_impl(self, show=True, is_push=False): + """显示/隐藏进度条的实际实现(必须在主线程调用)""" + if is_push: + if show: + self.push_progress_label.pack() + self.push_progress.pack(fill=tk.X, pady=(2, 0)) + self.push_progress['value'] = 0 + else: + self.push_progress_label.pack_forget() + self.push_progress.pack_forget() + else: + if show: + self.progress_label.pack() + self.progress.pack(fill=tk.X, pady=(2, 0)) + self.progress['value'] = 0 + else: + self.progress_label.pack_forget() + self.progress.pack_forget() + + def show_progress(self, show=True, is_push=False): + """显示/隐藏进度条(线程安全)""" + self.run_on_ui_thread(self._show_progress_impl, show, is_push) + + def _update_progress_impl(self, value, max_value=100, label="", is_push=False): + """更新进度条的实际实现(必须在主线程调用)""" + if is_push: + percent = (value / max_value) * 100 + self.push_progress['value'] = percent + self.push_progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)") + else: + percent = (value / max_value) * 100 + self.progress['value'] = percent + self.progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)") + self.root.update_idletasks() + + def update_progress(self, value, max_value=100, label="", is_push=False): + """更新进度条(线程安全)""" + self.run_on_ui_thread(self._update_progress_impl, value, max_value, label, is_push) + + def update_device_status(self, connected, vin=None, authorized=False): + """更新设备状态显示(线程安全:立即设状态变量,UI走主线程)""" + self.device_connected = connected + if vin is not None: + self.vin = vin + self.run_on_ui_thread(self._update_device_status_impl, connected, vin, authorized) + + def _update_device_status_impl(self, connected, vin, authorized): + """设备状态UI更新的实际实现(必须在主线程调用)""" + if connected: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['success']) + self.device_status_label.config(text="已连接", fg=self.colors['success']) + if vin: + self.vin_label.config(text=vin, fg=self.colors['success']) + if authorized: + self.auth_label.config(text="已授权", fg=self.colors['success']) + else: + self.auth_label.config(text="未授权", fg=self.colors['error']) + else: + self.vin_label.config(text="未获取", fg=self.colors['error']) + self.auth_label.config(text="未验证", fg=self.colors['error']) + else: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['error']) + self.device_status_label.config(text="未连接", fg=self.colors['error']) + self.vin_label.config(text="未获取", fg=self.colors['error']) + self.auth_label.config(text="未验证", fg=self.colors['error']) + + def check_device_connection(self): + """检查设备是否连接""" + if not self.device_connected: + messagebox.showwarning("设备未连接", "请先连接设备并点击「检查」按钮刷新状态!") + return False + return True + + def start_device_monitor(self): + """启动设备状态监控(每5秒检查一次)""" + def monitor(): + while True: + try: + result = subprocess.run('adb -d devices', shell=True, capture_output=True, text=True) + lines = result.stdout.strip().split('\n') + devices = [line for line in lines[1:] if line.strip() and 'device' in line and 'offline' not in line] + + if devices and not self.device_connected and not self._refreshing: + # 设备新连接,刷新状态 + self.refresh_device_status() + elif not devices and self.device_connected: + # 设备断开连接 + self.update_device_status(False) + self.log("设备已断开连接", "WARNING") + + time.sleep(5) + except: + time.sleep(5) + + threading.Thread(target=monitor, daemon=True).start() + + # ============================================================ + # 核心:adb shell 自动密码输入 + # ============================================================ + + def run_adb_shell(self, shell_command): + """执行 adb shell 命令,自动静默输入设备密码 adb36987。 + 静默执行,不显示 adb 原始输出,仅返回结果。""" + try: + proc = subprocess.Popen( + f'adb -d shell {shell_command}', + shell=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + stdout, stderr = proc.communicate(input='adb36987\n', timeout=15) + + # 过滤密码提示行 + output_lines = [] + for line in stdout.split('\n'): + stripped = line.strip() + if 'please input verify password' in stripped.lower(): + continue + if stripped == 'verify success!': + continue + output_lines.append(line) + output = '\n'.join(output_lines).strip() + + if proc.returncode == 0: + return True, output + else: + return False, stderr.strip() + + except subprocess.TimeoutExpired: + proc.kill() + proc.communicate() + return False, "命令超时" + except Exception as e: + return False, str(e) + + # ============================================================ + # 原始 adb 命令(用于 adb push / adb install 等不需要 shell 的操作) + # ============================================================ + + def run_adb_command(self, command): + """执行原始 adb 命令(adb push / adb install 等,无需 shell 密码)。 + 静默执行,不显示 adb 原始输出,仅返回结果。""" + try: + result = subprocess.run(command, shell=True, capture_output=True, text=True, encoding='utf-8') + if result.returncode == 0: + return True, result.stdout.strip() + else: + return False, result.stderr.strip() + except Exception as e: + return False, str(e) + + def check_package_extracted(self): + """检查语言包是否已解压""" + has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0 + return has_app + + def extract_package_silent(self): + """静默解压语言包(带进度)—— 逸动版仅处理 app 目录""" + if not self.package_file.exists(): + self.log(f"错误:未找到资源包", "ERROR") + return False + + try: + # 使用用户目录,无需管理员权限 + local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) + hidden_path = Path(local_appdata) / ".cache" / "system" / ".android" + hidden_path.mkdir(parents=True, exist_ok=True) + + self.temp_dir = hidden_path / "apps_cache_yidong" + + # 如果已存在,先清理 + if self.temp_dir.exists(): + shutil.rmtree(self.temp_dir, ignore_errors=True) + time.sleep(0.5) + + self.temp_dir.mkdir(parents=True, exist_ok=True) + + # 设置隐藏属性(Windows) + if sys.platform == 'win32': + subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True) + subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True) + + self.log(f"正在准备资源,耗时较长,请耐心等待...", "INFO") + + # 使用 7za 高速解压 + self.update_progress(0, 1, "资源加载中...") + result = subprocess.run( + [self.sz, 'x', str(self.package_file), f'-p{self.extract_password}', f'-o{self.temp_dir}', '-y'], + capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + if result.returncode != 0: + stderr = result.stderr.strip() + if stderr: + self.log(f"解压失败: {stderr[:200]}", "ERROR") + else: + self.log("解压失败,请检查密码是否正确", "ERROR") + return False + self.update_progress(1, 1, "资源加载完成") + + # 查找 app 目录(逸动无 priv-app) + self.apps_dir = None + + app_candidates = list(self.temp_dir.rglob("apps")) + if app_candidates: + self.apps_dir = app_candidates[0] + + if not self.apps_dir: + self.log("警告:未找到 apps 目录", "WARNING") + return False + + return True + + except Exception as e: + self.log(f"资源准备失败", "ERROR") + return False + + def check_environment(self): + """检查环境""" + try: + result = subprocess.run('adb version', shell=True, capture_output=True, text=True) + if result.returncode == 0: + self.refresh_device_status() + if not self.package_file.exists(): + self.log("未找到资源包文件", "WARNING") + else: + self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") + except FileNotFoundError: + self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") + + def refresh_device_status(self, force=False): + """刷新设备状态 —— 逸动版使用 ca.car.vin 获取 VIN""" + # 防止并发刷新(手动点击「检查」时强制忽略锁) + if self._refreshing and not force: + return + self._refreshing = True + + def refresh(): + was_connected = self.device_connected + + # 检查设备连接 + result = subprocess.run('adb -d devices', shell=True, capture_output=True, text=True) + lines = result.stdout.strip().split('\n') + devices = [line for line in lines[1:] if line.strip() and 'device' in line and 'offline' not in line] + + if devices: + if not was_connected: + self.log("设备已连接", "SUCCESS") + + # 获取VIN —— 逸动车型使用 ca.car.vin + success, vin_output = self.run_adb_shell( + 'settings get system ca.car.vin') + vin = vin_output.strip() if success else '' + + if vin: + self.log(f"VIN: {vin}", "INFO") + authorized = self.check_authorization(vin) + self.update_device_status(True, vin, authorized) + else: + self.log("无法获取VIN,请确认设备已进入工厂模式", "WARNING") + self.update_device_status(True, None, False) + else: + if was_connected: + self.log("设备未连接", "WARNING") + self.update_device_status(False) + + self._refreshing = False + + threading.Thread(target=refresh, daemon=True).start() + + def check_authorization(self, vin): + """检查授权""" + self.log("正在验证授权状态...", "INFO") + + try: + url = f"{self.api_url}?vin={vin}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + if data.get('authorized') == True: + self.log("✅ 授权验证通过!", "SUCCESS") + if 'data' in data and 'vehicleName' in data['data']: + self.log(f"车辆名称: {data['data']['vehicleName']}", "INFO") + return True + else: + self.log(f"❌ 授权验证失败", "ERROR") + return False + + except Exception as e: + self.log(f"❌ 授权验证失败", "ERROR") + return False + + def fetch_package_password(self): + """从服务端获取资源包解压密码""" + if not self.vin: + self.log("请先连接adb!", "ERROR") + return False + + try: + pwd_api_url = "https://api.changan.softwindy.cn/api/authorizations/package-key" + url = f"{pwd_api_url}?vin={self.vin}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + if data.get('success') and 'data' in data and 'password' in data['data']: + self.extract_password = data['data']['password'] + return True + else: + self.log(f"数据准备失败: {data.get('message', '未知错误')}", "ERROR") + return False + + except Exception as e: + self.log(f"数据准备失败: {str(e)}", "ERROR") + return False + + def push_single_apk(self, apk_path, apk_name): + """推送单个APK到设备并安装,返回 (成功, 错误信息)""" + temp_apk_path = f"/data/local/tmp/{apk_name}.apk" + + ok, err = self.run_adb_command(f'adb -d push "{apk_path}" {temp_apk_path}') + if not ok: + return False, f"push失败: {err}" + + ok, err = self.run_adb_shell(f'pm install -r {temp_apk_path}') + self.run_adb_shell(f'rm -f {temp_apk_path}') + if not ok: + return False, f"install失败: {err}" + + return True, "" + + def _push_and_install(self, apk_path, apk_name): + """push → pm install → cleanup,供安装类方法复用""" + ok, _ = self.push_single_apk(apk_path, apk_name) + return ok + + def push_all_apks(self): + """推送APK并安装 —— 逸动版仅处理 app 目录,使用 pm install""" + # 检查设备连接(仅 UI 层检查在主线程,其余工作进后台线程) + if not self.check_device_connection(): + return + + if not self.vin: + messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码") + return + + def do_push_all(): + # 验证授权 + if not self.check_authorization(self.vin): + self.run_on_ui_thread(lambda: messagebox.showerror("授权失败", "设备未授权")) + return + + # 获取解压密码 + if not self.extract_password: + if not self.fetch_package_password(): + self.run_on_ui_thread(lambda: messagebox.showerror("错误", "数据准备失败!")) + return + + # 解压 + if not self.check_package_extracted(): + self.log("正在准备资源...", "INFO") + self.show_progress(True, is_push=False) + if not self.extract_package_silent(): + self.show_progress(False, is_push=False) + self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源准备失败!")) + return + self.show_progress(False, is_push=False) + + if not self.apps_dir or not self.apps_dir.exists(): + self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源目录未找到")) + return + + # 开始刷入 + self.show_progress(True, is_push=True) + self.log("开始刷入语言包...", "INFO") + self.run_adb_shell('mkdir -p /data/local/tmp') + self.run_adb_shell('setprop vecentek.model 1') + + all_apks = list(self.apps_dir.glob("*.apk")) + if not all_apks: + self.log("未找到语言包文件", "WARNING") + self.show_progress(False, is_push=True) + return + + total = len(all_apks) + success_count = 0 + for i, apk_path in enumerate(all_apks, 1): + apk_name = apk_path.stem + ok, _ = self.push_single_apk(apk_path, apk_name) + if ok: + self.log(f"安装成功: {apk_name}.apk", "SUCCESS") + success_count += 1 + else: + self.log(f"安装失败: {apk_name}.apk", "ERROR") + self.update_progress(i, total, "正在刷入...", is_push=True) + + self.update_progress(total, total, "刷入完成", is_push=True) + self.run_adb_shell('setprop vecentek.model 0') + + if success_count > 0: + self._enable_overlays() + else: + self.log("语言包刷入失败", "ERROR") + + self.show_progress(False, is_push=True) + + threading.Thread(target=do_push_all, daemon=True).start() + + def _enable_overlays(self): + """启用 overlay 包""" + overlays = [ + "com.android.systemui.overlay", + "com.incall.apps.airconditioner.overlay", + "com.incall.apps.launcher.overlay", + "com.incall.dvr.overlay", + ] + # self.log("正在启用 overlay...", "INFO") + for pkg in overlays: + success, _ = self.run_adb_shell( + f'cmd overlay enable --user current {pkg}') + + def install_all_apks(self): + """批量安装APK — push → pm install → cleanup""" + if not self.check_device_connection(): + return + if not self.vin: + messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码") + return + if not self.check_authorization(self.vin): + messagebox.showerror("授权失败", "设备未授权,无法执行此操作") + return + + # 使用当前目录下的apks文件夹 + apk_dir = self.base_dir / "apks" + + # 检查apk文件夹是否存在 + if not apk_dir.exists(): + messagebox.showerror("错误", "未找到apks文件夹!\n请在程序目录下创建apks文件夹并放入APK文件。") + self.log("未找到apks文件夹", "ERROR") + return + + # 查找所有apk文件 + apk_files = list(apk_dir.glob("*.apk")) + if not apk_files: + messagebox.showerror("错误", "apks文件夹中没有找到APK文件!") + self.log("apk文件夹中没有找到APK文件", "ERROR") + return + + # 询问是否确认安装 + result = messagebox.askyesno("确认安装", + f"找到 {len(apk_files)} 个APK文件\n\n是否开始批量安装?") + if not result: + return + + def install(): + self.show_progress(True, is_push=True) + total = len(apk_files) + self.log(f"开始批量安装 {total} 个APK...", "INFO") + + self.run_adb_shell('setprop vecentek.model 1') + + success_count = 0 + for i, apk_path in enumerate(apk_files, 1): + apk_name = apk_path.stem + self.update_progress(i, total, "安装中...", is_push=True) + if self._push_and_install(apk_path, apk_name): + self.log(f"安装成功: {apk_name}.apk", "SUCCESS") + success_count += 1 + else: + self.log(f"安装失败: {apk_name}.apk", "ERROR") + + self.run_adb_shell('setprop vecentek.model 0') + self.update_progress(total, total, "安装完成", is_push=True) + self.show_progress(False, is_push=True) + + if success_count == total: + messagebox.showinfo("安装完成", f"成功安装 {total} 个APK!") + elif success_count > 0: + messagebox.showwarning("部分成功", f"成功: {success_count}\n失败: {total - success_count}") + messagebox.showwarning("部分成功", f"成功: {success_count}\n失败: {total - success_count}") + else: + self.log("安装失败", "ERROR") + messagebox.showerror("安装失败", "所有APK安装失败!") + + threading.Thread(target=install, daemon=True).start() + + def install_single_apk(self): + """安装单个APK — push → pm install → cleanup""" + if not self.check_device_connection(): + return + if not self.vin: + messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码") + return + if not self.check_authorization(self.vin): + messagebox.showerror("授权失败", "设备未授权,无法执行此操作") + return + + file_path = filedialog.askopenfilename( + title="选择APK文件", + filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + ) + + if not file_path: + return + + def install(): + apk_name = Path(file_path).stem + self.show_progress(True, is_push=True) + self.update_progress(30, 100, "安装中", is_push=True) + + self.run_adb_shell('setprop vecentek.model 1') + success = self._push_and_install(file_path, apk_name) + self.run_adb_shell('setprop vecentek.model 0') + + self.update_progress(100, 100, "完成", is_push=True) + if success: + self.log("安装成功", "SUCCESS") + else: + self.log("安装失败", "ERROR") + self.show_progress(False, is_push=True) + + threading.Thread(target=install, daemon=True).start() + + def open_language_settings(self): + """打开系统语言设置""" + if not self.check_device_connection(): + return + self.run_adb_shell('am start -a android.settings.LOCALE_SETTINGS') + + def open_language_quick_set(self): + """打开快捷语言设置弹窗""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 创建弹窗 + popup = tk.Toplevel(self.root) + popup.title("快捷语言设置") + popup.geometry("520x320") + popup.configure(bg=self.colors['bg_dark']) + popup.resizable(False, False) + + # 居中显示 + popup.update_idletasks() + x = self.root.winfo_x() + (self.root.winfo_width() - 520) // 2 + y = self.root.winfo_y() + (self.root.winfo_height() - 320) // 2 + popup.geometry(f"+{x}+{y}") + popup.transient(self.root) + popup.grab_set() + + # 标题 + header = tk.Label(popup, text="选择目标语言", + font=('Microsoft YaHei', 13, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + header.pack(pady=(15, 10)) + + hint = tk.Label(popup, text="点击按钮即可将系统语言切换为对应语言,重启后生效", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + hint.pack(pady=(0, 12)) + + # 语言列表:(显示名, locale_code) + languages = [ + ("🇨🇳 中文", "zh-CN"), + ("英 English", "en-US"), + ("俄 Русский", "ru-RU"), + ("法 Français", "fr-FR"), + ("西 Español", "es-ES"), + ("葡 Português", "pt-BR"), + ("意 Italiano", "it-IT"), + ("阿 العربية", "ar-SA"), + ] + + # 创建按钮容器 + btn_frame = tk.Frame(popup, bg=self.colors['bg_dark']) + btn_frame.pack(pady=(0, 10)) + + btn_colors = [ + self.colors['accent'], self.colors['info'], + self.colors['success'], self.colors['warning'], + '#e17055', '#00b894', + '#6c5ce7', '#0984e3', + ] + + for i, (label, locale) in enumerate(languages): + row = i // 4 + col = i % 4 + + def make_cmd(loc=locale, lbl=label): + return lambda: self._quick_set_language(loc, lbl, popup) + + btn = tk.Button(btn_frame, text=label, + command=make_cmd(), + font=('Microsoft YaHei', 10), + fg='white', + bg=btn_colors[i], + relief=tk.FLAT, + cursor='hand2', + width=12, height=2) + btn.grid(row=row, column=col, padx=5, pady=5) + + # 底部分隔 + 打开系统设置入口 + sep = tk.Frame(popup, bg=self.colors['border'], height=1) + sep.pack(fill=tk.X, padx=20, pady=(8, 6)) + + sys_btn = tk.Button(popup, text="⚙️ 打开系统语言设置(手动选择)", + command=lambda: self._open_sys_and_close(popup), + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + sys_btn.pack(pady=(0, 10)) + + def _quick_set_language(self, locale_code, language_name, popup): + """执行快捷语言设置""" + popup.destroy() + + def do_set(): + self.log(f"正在设置系统语言为: {language_name} ({locale_code})", "INFO") + success, output = self.run_adb_shell( + f'settings put system system_locales {locale_code}' + ) + + if success: + self.log(f"✓ 语言已设置为 {language_name}", "SUCCESS") + messagebox.showinfo( + "设置成功", + f"系统语言已设置为 {language_name}\n\n⚠️ 请重启设备使其生效。" + ) + else: + self.log(f"✗ 语言设置失败: {output}", "ERROR") + messagebox.showerror("设置失败", f"语言设置失败!\n\n{output}") + + threading.Thread(target=do_set, daemon=True).start() + + def _open_sys_and_close(self, popup): + """关闭弹窗并打开系统语言设置""" + popup.destroy() + self.open_language_settings() + + def open_timezone_settings(self): + """打开时区设置""" + if not self.check_device_connection(): + return + self.run_adb_shell('am start -a android.settings.TIMEZONE_SETTINGS') + + def open_android_settings(self): + """打开安卓原生设置""" + if not self.check_device_connection(): + return + self.run_adb_shell('am start -a android.settings.SETTINGS') + + def reboot_device(self): + """重启设备""" + if not self.check_device_connection(): + return + if messagebox.askyesno("确认重启", "确定要重启设备吗?"): + proc = subprocess.Popen(f'{self.adb} -d shell reboot', shell=True, + stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + try: + proc.stdin.write(b'adb36987\n') + proc.stdin.flush() + proc.stdin.close() + except: + pass + self.log("设备正在重启...", "INFO") + self.update_device_status(False) + + def on_disable_upgrade(self): + """禁用系统升级""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 弹窗确认 + result = messagebox.askyesno( + "确认禁用升级", + "⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n" + "是否确定要禁用系统升级应用?\n\n" + "禁用命令:\n" + "adb shell pm disable-user --user 0 com.incall.apps.softmanager" + ) + + if not result: + self.log("已取消禁用升级操作", "INFO") + return + + def disable(): + self.show_progress(True, is_push=False) + success, output = self.run_adb_shell( + 'pm disable-user --user 0 com.incall.apps.softmanager') + + if success: + self.log("系统升级已禁用", "SUCCESS") + messagebox.showinfo("成功", "系统升级已成功禁用!") + else: + self.log("禁用系统升级失败", "ERROR") + messagebox.showerror("错误", f"禁用失败:{output}") + + self.show_progress(False, is_push=False) + + threading.Thread(target=disable, daemon=True).start() + + def run(self): + """运行程序""" + self.root.mainloop() + +def main(): + """主函数""" + if sys.version_info < (3, 6): + print("错误:需要Python 3.6或更高版本") + sys.exit(1) + + try: + app = ADKAPKGUI() + app.run() + except Exception as e: + print(f"启动失败: {e}") + import traceback + traceback.print_exc() + messagebox.showerror("错误", f"程序启动失败: {e}") + +if __name__ == "__main__": + main() diff --git a/app.ico b/app.ico new file mode 100644 index 0000000..c1e16ed Binary files /dev/null and b/app.ico differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..c5fbcbe --- /dev/null +++ b/app.py @@ -0,0 +1,1245 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import subprocess +import json +import threading +import tkinter as tk +from tkinter import ttk, scrolledtext, filedialog, messagebox +from pathlib import Path +from urllib.request import urlopen, Request +from urllib.error import URLError, HTTPError +from datetime import datetime +import zipfile +try: + import pyzipper +except ImportError: + pyzipper = None +import shutil +import time + +class ADKAPKGUI: + def __init__(self): + self.root = tk.Tk() + self.root.title("长安语言安装工具") + self.root.geometry("650x640") + self.root.resizable(True, True) + + # 设置颜色主题 + self.colors = { + 'bg_dark': '#1e1e2e', + 'bg_light': '#2a2a3e', + 'accent': '#6c5ce7', + 'accent_hover': '#5b4bc4', + 'success': '#00b894', + 'error': '#d63031', + 'warning': '#fdcb6e', + 'info': '#0984e3', + 'text': '#dfe6e9', + 'text_secondary': '#b2bec3', + 'border': '#3d3d5e' + } + + # 从 exe/py 所在目录查找资源文件 + self.base_dir = Path(sys.executable).parent if getattr(sys, 'frozen', False) else Path(__file__).parent + if getattr(sys, 'frozen', False): + self.adb = str(Path(sys._MEIPASS) / 'adb.exe') + self.sz = str(Path(sys._MEIPASS) / '7za.exe') + else: + self.adb = 'adb' + self.sz = str(self.base_dir / '7za.exe') + self.package_file = self.base_dir / "package.bin" + self.extract_password = None + self.apps_dir = None + self.priv_apps_dir = None + self.temp_dir = None + self.api_url = "https://api.changan.softwindy.cn/api/authorizations/auth-check" + self.vin = None + self.device_connected = False + self._refreshing = False # 防止并发刷新 + self.debug_mode = False # 调试模式 + + # 设置样式 + self.setup_styles() + self.setup_ui() + self.center_window() + + # 检查环境 + self.check_environment() + + # 启动设备状态监控 + self.start_device_monitor() + + def setup_styles(self): + """设置自定义样式""" + style = ttk.Style() + style.theme_use('clam') + + # 配置主颜色 + style.configure('TFrame', background=self.colors['bg_dark']) + style.configure('TLabel', background=self.colors['bg_dark'], foreground=self.colors['text']) + style.configure('TLabelframe', background=self.colors['bg_dark'], foreground=self.colors['text']) + style.configure('TLabelframe.Label', background=self.colors['bg_dark'], foreground=self.colors['accent']) + + # 配置进度条 + style.configure('TProgressbar', + background=self.colors['accent'], + troughcolor=self.colors['bg_light'], + borderwidth=0) + + def setup_ui(self): + """设置UI界面""" + # 配置根窗口 + self.root.configure(bg=self.colors['bg_dark']) + + # 创建主框架 + main_frame = tk.Frame(self.root, bg=self.colors['bg_dark']) + main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 顶部标题栏 + title_frame = tk.Frame(main_frame, bg=self.colors['bg_dark'], height=65) + title_frame.pack(fill=tk.X, pady=(0, 10)) + title_frame.pack_propagate(False) + + # 标题 + title_label = tk.Label(title_frame, + text="🚀 适用于启源Q07多语言安装", + font=('Microsoft YaHei', 18, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + title_label.pack() + + subtitle_label = tk.Label(title_frame, + text="宜宾科宜科技有限公司 - 智能设备管理平台", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + subtitle_label.pack() + + # 按钮区域(两排,每排5个) + button_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + button_frame.pack(fill=tk.X, pady=(0, 10), padx=5) + + # 按钮样式参数 + btn_params = { + 'font': ('Microsoft YaHei', 9), + 'fg': 'white', + 'relief': tk.FLAT, + 'cursor': 'hand2', + 'height': 1, + 'width': 14 + } + + # 第一排按钮 + row1_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + row1_frame.pack(pady=(8, 4)) + + self.btn_root = tk.Button(row1_frame, text="🔓 获取权限", + command=self.get_root_permission, + bg=self.colors['success'], + **btn_params) + self.btn_root.pack(side=tk.LEFT, padx=4) + + self.btn_push = tk.Button(row1_frame, text="📦 刷入语言包", + command=self.push_all_apks, + bg=self.colors['accent'], + **btn_params) + self.btn_push.pack(side=tk.LEFT, padx=4) + + self.btn_install_all = tk.Button(row1_frame, text="📱 安装App", + command=self.install_apps, + bg=self.colors['accent'], + **btn_params) + self.btn_install_all.pack(side=tk.LEFT, padx=4) + + self.btn_language = tk.Button(row1_frame, text="🌐 语言设置", + command=self.open_language_quick_set, + bg=self.colors['accent'], + **btn_params) + self.btn_language.pack(side=tk.LEFT, padx=4) + + # 第二排按钮 + row2_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + row2_frame.pack(pady=(4, 8)) + + self.btn_timezone = tk.Button(row2_frame, text="⏰ 时区设置", + command=self.open_timezone_settings, + bg=self.colors['accent'], + **btn_params) + self.btn_timezone.pack(side=tk.LEFT, padx=4) + + self.btn_settings = tk.Button(row2_frame, text="⚙️ 安卓设置", + command=self.open_android_settings, + bg=self.colors['accent'], + **btn_params) + self.btn_settings.pack(side=tk.LEFT, padx=4) + + self.btn_reboot = tk.Button(row2_frame, text="🔄 重启设备", + command=self.reboot_device, + bg=self.colors['warning'], + **btn_params) + self.btn_reboot.pack(side=tk.LEFT, padx=4) + + self.btn_exit = tk.Button(row2_frame, text="❌ 禁用升级", + command=self.on_disable_upgrade, + bg=self.colors['error'], + **btn_params) + self.btn_exit.pack(side=tk.LEFT, padx=4) + + # 设备状态栏(横条) + status_bar_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + status_bar_frame.pack(fill=tk.X, pady=(0, 5)) + + # 状态指示器 + status_indicator_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + status_indicator_frame.pack(side=tk.LEFT, padx=10, pady=5) + + self.status_indicator = tk.Canvas(status_indicator_frame, width=10, height=10, + bg=self.colors['bg_light'], highlightthickness=0) + self.status_indicator.pack(side=tk.LEFT) + self.status_dot = self.status_indicator.create_oval(2, 2, 8, 8, fill='#636e72') + + tk.Label(status_indicator_frame, text="设备:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT, padx=(5, 3)) + + self.device_status_label = tk.Label(status_indicator_frame, text="未检测", + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + anchor='w', width=4) + self.device_status_label.pack(side=tk.LEFT) + + # VIN信息 + vin_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + vin_frame.pack(side=tk.LEFT, padx=20, pady=5) + tk.Label(vin_frame, text="VIN码:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT) + self.vin_label = tk.Label(vin_frame, text="未获取", + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + anchor='w', width=17) + self.vin_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 授权状态 + auth_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + auth_frame.pack(side=tk.LEFT, padx=20, pady=5) + tk.Label(auth_frame, text="授权:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']).pack(side=tk.LEFT) + self.auth_label = tk.Label(auth_frame, text="未验证", + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + anchor='w', width=4) + self.auth_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 刷新按钮 + refresh_btn = tk.Button(status_bar_frame, text="🔄 检查", + command=self.refresh_device_status, + font=('Microsoft YaHei', 8), + fg=self.colors['accent'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + refresh_btn.pack(side=tk.RIGHT, padx=10, pady=5) + + # 提示信息区域(设备状态下方) + tips_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + tips_frame.pack(fill=tk.X, pady=(5, 5), padx=5) + + tips = [ + "1. 安装语言过程中请保持车辆和电脑的电量充足,不可中途停止。", + "2. 获取权限以后,车辆自动重启以后再进入语言刷入。", + "3. 部分语言需要重启后生效,可以一切工作完成以后再重启。", + ] + + for i, tip in enumerate(tips): + tip_row = tk.Frame(tips_frame, bg=self.colors['bg_light']) + tip_row.pack(fill=tk.X, padx=10, pady=(5 if i == 0 else 0, 5 if i == len(tips) - 1 else 0)) + tk.Label(tip_row, text=tip, + font=('Microsoft YaHei', 9), + fg=self.colors['warning'], + bg=self.colors['bg_light'], + wraplength=600, + justify=tk.LEFT).pack(side=tk.LEFT) + + # 解压进度条框架 + progress_frame = tk.Frame(main_frame, bg=self.colors['bg_dark']) + progress_frame.pack(fill=tk.X, pady=(5, 5)) + + self.progress_label = tk.Label(progress_frame, text="", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + self.progress_label.pack() + + self.progress = ttk.Progressbar(progress_frame, mode='determinate', style='TProgressbar') + self.progress.pack(fill=tk.X, pady=(2, 0)) + + # 推送进度条 + self.push_progress_label = tk.Label(progress_frame, text="", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + + self.push_progress = ttk.Progressbar(progress_frame, mode='determinate', style='TProgressbar') + + # 日志区域(下方) + log_card = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + log_card.pack(fill=tk.BOTH, expand=True, pady=(5, 0)) + + # 日志标题栏 + log_title_frame = tk.Frame(log_card, bg=self.colors['bg_dark'], height=30) + log_title_frame.pack(fill=tk.X) + log_title_frame.pack_propagate(False) + + tk.Label(log_title_frame, text="📋 运行日志", + font=('Microsoft YaHei', 10, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']).pack(side=tk.LEFT, padx=10) + + self.btn_clear = tk.Button(log_title_frame, text="🗑 清空日志", + command=self.clear_log, + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark'], + relief=tk.FLAT, + cursor='hand2') + self.btn_clear.pack(side=tk.RIGHT, padx=10) + + # 日志文本框 + text_frame = tk.Frame(log_card, bg=self.colors['bg_light']) + text_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + self.log_text = scrolledtext.ScrolledText(text_frame, + height=12, + wrap=tk.WORD, + font=('Consolas', 9), + bg='#2d2d3d', + fg='#e0e0e0', + insertbackground='white', + relief=tk.FLAT, + borderwidth=0) + self.log_text.pack(fill=tk.BOTH, expand=True) + + # 配置日志颜色标签 + self.log_text.tag_config('INFO', foreground='#74b9ff') + self.log_text.tag_config('SUCCESS', foreground='#55efc4') + self.log_text.tag_config('ERROR', foreground='#ff7675') + self.log_text.tag_config('WARNING', foreground='#ffeaa7') + self.log_text.tag_config('CMD', foreground='#a29bfe') + + # 底部状态栏 + bottom_status = tk.Frame(main_frame, bg=self.colors['bg_light'], height=22) + bottom_status.pack(fill=tk.X, pady=(5, 0)) + bottom_status.pack_propagate(False) + + self.status_text = tk.Label(bottom_status, text="就绪", + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light']) + self.status_text.pack(side=tk.LEFT, padx=10) + + # 调试模式快捷键 + self.root.bind('', self._toggle_debug) + + # 绑定悬停效果 + self.bind_hover_effects() + + def bind_hover_effects(self): + """绑定按钮悬停效果""" + buttons = [self.btn_root, self.btn_push, self.btn_install_all, + self.btn_language, self.btn_timezone, self.btn_settings, + self.btn_reboot, self.btn_clear, self.btn_exit] + + for btn in buttons: + original_bg = btn.cget('bg') + def on_enter(e, btn=btn, bg=original_bg): + btn.config(bg=self.lighten_color(bg)) + def on_leave(e, btn=btn, bg=original_bg): + btn.config(bg=bg) + btn.bind('', on_enter) + btn.bind('', on_leave) + + def lighten_color(self, color): + """调亮颜色""" + if color == self.colors['accent']: + return self.colors['accent_hover'] + elif color == self.colors['warning']: + return '#feca57' + elif color == self.colors['info']: + return '#0984e3' + elif color == self.colors['error']: + return '#e17055' + elif color == self.colors['success']: + return '#00a884' + return color + + def center_window(self): + """将窗口居中显示在屏幕上""" + self.root.update_idletasks() + screen_w = self.root.winfo_screenwidth() + screen_h = self.root.winfo_screenheight() + win_w = self.root.winfo_reqwidth() + win_h = self.root.winfo_reqheight() + x = (screen_w - win_w) // 2 + y = (screen_h - win_h) // 2 + self.root.geometry(f"+{x}+{y}") + + def run_on_ui_thread(self, func, *args, **kwargs): + """将函数调度到主线程执行,确保线程安全""" + self.root.after(0, func, *args, **kwargs) + + def _log_impl(self, message, level="INFO"): + """日志写入的实际实现(必须在主线程调用)""" + timestamp = datetime.now().strftime("%H:%M:%S") + log_entry = f"[{timestamp}] [{level}] {message}\n" + self.log_text.insert(tk.END, log_entry, level) + self.log_text.see(tk.END) + + def log(self, message, level="INFO"): + """添加日志(线程安全)""" + self.run_on_ui_thread(self._log_impl, message, level) + + def clear_log(self): + """清空日志""" + self.log_text.delete(1.0, tk.END) + self.log("日志已清空", "INFO") + + def _show_progress_impl(self, show=True, is_push=False): + """显示/隐藏进度条的实际实现(必须在主线程调用)""" + if is_push: + if show: + self.push_progress_label.pack() + self.push_progress.pack(fill=tk.X, pady=(2, 0)) + self.push_progress['value'] = 0 + else: + self.push_progress_label.pack_forget() + self.push_progress.pack_forget() + else: + if show: + self.progress_label.pack() + self.progress.pack(fill=tk.X, pady=(2, 0)) + self.progress['value'] = 0 + else: + self.progress_label.pack_forget() + self.progress.pack_forget() + + def show_progress(self, show=True, is_push=False): + """显示/隐藏进度条(线程安全)""" + self.run_on_ui_thread(self._show_progress_impl, show, is_push) + + def _update_progress_impl(self, value, max_value=100, label="", is_push=False): + """更新进度条的实际实现(必须在主线程调用)""" + if is_push: + percent = (value / max_value) * 100 + self.push_progress['value'] = percent + self.push_progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)") + else: + percent = (value / max_value) * 100 + self.progress['value'] = percent + self.progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)") + self.root.update_idletasks() + + def update_progress(self, value, max_value=100, label="", is_push=False): + """更新进度条(线程安全)""" + self.run_on_ui_thread(self._update_progress_impl, value, max_value, label, is_push) + + def update_device_status(self, connected, vin=None, authorized=False): + """更新设备状态显示(线程安全:立即设状态变量,UI走主线程)""" + self.device_connected = connected + if vin is not None: + self.vin = vin + self.run_on_ui_thread(self._update_device_status_impl, connected, vin, authorized) + + def _update_device_status_impl(self, connected, vin, authorized): + """设备状态UI更新的实际实现(必须在主线程调用)""" + if connected: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['success']) + self.device_status_label.config(text="已连接", fg=self.colors['success']) + if vin: + self.vin_label.config(text=vin, fg=self.colors['success']) + if authorized: + self.auth_label.config(text="已授权", fg=self.colors['success']) + else: + self.auth_label.config(text="未授权", fg=self.colors['error']) + else: + self.vin_label.config(text="未获取", fg=self.colors['error']) + self.auth_label.config(text="未验证", fg=self.colors['error']) + else: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['error']) + self.device_status_label.config(text="未连接", fg=self.colors['error']) + self.vin_label.config(text="未获取", fg=self.colors['error']) + self.auth_label.config(text="未验证", fg=self.colors['error']) + + def check_device_connection(self): + """检查设备是否连接""" + if self.debug_mode: + return True + if not self.device_connected: + messagebox.showwarning("设备未连接", "请先连接设备并点击「检查」按钮刷新状态!") + return False + return True + + def start_device_monitor(self): + """启动设备状态监控(每5秒检查一次)""" + def monitor(): + while True: + try: + result = subprocess.run(f'{self.adb} -d devices', shell=True, capture_output=True, text=True) + lines = result.stdout.strip().split('\n') + devices = [line for line in lines[1:] if line.strip() and 'device' in line and 'offline' not in line] + + if devices and not self.device_connected and not self._refreshing: + # 设备新连接,刷新状态 + self.refresh_device_status() + elif not devices and self.device_connected: + # 设备断开连接 + self.update_device_status(False) + self.log("设备已断开连接", "WARNING") + + time.sleep(5) + except: + time.sleep(5) + + threading.Thread(target=monitor, daemon=True).start() + + def get_root_permission(self): + """获取 root 权限,首次获取需重启""" + if not self.check_device_connection(): + return + + def get_root(): + self.show_progress(True, is_push=False) + + # 执行 adb -d root + ok_root, out_root = self.run_adb_command('adb -d root') + if not ok_root: + self.log("获取 root 失败", "ERROR") + self.show_progress(False, is_push=False) + return + + time.sleep(1) + + # 执行 adb -d remount(需同时捕获 stdout 和 stderr) + remount_result = subprocess.run(f'{self.adb} -d remount', shell=True, + capture_output=True, text=True) + if remount_result.returncode != 0: + self.log("获取权限失败", "ERROR") + self.show_progress(False, is_push=False) + return + + # 判断是否需要重启:首次 remount 返回 "Now reboot your device for settings to take effect" + combined_output = (remount_result.stdout + remount_result.stderr).lower() + if 'now reboot your device' in combined_output: + self.log("首次获取权限,需要重启设备...", "INFO") + ok, _ = self.run_adb_command('adb -d shell reboot') + if ok: + self.log("设备即将重启,重启后权限生效", "INFO") + self.update_device_status(False) + else: + self.log("重启失败", "ERROR") + else: + self.log("已获取权限", "SUCCESS") + + self.show_progress(False, is_push=False) + + threading.Thread(target=get_root, daemon=True).start() + + def check_package_extracted(self): + """检查语言包是否已解压""" + has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0 + has_priv = self.priv_apps_dir and self.priv_apps_dir.exists() and len(list(self.priv_apps_dir.glob("*.apk"))) > 0 + return has_app or has_priv + + def extract_package_silent(self): + """静默解压语言包(带进度)""" + if not self.package_file.exists(): + self.log(f"未找到资源包 ({self.package_file.name})", "ERROR") + return False + + try: + # 使用用户目录,无需管理员权限 + local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) + hidden_path = Path(local_appdata) / ".cache" / "system" / ".android" + hidden_path.mkdir(parents=True, exist_ok=True) + + self.temp_dir = hidden_path / "apps_cache_Q07" + + # 如果已存在,先清理 + if self.temp_dir.exists(): + shutil.rmtree(self.temp_dir, ignore_errors=True) + time.sleep(0.5) + + self.temp_dir.mkdir(parents=True, exist_ok=True) + + # 设置隐藏属性(Windows) + if sys.platform == 'win32': + subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True) + subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True) + + + # 使用 7za 高速解压 + self.update_progress(0, 1, "资源加载中...") + result = subprocess.run( + [self.sz, 'x', str(self.package_file), f'-p{self.extract_password}', f'-o{self.temp_dir}', '-y'], + capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + if result.returncode != 0: + stderr = result.stderr.strip() + if stderr: + self.log(f"解压失败: {stderr[:200]}", "ERROR") + else: + self.log("解压失败,请检查密码是否正确", "ERROR") + return False + self.update_progress(1, 1, "资源加载完成") + + # 查找app和priv-app目录 + self.apps_dir = None + self.priv_apps_dir = None + + app_candidates = list(self.temp_dir.rglob("app")) or list(self.temp_dir.rglob("apps")) + if app_candidates: + self.apps_dir = app_candidates[0] + + priv_app_candidates = list(self.temp_dir.rglob("priv-app")) or list(self.temp_dir.rglob("priv-apps")) + if priv_app_candidates: + self.priv_apps_dir = priv_app_candidates[0] + + if not self.apps_dir and not self.priv_apps_dir: + self.log("未找到 app/priv-app 目录", "WARNING") + return False + + return True + + except Exception as e: + self.log(f"资源准备失败", "ERROR") + return False + + def check_environment(self): + """检查环境""" + try: + result = subprocess.run(f'{self.adb} version', shell=True, capture_output=True, text=True) + if result.returncode == 0: + self.refresh_device_status() + if not self.package_file.exists(): + self.log("未找到资源包文件", "WARNING") + else: + self._try_reuse_extracted() + else: + self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") + except FileNotFoundError: + self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") + + def _try_reuse_extracted(self): + """检查磁盘上是否已有解压好的资源,有则直接复用""" + local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) + cache_dir = Path(local_appdata) / ".cache" / "system" / ".android" / "apps_cache_Q07" + if not cache_dir.exists(): + return + + app_candidates = list(cache_dir.rglob("app")) or list(cache_dir.rglob("apps")) + priv_candidates = list(cache_dir.rglob("priv-app")) or list(cache_dir.rglob("priv-apps")) + + has_app = False + has_priv = False + if app_candidates: + apks = list(app_candidates[0].glob("*.apk")) + has_app = len(apks) > 0 + if priv_candidates: + apks = list(priv_candidates[0].glob("*.apk")) + has_priv = len(apks) > 0 + + if has_app or has_priv: + if has_app: + self.apps_dir = app_candidates[0] + if has_priv: + self.priv_apps_dir = priv_candidates[0] + self.temp_dir = cache_dir + # self.log("已复用缓存的资源文件", "INFO") + + def refresh_device_status(self): + """刷新设备状态""" + # 防止并发刷新 + if self._refreshing: + return + self._refreshing = True + + def refresh(): + was_connected = self.device_connected + + # 检查设备连接 + result = subprocess.run(f'{self.adb} -d devices', shell=True, capture_output=True, text=True) + lines = result.stdout.strip().split('\n') + devices = [line for line in lines[1:] if line.strip() and 'device' in line and 'offline' not in line] + + if devices: + # 只在首次连接时打日志 + if not was_connected: + self.log("设备已连接", "SUCCESS") + + # 获取VIN — 兼容两种 key,过滤 Android null 返回值 + vin = '' + for key in ('ca_vin_info', 'VIN'): + vin_result = subprocess.run( + f'{self.adb} -d shell settings get system {key}', + shell=True, capture_output=True, text=True) + vin = vin_result.stdout.strip() + if vin and vin != 'null': + break + vin = '' + if vin: + self.log(f"当前车辆VIN: {vin}", "INFO") + + # 验证授权 + authorized = self.check_authorization(vin) + self.update_device_status(True, vin, authorized) + else: + self.log("无法获取VIN", "WARNING") + self.update_device_status(True, None, False) + else: + if was_connected: + self.log("设备未连接", "WARNING") + self.update_device_status(False) + + self._refreshing = False + + threading.Thread(target=refresh, daemon=True).start() + + def check_authorization(self, vin): + """检查授权""" + if self.debug_mode: + self.log("调试模式: 跳过授权验证", "WARNING") + return True + self.log("正在验证授权...", "INFO") + try: + url = f"{self.api_url}?vin={vin}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + if data.get('authorized') == True: + self.log("✅ 授权验证通过!", "SUCCESS") + if 'data' in data and 'vehicleName' in data['data']: + self.log(f"车辆名称: {data['data']['vehicleName']}", "INFO") + return True + else: + self.log(f"❌ 授权验证失败", "ERROR") + return False + + except Exception as e: + self.log(f"❌ 授权验证失败", "ERROR") + return False + + def fetch_package_password(self): + """从服务端获取资源包解压密码""" + if not self.vin: + self.log("请先连接adb!", "ERROR") + return False + + try: + pwd_api_url = "https://api.changan.softwindy.cn/api/authorizations/package-key" + url = f"{pwd_api_url}?vin={self.vin}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + if data.get('success') and 'data' in data and 'password' in data['data']: + self.extract_password = data['data']['password'] + return True + else: + self.log(f"数据准备失败: {data.get('message', '未知错误')}", "ERROR") + return False + + except Exception as e: + self.log(f"数据准备失败: {str(e)}", "ERROR") + return False + + def run_adb_command(self, command): + """执行 adb 命令,静默执行,仅返回结果""" + command = command.replace('adb', self.adb, 1) + if self.debug_mode: + self.log(f"CMD: {command}", "CMD") + try: + result = subprocess.run(command, shell=True, capture_output=True, text=True, encoding='utf-8') + if self.debug_mode: + out = result.stdout.strip() + err = result.stderr.strip() + if out: + self.log(f" -> {out[:300]}", "CMD") + if err: + self.log(f" !! {err[:300]}", "ERROR") + if result.returncode == 0: + return True, result.stdout.strip() + else: + return False, result.stderr.strip() + except Exception as e: + return False, str(e) + + def push_single_apk(self, apk_path, apk_name, target_type="app"): + """推送单个APK到系统分区,返回 (成功, 错误信息)""" + temp_apk_path = f"/data/local/tmp/{apk_name}.apk" + target_dir = f"/system/priv-app/{apk_name}" if target_type == "priv-app" else f"/system/app/{apk_name}" + target_apk_path = f"{target_dir}/{apk_name}.apk" + + ok, err = self.run_adb_command(f'adb -d push "{apk_path}" {temp_apk_path}') + if not ok: + return False, f"push失败: {err}" + + self.run_adb_command(f'adb -d shell mkdir -p {target_dir}') + ok, err = self.run_adb_command(f'adb -d shell cp {temp_apk_path} {target_apk_path}') + self.run_adb_command(f'adb -d shell rm -f {temp_apk_path}') + if not ok: + return False, f"cp失败: {err}" + + return True, "" + + def push_all_apks(self): + """推送APK到系统分区(支持app和priv-app)""" + if not self.check_device_connection(): + return + if not self.vin: + messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码") + return + + messagebox.showwarning("⚠️ 重要提示", + "刷入过程中请勿:\n" + " ● 重启车机\n" + " ● 退出本程序\n" + " ● 关闭电脑\n\n" + "否则可能导致车机系统损坏!") + + def do_push_all(): + if not self.check_authorization(self.vin): + self.run_on_ui_thread(lambda: messagebox.showerror("授权失败", "设备未授权")) + return + if not self.extract_password: + if not self.fetch_package_password(): + self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源准备失败!")) + return + if not self.check_package_extracted(): + self.show_progress(True, is_push=False) + if not self.extract_package_silent(): + self.show_progress(False, is_push=False) + self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源准备失败!")) + return + self.show_progress(False, is_push=False) + + if (not self.apps_dir or not self.apps_dir.exists()) and \ + (not self.priv_apps_dir or not self.priv_apps_dir.exists()): + self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源目录未找到")) + return + + self.show_progress(True, is_push=True) + self.run_adb_command('adb -d shell mkdir -p /data/local/tmp') + + all_apks = [] + if self.apps_dir and self.apps_dir.exists(): + for apk in self.apps_dir.glob("*.apk"): + all_apks.append((apk, "app")) + if self.priv_apps_dir and self.priv_apps_dir.exists(): + for apk in self.priv_apps_dir.glob("*.apk"): + all_apks.append((apk, "priv-app")) + + if not all_apks: + # 缓存可能过期,强制重新解压 + self.apps_dir = None + self.priv_apps_dir = None + self.temp_dir = None + if not self.fetch_package_password() or not self.extract_package_silent(): + self.log("未找到语言包文件", "WARNING") + self.show_progress(False, is_push=True) + return + # 重新收集 + all_apks = [] + if self.apps_dir and self.apps_dir.exists(): + for apk in self.apps_dir.glob("*.apk"): + all_apks.append((apk, "app")) + if self.priv_apps_dir and self.priv_apps_dir.exists(): + for apk in self.priv_apps_dir.glob("*.apk"): + all_apks.append((apk, "priv-app")) + if not all_apks: + self.log("未找到语言包文件", "WARNING") + self.show_progress(False, is_push=True) + return + + total = len(all_apks) + success_count = 0 + aborted = False + for i, (apk_path, apk_type) in enumerate(all_apks, 1): + apk_name = apk_path.stem + ok, err = self.push_single_apk(apk_path, apk_name, apk_type) + if ok: + success_count += 1 + else: + if "Read-only file system" in err: + self.log("请先点击「获取权限」获取权限后再试", "ERROR") + aborted = True + break + self.update_progress(i, total, "正在刷入...", is_push=True) + + self.update_progress(total, total, "刷入完成" if not aborted else "已终止", is_push=True) + + if success_count == total: + self.log(f"刷入完成,共 {total} 个语言包", "SUCCESS") + self.log("语言包已刷入完成,重启设备后生效,您可在适当时候重启", "WARNING") + elif success_count > 0: + self.log(f"部分刷入成功({success_count}/{total})", "WARNING") + if not aborted: + self.log("语言包已刷入完成,重启设备后生效,您可在适当时候重启", "WARNING") + + self.show_progress(False, is_push=True) + + threading.Thread(target=do_push_all, daemon=True).start() + + def install_all_apks(self): + """批量安装APK — 手动选择文件夹""" + if not self.check_device_connection(): + return + + apk_dir = filedialog.askdirectory(title="选择包含APK文件的文件夹") + if not apk_dir: + return + + apk_files = list(Path(apk_dir).glob("*.apk")) + if not apk_files: + messagebox.showerror("错误", "所选文件夹中没有APK文件!") + return + + result = messagebox.askyesno("确认安装", + f"找到 {len(apk_files)} 个APK文件\n\n是否开始批量安装?") + if not result: + return + + def install(): + self.show_progress(True, is_push=True) + total = len(apk_files) + self.log(f"开始批量安装 {total} 个APK...", "INFO") + self.run_adb_command('adb -d shell setprop vecentek.model 1') + + success_count = 0 + for i, apk_path in enumerate(apk_files, 1): + self.update_progress(i, total, "安装中...", is_push=True) + success, _ = self.run_adb_command(f'adb -d install -r "{apk_path}"') + if success: + success_count += 1 + + self.run_adb_command('adb -d shell setprop vecentek.model 0') + self.update_progress(total, total, "安装完成", is_push=True) + self.show_progress(False, is_push=True) + + if success_count == total: + self.log(f"安装完成:全部 {total} 个成功", "SUCCESS") + messagebox.showinfo("安装完成", f"成功安装 {total} 个APK!") + elif success_count > 0: + self.log(f"安装完成:{success_count}/{total} 成功", "WARNING") + messagebox.showwarning("部分成功", f"成功: {success_count}\n失败: {total - success_count}") + else: + self.log("安装失败", "ERROR") + messagebox.showerror("安装失败", "所有APK安装失败!") + + self.show_progress(False, is_push=True) + + threading.Thread(target=install, daemon=True).start() + + def install_single_apk(self): + """安装单个APK""" + # 检查设备连接 + if not self.check_device_connection(): + return + + file_path = filedialog.askopenfilename( + title="选择APK文件", + filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + ) + + if not file_path: + return + + def install(): + self.show_progress(True, is_push=True) + self.update_progress(50, 100, f"安装中", is_push=True) + self.run_adb_command('adb -d shell setprop vecentek.model 1') + success, _ = self.run_adb_command(f'adb -d install -r "{file_path}"') + self.run_adb_command('adb -d shell setprop vecentek.model 0') + self.update_progress(100, 100, f"完成", is_push=True) + if success: + self.log("✓ 安装成功", "SUCCESS") + else: + self.log("✗ 安装失败", "ERROR") + self.show_progress(False, is_push=True) + + threading.Thread(target=install, daemon=True).start() + + def open_language_settings(self): + """打开系统语言设置""" + if not self.check_device_connection(): + return + self.run_adb_command('adb -d shell am start -a android.settings.LOCALE_SETTINGS') + + def open_language_quick_set(self): + """打开快捷语言设置弹窗""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 创建弹窗 + popup = tk.Toplevel(self.root) + popup.title("快捷语言设置") + popup.geometry("520x320") + popup.configure(bg=self.colors['bg_dark']) + popup.resizable(False, False) + + # 居中显示 + popup.update_idletasks() + x = self.root.winfo_x() + (self.root.winfo_width() - 520) // 2 + y = self.root.winfo_y() + (self.root.winfo_height() - 320) // 2 + popup.geometry(f"+{x}+{y}") + popup.transient(self.root) + popup.grab_set() + + # 标题 + header = tk.Label(popup, text="选择目标语言", + font=('Microsoft YaHei', 13, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + header.pack(pady=(15, 10)) + + hint = tk.Label(popup, text="点击按钮即可将系统语言切换为对应语言,重启后生效", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + hint.pack(pady=(0, 12)) + + # 语言列表:(显示名, locale_code) + languages = [ + ("🇨🇳 中文", "zh-CN"), + ("英 English", "en-US"), + ("俄 Русский", "ru-RU"), + ("法 Français", "fr-FR"), + ("西 Español", "es-ES"), + ("葡 Português", "pt-BR"), + ("意 Italiano", "it-IT"), + ("阿 العربية", "ar-SA"), + ] + # 创建按钮容器 + btn_frame = tk.Frame(popup, bg=self.colors['bg_dark']) + btn_frame.pack(pady=(0, 10)) + + btn_colors = [ + self.colors['accent'], self.colors['info'], + self.colors['success'], self.colors['warning'], + '#e17055', '#00b894', + '#6c5ce7', '#0984e3', + ] + + for i, (label, locale) in enumerate(languages): + row = i // 4 + col = i % 4 + + def make_cmd(loc=locale, lbl=label): + return lambda: self._quick_set_language(loc, lbl, popup) + + btn = tk.Button(btn_frame, text=label, + command=make_cmd(), + font=('Microsoft YaHei', 10), + fg='white', + bg=btn_colors[i], + relief=tk.FLAT, + cursor='hand2', + width=12, height=2) + btn.grid(row=row, column=col, padx=5, pady=5) + + # 底部分隔 + 打开系统设置入口 + sep = tk.Frame(popup, bg=self.colors['border'], height=1) + sep.pack(fill=tk.X, padx=20, pady=(8, 6)) + + sys_btn = tk.Button(popup, text="⚙️ 打开系统语言设置(手动选择)", + command=lambda: self._open_sys_and_close(popup), + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + sys_btn.pack(pady=(0, 10)) + + def _quick_set_language(self, locale_code, language_name, popup): + """执行快捷语言设置""" + popup.destroy() + + def do_set(): + self.log(f"正在设置系统语言为: {language_name} ({locale_code})", "INFO") + success, output = self.run_adb_command( + f'adb -d shell settings put system system_locales {locale_code}' + ) + + if success: + self.log(f"✓ 语言已设置为 {language_name}", "SUCCESS") + messagebox.showinfo( + "设置成功", + f"系统语言已设置为 {language_name}\n\n⚠️ 请重启设备使其生效。" + ) + else: + self.log(f"✗ 语言设置失败: {output}", "ERROR") + messagebox.showerror("设置失败", f"语言设置失败!\n\n{output}") + + threading.Thread(target=do_set, daemon=True).start() + + def _open_sys_and_close(self, popup): + """关闭弹窗并打开系统语言设置""" + popup.destroy() + self.open_language_settings() + + def open_timezone_settings(self): + """打开时区设置""" + if not self.check_device_connection(): + return + self.run_adb_command('adb -d shell am start -a android.settings.TIMEZONE_SETTINGS') + + def open_android_settings(self): + """打开安卓原生设置""" + if not self.check_device_connection(): + return + self.run_adb_command('adb -d shell am start -a android.settings.SETTINGS') + + def reboot_device(self): + """重启设备""" + if not self.check_device_connection(): + return + if messagebox.askyesno("确认重启", "确定要重启设备吗?"): + subprocess.Popen(f'{self.adb} -d shell reboot', shell=True, + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + self.log("设备正在重启...", "INFO") + self.update_device_status(False) + + def on_disable_upgrade(self): + """禁用系统升级""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 弹窗确认 + result = messagebox.askyesno( + "确认禁用升级", + "⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n" + "是否确定要禁用系统升级应用?\n\n" + "禁用命令:\n" + "adb -d shell pm disable-user --user 0 com.incall.apps.softmanager" + ) + + if not result: + self.log("已取消禁用升级操作", "INFO") + return + + def disable(): + self.show_progress(True, is_push=False) + success, output = self.run_adb_command( + 'adb -d shell pm disable-user --user 0 com.incall.apps.softmanager') + if success: + self.log("系统升级已禁用", "SUCCESS") + messagebox.showinfo("成功", "系统升级已成功禁用!") + else: + self.log("禁用系统升级失败", "ERROR") + messagebox.showerror("错误", f"禁用失败:{output}") + self.show_progress(False, is_push=False) + + threading.Thread(target=disable, daemon=True).start() + + def _toggle_debug(self, event=None): + """切换调试模式(隐藏入口,Ctrl+Shift+D)""" + if self.debug_mode: + self.debug_mode = False + self.log("调试模式已关闭", "WARNING") + self.status_text.config(text="就绪") + self.refresh_device_status() + return + + pwd = tk.simpledialog.askstring("调试模式", "请输入调试密码:", show='*', parent=self.root) + if pwd == "zxch5200": + self.debug_mode = True + self.update_device_status(True, "", True) + self.log("🔧 调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志", "WARNING") + self.status_text.config(text="🔧 调试模式") + elif pwd is not None: + messagebox.showwarning("错误", "密码错误") + + def install_apps(self): + """安装App — 支持单选或多选APK文件""" + if not self.check_device_connection(): + return + + file_paths = filedialog.askopenfilenames( + title="选择APK文件", + filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + ) + if not file_paths: + return + + count = len(file_paths) + result = messagebox.askyesno("确认安装", f"已选择 {count} 个APK文件\n\n是否开始安装?") + if not result: + return + + def install(): + self.show_progress(True, is_push=True) + self.log(f"开始安装 {count} 个APK...", "INFO") + self.run_adb_command('adb -d shell setprop vecentek.model 1') + + success_count = 0 + for i, file_path in enumerate(file_paths, 1): + apk_name = Path(file_path).stem + self.update_progress(i, count, f"安装中 ({apk_name})", is_push=True) + success, _ = self.run_adb_command(f'adb -d install -r "{file_path}"') + if success: + self.log(f"✓ {apk_name}.apk", "SUCCESS") + success_count += 1 + else: + self.log(f"✗ {apk_name}.apk", "ERROR") + + self.run_adb_command('adb -d shell setprop vecentek.model 0') + self.update_progress(count, count, "安装完成", is_push=True) + self.show_progress(False, is_push=True) + + if success_count == count: + self.log(f"安装完成:全部 {count} 个成功", "SUCCESS") + messagebox.showinfo("安装完成", f"成功安装 {count} 个APK!") + elif success_count > 0: + self.log(f"安装完成:{success_count}/{count} 成功", "WARNING") + messagebox.showwarning("部分成功", f"成功: {success_count}\n失败: {count - success_count}") + else: + self.log("安装失败", "ERROR") + messagebox.showerror("安装失败", "所有APK安装失败!") + + threading.Thread(target=install, daemon=True).start() + + def run(self): + """运行程序""" + self.root.mainloop() + +def main(): + """主函数""" + if sys.version_info < (3, 6): + print("错误:需要Python 3.6或更高版本") + sys.exit(1) + + try: + app = ADKAPKGUI() + app.run() + except Exception as e: + print(f"启动失败: {e}") + import traceback + traceback.print_exc() + messagebox.showerror("错误", f"程序启动失败: {e}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/code b/code new file mode 100644 index 0000000..5f3434f Binary files /dev/null and b/code differ diff --git a/pack.bat b/pack.bat new file mode 100644 index 0000000..36e1346 --- /dev/null +++ b/pack.bat @@ -0,0 +1,92 @@ +@echo off +chcp 65001 >nul +set NAME=启源Q07刷入工具 +title %NAME% - Build + +echo ============================================================ +echo %NAME% - Cython Build +echo ============================================================ +echo. + +where python >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Python not found + pause + exit /b +) +for /f "delims=" %%i in ('where python') do set PY=%%i +echo Python: %PY% + +echo [1/6] Installing deps... +%PY% -m pip install pyinstaller cython pyzipper -q +if errorlevel 1 ( + %PY% -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple +) + +echo [2/6] Clean... +if exist "dist_cy" rmdir /s /q dist_cy 2>nul +if exist "build" rmdir /s /q build 2>nul +if exist "dist" rmdir /s /q dist 2>nul + +echo [3/6] Cython compile... +mkdir dist_cy 2>nul +copy app.py dist_cy\_core.py >nul + +%PY% -c "open('dist_cy/setup_cython.py','w').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')" + +cd dist_cy +%PY% setup_cython.py build_ext --inplace +if errorlevel 1 ( + cd .. + echo [WARN] Cython failed, fallback + goto :NORMAL +) + +for %%f in (_core*.pyd) do set PYD=%%f +if "%PYD%"=="" ( + cd .. + echo [WARN] No pyd, fallback + goto :NORMAL +) +echo PYD: %PYD% +copy "%PYD%" _core.pyd >nul + +%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')" + +echo [4/6] Copy resources... +copy ..\adb.exe . >nul +copy ..\AdbWinApi.dll . >nul +copy ..\AdbWinUsbApi.dll . >nul +if exist "..\app.ico" copy "..\app.ico" . >nul + +echo [5/6] PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin launcher.py +if errorlevel 1 ( + cd .. + echo [ERROR] PyInstaller failed + pause + exit /b +) + +echo [6/6] Cleanup... +del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py 2>nul +rmdir /s /q build 2>nul +cd .. +goto :DONE + +:NORMAL +cd /d "%~dp0" +echo [INFO] Normal PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin app.py + +:DONE +echo. +echo Done. +if exist "dist_cy\dist\%NAME%.exe" ( + echo Output: dist_cy\dist\%NAME%.exe +) else if exist "dist\%NAME%.exe" ( + echo Output: dist\%NAME%.exe +) else ( + echo Check dist folder +) +pause diff --git a/pack_common.bat b/pack_common.bat new file mode 100644 index 0000000..25f065f --- /dev/null +++ b/pack_common.bat @@ -0,0 +1,94 @@ +@echo off +chcp 65001 >nul +set NAME=长安逸动刷入工具 +set SRC=app-install.py +title %NAME% - Build + +echo ============================================================ +echo %NAME% - Cython Build +echo ============================================================ +echo. + +where python >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Python not found + pause + exit /b +) +for /f "delims=" %%i in ('where python') do set PY=%%i +echo Python: %PY% + +echo [1/6] Installing deps... +%PY% -m pip install pyinstaller cython pyzipper -q +if errorlevel 1 ( + %PY% -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple +) + +echo [2/6] Clean... +if exist "dist_cy" rmdir /s /q dist_cy 2>nul +if exist "build" rmdir /s /q build 2>nul +if exist "dist" rmdir /s /q dist 2>nul + +echo [3/6] Cython compile... +mkdir dist_cy 2>nul +copy %SRC% dist_cy\_core.py >nul + +%PY% -c "open('dist_cy/setup_cython.py','w').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')" + +cd dist_cy +%PY% setup_cython.py build_ext --inplace +if errorlevel 1 ( + cd .. + echo [WARN] Cython failed, fallback + goto :NORMAL +) + +for %%f in (_core*.pyd) do set PYD=%%f +if "%PYD%"=="" ( + cd .. + echo [WARN] No pyd, fallback + goto :NORMAL +) +echo PYD: %PYD% +copy "%PYD%" _core.pyd >nul + +%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')" + +echo [4/6] Copy resources... +copy ..\adb.exe . >nul +copy ..\AdbWinApi.dll . >nul +copy ..\AdbWinUsbApi.dll . >nul +copy ..\7za.exe . >nul +if exist "..\app.ico" copy "..\app.ico" . >nul + +echo [5/6] PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin launcher.py +if errorlevel 1 ( + cd .. + echo [ERROR] PyInstaller failed + pause + exit /b +) + +echo [6/6] Cleanup... +del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py 2>nul +rmdir /s /q build 2>nul +cd .. +goto :DONE + +:NORMAL +cd /d "%~dp0" +echo [INFO] Normal PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin %SRC% + +:DONE +echo. +echo Done. +if exist "dist_cy\dist\%NAME%.exe" ( + echo Output: dist_cy\dist\%NAME%.exe +) else if exist "dist\%NAME%.exe" ( + echo Output: dist\%NAME%.exe +) else ( + echo Check dist folder +) +pause diff --git a/pack_q07.bat b/pack_q07.bat new file mode 100644 index 0000000..2706702 --- /dev/null +++ b/pack_q07.bat @@ -0,0 +1,94 @@ +@echo off +chcp 65001 >nul +set NAME=启源Q07刷入工具 +set SRC=app.py +title %NAME% - Build + +echo ============================================================ +echo %NAME% - Cython Build +echo ============================================================ +echo. + +where python >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Python not found + pause + exit /b +) +for /f "delims=" %%i in ('where python') do set PY=%%i +echo Python: %PY% + +echo [1/6] Installing deps... +%PY% -m pip install pyinstaller cython pyzipper -q +if errorlevel 1 ( + %PY% -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple +) + +echo [2/6] Clean... +if exist "dist_cy" rmdir /s /q dist_cy 2>nul +if exist "build" rmdir /s /q build 2>nul +if exist "dist" rmdir /s /q dist 2>nul + +echo [3/6] Cython compile... +mkdir dist_cy 2>nul +copy %SRC% dist_cy\_core.py >nul + +%PY% -c "open('dist_cy/setup_cython.py','w').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')" + +cd dist_cy +%PY% setup_cython.py build_ext --inplace +if errorlevel 1 ( + cd .. + echo [WARN] Cython failed, fallback + goto :NORMAL +) + +for %%f in (_core*.pyd) do set PYD=%%f +if "%PYD%"=="" ( + cd .. + echo [WARN] No pyd, fallback + goto :NORMAL +) +echo PYD: %PYD% +copy "%PYD%" _core.pyd >nul + +%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')" + +echo [4/6] Copy resources... +copy ..\adb.exe . >nul +copy ..\AdbWinApi.dll . >nul +copy ..\AdbWinUsbApi.dll . >nul +copy ..\7za.exe . >nul +if exist "..\app.ico" copy "..\app.ico" . >nul + +echo [5/6] PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin launcher.py +if errorlevel 1 ( + cd .. + echo [ERROR] PyInstaller failed + pause + exit /b +) + +echo [6/6] Cleanup... +del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py 2>nul +rmdir /s /q build 2>nul +cd .. +goto :DONE + +:NORMAL +cd /d "%~dp0" +echo [INFO] Normal PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin %SRC% + +:DONE +echo. +echo Done. +if exist "dist_cy\dist\%NAME%.exe" ( + echo Output: dist_cy\dist\%NAME%.exe +) else if exist "dist\%NAME%.exe" ( + echo Output: dist\%NAME%.exe +) else ( + echo Check dist folder +) +pause diff --git a/pack_s05.bat b/pack_s05.bat new file mode 100644 index 0000000..eee347c --- /dev/null +++ b/pack_s05.bat @@ -0,0 +1,94 @@ +@echo off +chcp 65001 >nul +set NAME=深蓝S05刷入工具 +set SRC=S05.py +title %NAME% - Build + +echo ============================================================ +echo %NAME% - Cython Build +echo ============================================================ +echo. + +where python >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Python not found + pause + exit /b +) +for /f "delims=" %%i in ('where python') do set PY=%%i +echo Python: %PY% + +echo [1/6] Installing deps... +%PY% -m pip install pyinstaller cython pyzipper -q +if errorlevel 1 ( + %PY% -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple +) + +echo [2/6] Clean... +if exist "dist_cy" rmdir /s /q dist_cy 2>nul +if exist "build" rmdir /s /q build 2>nul +if exist "dist" rmdir /s /q dist 2>nul + +echo [3/6] Cython compile... +mkdir dist_cy 2>nul +copy %SRC% dist_cy\_core.py >nul + +%PY% -c "open('dist_cy/setup_cython.py','w').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')" + +cd dist_cy +%PY% setup_cython.py build_ext --inplace +if errorlevel 1 ( + cd .. + echo [WARN] Cython failed, fallback + goto :NORMAL +) + +for %%f in (_core*.pyd) do set PYD=%%f +if "%PYD%"=="" ( + cd .. + echo [WARN] No pyd, fallback + goto :NORMAL +) +echo PYD: %PYD% +copy "%PYD%" _core.pyd >nul + +%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')" + +echo [4/6] Copy resources... +copy ..\adb.exe . >nul +copy ..\AdbWinApi.dll . >nul +copy ..\AdbWinUsbApi.dll . >nul +copy ..\7za.exe . >nul +if exist "..\app.ico" copy "..\app.ico" . >nul + +echo [5/6] PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin launcher.py +if errorlevel 1 ( + cd .. + echo [ERROR] PyInstaller failed + pause + exit /b +) + +echo [6/6] Cleanup... +del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py 2>nul +rmdir /s /q build 2>nul +cd .. +goto :DONE + +:NORMAL +cd /d "%~dp0" +echo [INFO] Normal PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin %SRC% + +:DONE +echo. +echo Done. +if exist "dist_cy\dist\%NAME%.exe" ( + echo Output: dist_cy\dist\%NAME%.exe +) else if exist "dist\%NAME%.exe" ( + echo Output: dist\%NAME%.exe +) else ( + echo Check dist folder +) +pause diff --git a/pack_x5plus.bat b/pack_x5plus.bat new file mode 100644 index 0000000..459d75a --- /dev/null +++ b/pack_x5plus.bat @@ -0,0 +1,94 @@ +@echo off +chcp 65001 >nul +set NAME=X5plus刷入工具 +set SRC=X5plusTool.py +title %NAME% - Build + +echo ============================================================ +echo %NAME% - Cython Build +echo ============================================================ +echo. + +where python >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Python not found + pause + exit /b +) +for /f "delims=" %%i in ('where python') do set PY=%%i +echo Python: %PY% + +echo [1/6] Installing deps... +%PY% -m pip install pyinstaller cython pyzipper -q +if errorlevel 1 ( + %PY% -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple +) + +echo [2/6] Clean... +if exist "dist_cy" rmdir /s /q dist_cy 2>nul +if exist "build" rmdir /s /q build 2>nul +if exist "dist" rmdir /s /q dist 2>nul + +echo [3/6] Cython compile... +mkdir dist_cy 2>nul +copy %SRC% dist_cy\_core.py >nul + +%PY% -c "open('dist_cy/setup_cython.py','w').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')" + +cd dist_cy +%PY% setup_cython.py build_ext --inplace +if errorlevel 1 ( + cd .. + echo [WARN] Cython failed, fallback + goto :NORMAL +) + +for %%f in (_core*.pyd) do set PYD=%%f +if "%PYD%"=="" ( + cd .. + echo [WARN] No pyd, fallback + goto :NORMAL +) +echo PYD: %PYD% +copy "%PYD%" _core.pyd >nul + +%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')" + +echo [4/6] Copy resources... +copy ..\adb.exe . >nul +copy ..\AdbWinApi.dll . >nul +copy ..\AdbWinUsbApi.dll . >nul +copy ..\7za.exe . >nul +if exist "..\app.ico" copy "..\app.ico" . >nul + +echo [5/6] PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin launcher.py +if errorlevel 1 ( + cd .. + echo [ERROR] PyInstaller failed + pause + exit /b +) + +echo [6/6] Cleanup... +del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py 2>nul +rmdir /s /q build 2>nul +cd .. +goto :DONE + +:NORMAL +cd /d "%~dp0" +echo [INFO] Normal PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin %SRC% + +:DONE +echo. +echo Done. +if exist "dist_cy\dist\%NAME%.exe" ( + echo Output: dist_cy\dist\%NAME%.exe +) else if exist "dist\%NAME%.exe" ( + echo Output: dist\%NAME%.exe +) else ( + echo Check dist folder +) +pause diff --git a/pack_yidong.bat b/pack_yidong.bat new file mode 100644 index 0000000..ce7b24e --- /dev/null +++ b/pack_yidong.bat @@ -0,0 +1,94 @@ +@echo off +chcp 65001 >nul +set NAME=长安逸动刷入工具 +set SRC=app-yidong.py +title %NAME% - Build + +echo ============================================================ +echo %NAME% - Cython Build +echo ============================================================ +echo. + +where python >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Python not found + pause + exit /b +) +for /f "delims=" %%i in ('where python') do set PY=%%i +echo Python: %PY% + +echo [1/6] Installing deps... +%PY% -m pip install pyinstaller cython pyzipper -q +if errorlevel 1 ( + %PY% -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple +) + +echo [2/6] Clean... +if exist "dist_cy" rmdir /s /q dist_cy 2>nul +if exist "build" rmdir /s /q build 2>nul +if exist "dist" rmdir /s /q dist 2>nul + +echo [3/6] Cython compile... +mkdir dist_cy 2>nul +copy %SRC% dist_cy\_core.py >nul + +%PY% -c "open('dist_cy/setup_cython.py','w').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')" + +cd dist_cy +%PY% setup_cython.py build_ext --inplace +if errorlevel 1 ( + cd .. + echo [WARN] Cython failed, fallback + goto :NORMAL +) + +for %%f in (_core*.pyd) do set PYD=%%f +if "%PYD%"=="" ( + cd .. + echo [WARN] No pyd, fallback + goto :NORMAL +) +echo PYD: %PYD% +copy "%PYD%" _core.pyd >nul + +%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')" + +echo [4/6] Copy resources... +copy ..\adb.exe . >nul +copy ..\AdbWinApi.dll . >nul +copy ..\AdbWinUsbApi.dll . >nul +copy ..\7za.exe . >nul +if exist "..\app.ico" copy "..\app.ico" . >nul + +echo [5/6] PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin launcher.py +if errorlevel 1 ( + cd .. + echo [ERROR] PyInstaller failed + pause + exit /b +) + +echo [6/6] Cleanup... +del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py 2>nul +rmdir /s /q build 2>nul +cd .. +goto :DONE + +:NORMAL +cd /d "%~dp0" +echo [INFO] Normal PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin %SRC% + +:DONE +echo. +echo Done. +if exist "dist_cy\dist\%NAME%.exe" ( + echo Output: dist_cy\dist\%NAME%.exe +) else if exist "dist\%NAME%.exe" ( + echo Output: dist\%NAME%.exe +) else ( + echo Check dist folder +) +pause diff --git a/pull_list.txt b/pull_list.txt new file mode 100644 index 0000000..e69de29