diff --git a/7za.exe b/7za.exe index 7f6bf86..2577379 100644 Binary files a/7za.exe and b/7za.exe differ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9dc9181 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,168 @@ +# 长安语言刷入工具 (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 + +## Current local state (2026-05-20) + +- `S05.py` is kept as the original S05 tool entry. Do not fold the fixed-version changes back into it unless explicitly requested. +- `S05_fixed.py` is the active experimental/fixed S05 copy. It includes: + - `simpledialog` import fix for debug mode. + - Thread-safe message boxes from background threads via `run_on_ui_thread`. + - Quoted/portable ADB command path helper. + - `try/finally` recovery for `setprop vecentek.model 0` in install flows. + - `_refreshing` reset in `refresh_device_status()` via `finally`. + - URL-encoded VIN query parameters. + - `Ctrl+Shift+E` debug-only package extraction test that skips device/auth checks and asks for the package password. + - 7za progress support: detects `-bsp1` support and enables `-bsp1 -bso0 -bse1` only when supported, otherwise falls back to normal extraction. +- Root `7za.exe` has been replaced with 7-Zip Extra `26.01 (x64)` and is the file pack scripts will bundle. +- The unpacked 7-Zip Extra files/folders (`x64/`, `arm64/`, `Far/`, `7za.dll`, `7zxa.dll`, docs) are present locally, but existing pack scripts only bundle root `7za.exe`. +- `pack_s05_fixed.bat` was added to build `S05_fixed.py` separately as `深蓝S05刷入工具_fixed.exe`; it leaves `pack_s05.bat` unchanged. +- Do not add `chmod`, `chown`, or `restorecon` commands to the S05 system-app push path unless explicitly requested; the user confirmed the target system inherits permissions automatically. diff --git a/S05_fixed.py b/S05_fixed.py new file mode 100644 index 0000000..393aa29 --- /dev/null +++ b/S05_fixed.py @@ -0,0 +1,1671 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import subprocess +import json +import re +import threading +import tkinter as tk +from tkinter import ttk, scrolledtext, filedialog, messagebox, simpledialog +from pathlib import Path +from urllib.request import urlopen, Request +from urllib.error import URLError, HTTPError +from urllib.parse import urlencode +from datetime import datetime +import zipfile +try: + import pyzipper +except ImportError: + pyzipper = None +import shutil +import time + +class ADKAPKGUI: + def __init__(self): + self.root = tk.Tk() + self.root.title("长安语言安装工具") + self.root.geometry("650x640") + self.root.resizable(True, True) + + # 设置颜色主题 + self.colors_dark = { + 'bg_dark': '#1e1e2e', + 'bg_light': '#2a2a3e', + 'accent': '#6c5ce7', + 'accent_hover': '#5b4bc4', + 'success': '#00b894', + 'error': '#d63031', + 'warning': '#fdcb6e', + 'info': '#0984e3', + 'text': '#dfe6e9', + 'text_secondary': '#b2bec3', + 'border': '#3d3d5e' + } + self.colors_light = { + 'bg_dark': '#f5f5f5', + 'bg_light': '#ffffff', + 'accent': '#6c5ce7', + 'accent_hover': '#5b4bc4', + 'success': '#00b894', + 'error': '#d63031', + 'warning': '#e17055', + 'info': '#0984e3', + 'text': '#2d3436', + 'text_secondary': '#636e72', + 'border': '#dfe6e9' + } + self.colors = dict(self.colors_dark) + self.theme = 'dark' + + # 多语言 + self.lang = 'zh' + self.T = { + 'zh': { + 'title': '深蓝S05多语言安装', + 'btn_root': '🔓 获取权限', + 'btn_push': '📦 刷入语言包', + 'btn_install': '📱 安装App', + 'btn_language': '🌐 语言设置', + 'btn_timezone': '⏰ 时区设置', + 'btn_settings': '⚙️ 安卓设置', + 'btn_reboot': '🔄 重启设备', + 'btn_disable_upgrade': '❌ 禁用升级', + 'btn_clear_log': '🗑 清空日志', + 'device_label': '设备:', + 'vin_label': 'VIN码:', + 'auth_label': '授权:', + 'log_title': '📋 运行日志', + 'status_ready': '就绪', + 'status_connected': '已连接', + 'status_disconnected': '未连接', + 'status_detecting': '未检测', + 'vin_none': '未获取', + 'auth_none': '未验证', + 'auth_yes': '已授权', + 'auth_no': '未授权', + 'btn_refresh': '🔄 检查', + 'hint_factory': '🔧 工厂模式:拨号 *#*#888 ,密码:369875', + 'theme_dark': '🌙 暗色', + 'theme_light': '☀️ 亮色', + 'lang_zh': '中', + 'lang_en': 'EN', + 'switch_lang': '语言 / Language', + 'switch_theme': '切换主题', + 'about_company': '宜宾科宜科技有限公司 - 出口改装一站式服务', + }, + 'en': { + 'title': 'Shenlan S05 Multi-Language', + 'btn_root': '🔓 Get Root', + 'btn_push': '📦 Flash Lang Pkg', + 'btn_install': '📱 Install App', + 'btn_language': '🌐 Language', + 'btn_timezone': '⏰ Timezone', + 'btn_settings': '⚙️ Settings', + 'btn_reboot': '🔄 Reboot', + 'btn_disable_upgrade': '❌ Disable OTA', + 'btn_clear_log': '🗑 Clear Log', + 'device_label': 'Device:', + 'vin_label': 'VIN:', + 'auth_label': 'Auth:', + 'log_title': '📋 Log', + 'status_ready': 'Ready', + 'status_connected': 'Connected', + 'status_disconnected': 'Disconnected', + 'status_detecting': 'Detecting', + 'vin_none': 'None', + 'auth_none': 'Unknown', + 'auth_yes': 'Authorized', + 'auth_no': 'Unauthorized', + 'btn_refresh': '🔄 Check', + 'hint_factory': '🔧 Factory Mode: dial *#*#888, password: 369875', + 'theme_dark': '🌙 Dark', + 'theme_light': '☀️ Light', + 'lang_zh': '中', + 'lang_en': 'EN', + 'switch_lang': 'Language', + 'switch_theme': 'Theme', + 'about_company': 'Yibin Keyi Technology - Export Modification Service', + } + } + + # 从 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.btn_theme_switch = tk.Button(bottom_status, text="🌙 暗色", + command=self.toggle_theme, + font=('Microsoft YaHei', 8), + fg=self.colors['accent'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + self.btn_theme_switch.pack(side=tk.RIGHT, padx=5) + + self.btn_lang_switch = tk.Button(bottom_status, text="EN", + command=self.toggle_lang, + font=('Microsoft YaHei', 8, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + self.btn_lang_switch.pack(side=tk.RIGHT, padx=5) + + # 调试模式快捷键 + self.root.bind('', self._toggle_debug) + self.root.bind('', self._debug_test_extract) + + # 绑定悬停效果 + 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 _adb_cmd(self): + """返回可安全用于 shell 命令字符串的 adb 路径""" + return subprocess.list2cmdline([self.adb]) + + def t(self, key): + """获取翻译文本""" + return self.T.get(self.lang, self.T['zh']).get(key, key) + + def toggle_lang(self): + """切换语言""" + self.lang = 'en' if self.lang == 'zh' else 'zh' + self.btn_lang_switch.config(text=self.t('lang_en') if self.lang == 'zh' else self.t('lang_zh')) + self._refresh_ui_texts() + self.log(f"语言已切换为 {'English' if self.lang == 'en' else '中文'}", "INFO") + + def toggle_theme(self): + """切换主题""" + if self.theme == 'dark': + self.colors = dict(self.colors_light) + self.theme = 'light' + self.btn_theme_switch.config(text=self.t('theme_dark')) + else: + self.colors = dict(self.colors_dark) + self.theme = 'dark' + self.btn_theme_switch.config(text=self.t('theme_light')) + self._apply_theme() + + def _apply_theme(self): + """应用当前主题到所有控件""" + c = self.colors + self.root.configure(bg=c['bg_dark']) + style = ttk.Style() + style.configure('TFrame', background=c['bg_dark']) + style.configure('TLabel', background=c['bg_dark'], foreground=c['text']) + style.configure('TLabelframe', background=c['bg_dark'], foreground=c['text']) + style.configure('TLabelframe.Label', background=c['bg_dark'], foreground=c['accent']) + style.configure('TProgressbar', background=c['accent'], troughcolor=c['bg_light'], borderwidth=0) + self.log_text.tag_config('INFO', foreground='#74b9ff') + self.log_text.tag_config('SUCCESS', foreground='#55efc4') + self.log_text.tag_config('ERROR', foreground='#ff7675') + self.log_text.tag_config('WARNING', foreground='#ffeaa7') + self.log_text.tag_config('CMD', foreground='#a29bfe') + if self.theme == 'light': + self.log_text.configure(bg='#ffffff', fg='#2d3436') + else: + self.log_text.configure(bg='#2d2d3d', fg='#e0e0e0') + + def _refresh_ui_texts(self): + """刷新所有UI文本""" + t = self.t + widgets = [ + (getattr(self, 'title_label', None), 'title', None), + (getattr(self, 'subtitle_label', None), 'about_company', None), + (getattr(self, 'btn_root', None), 'btn_root', None), + (getattr(self, 'btn_push', None), 'btn_push', None), + (getattr(self, 'btn_install_all', None), 'btn_install', None), + (getattr(self, 'btn_language', None), 'btn_language', None), + (getattr(self, 'btn_timezone', None), 'btn_timezone', None), + (getattr(self, 'btn_settings', None), 'btn_settings', None), + (getattr(self, 'btn_reboot', None), 'btn_reboot', None), + (getattr(self, 'btn_exit', None), 'btn_disable_upgrade', None), + (getattr(self, 'btn_clear', None), 'btn_clear_log', None), + (getattr(self, 'log_title_label', None), 'log_title', None), + (getattr(self, 'status_text', None), 'status_ready', None), + (getattr(self, 'device_label', None), 'device_label', None), + (getattr(self, 'vin_label_title', None), 'vin_label', None), + (getattr(self, 'auth_label_title', None), 'auth_label', None), + (getattr(self, 'btn_refresh', None), 'btn_refresh', None), + (getattr(self, 'hint_label', None), 'hint_factory', None), + ] + for w, key, _ in widgets: + if w: + w.config(text=t(key)) + self.btn_theme_switch.config(text=t('theme_light') if self.theme == 'dark' else t('theme_dark')) + self.btn_lang_switch.config(text=t('lang_en') if self.lang == 'zh' else t('lang_zh')) + if self.vin: + self._update_device_status_impl(self.device_connected, self.vin, + getattr(self, '_last_authorized', False)) + + 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更新的实际实现(必须在主线程调用)""" + self._last_authorized = authorized + t = self.t + if connected: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['success']) + self.device_status_label.config(text=t('status_connected'), fg=self.colors['success']) + if vin: + self.vin_label.config(text=vin, fg=self.colors['success']) + if authorized: + self.auth_label.config(text=t('auth_yes'), fg=self.colors['success']) + else: + self.auth_label.config(text=t('auth_no'), fg=self.colors['error']) + else: + self.vin_label.config(text=t('vin_none'), fg=self.colors['error']) + self.auth_label.config(text=t('auth_none'), fg=self.colors['error']) + else: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['error']) + self.device_status_label.config(text=t('status_disconnected'), fg=self.colors['error']) + self.vin_label.config(text=t('vin_none'), fg=self.colors['error']) + self.auth_label.config(text=t('auth_none'), fg=self.colors['error']) + + def check_device_connection(self): + """检查设备是否连接""" + if self.debug_mode: + return True + if not self.device_connected: + messagebox.showwarning("设备未连接", "请先连接设备并点击「检查」按钮刷新状态!") + return False + return True + + def start_device_monitor(self): + """启动设备状态监控(每5秒检查一次)""" + def monitor(): + while True: + try: + result = subprocess.run(f'{self._adb_cmd()} -d devices', shell=True, capture_output=True, text=True) + lines = result.stdout.strip().split('\n') + devices = [line for line in lines[1:] if line.strip() and 'device' in line and 'offline' not in line] + + if devices and not self.device_connected and not self._refreshing: + # 设备新连接,刷新状态 + self.refresh_device_status() + elif not devices and self.device_connected: + # 设备断开连接 + self.update_device_status(False) + self.log("设备已断开连接", "WARNING") + + time.sleep(5) + except: + time.sleep(5) + + threading.Thread(target=monitor, daemon=True).start() + + 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_cmd()} -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 _decode_7z_output(self, output): + """解码 7za 输出,兼容中文 Windows 控制台编码""" + for enc in ('gbk', 'utf-8'): + try: + return output.decode(enc) + except UnicodeDecodeError: + continue + return output.decode('utf-8', errors='replace') + + def _extract_with_7za_progress(self): + """运行 7za 并实时解析百分比进度""" + self.update_progress(0, 100, "资源加载中...") + cmd = [ + self.sz, 'x', str(self.package_file), + f'-p{self.extract_password}', + f'-o{self.temp_dir}', '-y' + ] + if self._seven_zip_supports_progress_stream(): + cmd.extend(['-bsp1', '-bso0', '-bse1']) + creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.DEVNULL, + creationflags=creationflags, + bufsize=0 + ) + + output = bytearray() + last_percent = -1 + while True: + chunk = proc.stdout.read(1) if proc.stdout else b'' + if not chunk: + if proc.poll() is not None: + break + time.sleep(0.05) + continue + + output.extend(chunk) + if len(output) > 60000: + del output[:-60000] + + matches = re.findall(rb'(\d{1,3})%', bytes(output[-512:])) + if matches: + percent = min(100, int(matches[-1])) + if percent != last_percent: + last_percent = percent + self.update_progress(percent, 100, "资源加载中...") + + return_code = proc.wait() + decoded_output = self._decode_7z_output(bytes(output)) + if return_code == 0: + self.update_progress(100, 100, "资源加载完成") + return True, decoded_output + return False, decoded_output + + def _seven_zip_supports_progress_stream(self): + """检测 7za 是否支持 -bsp1 进度流参数""" + try: + result = subprocess.run( + [self.sz], + capture_output=True, + text=True, + errors='ignore', + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + return '-bs{o|e|p}' in (result.stdout + result.stderr) + except Exception: + return False + + def extract_package_silent(self): + """静默解压语言包(带进度)""" + if not self.package_file.exists(): + self.log(f"错误:未找到资源包 ({self.package_file})", "ERROR") + return False + + if not self.extract_password: + self.log("错误:解压密码未设置", "ERROR") + return False + + if not os.path.exists(self.sz): + self.log(f"错误:未找到 7za.exe ({self.sz})", "ERROR") + return False + + try: + # 使用用户目录,无需管理员权限 + local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) + hidden_path = Path(local_appdata) / ".cache" / "system" / ".android" + hidden_path.mkdir(parents=True, exist_ok=True) + + self.temp_dir = hidden_path / "apps_cache_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) + + self.log(f"正在准备资源包...", "INFO") + + ok, err_msg = self._extract_with_7za_progress() + if not ok: + if err_msg.strip(): + self.log(f"资源准备失败: {err_msg.strip()[:300]}", "ERROR") + else: + self.log("资源准备失败", "ERROR") + return False + + # 查找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("警告:未找到对应目录", "WARNING") + return False + + apk_count = len(list(self.apps_dir.glob("*.apk"))) if self.apps_dir else 0 + priv_count = len(list(self.priv_apps_dir.glob("*.apk"))) if self.priv_apps_dir else 0 + self.log(f"资源准备完成", "SUCCESS") + return True + + except Exception as e: + if getattr(self, 'debug_mode', False): + self.log(f"资源准备失败: {str(e)}", "ERROR") + import traceback + self.log(traceback.format_exc(), "ERROR") + else: + self.log("资源准备失败,请检查网络连接后重试", "ERROR") + return False + + def check_environment(self): + """检查环境""" + try: + result = subprocess.run(f'{self._adb_cmd()} version', shell=True, capture_output=True, text=True) + if result.returncode == 0: + self.refresh_device_status() + if not self.package_file.exists(): + self.log("未找到资源包文件", "WARNING") + else: + self._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(): + try: + was_connected = self.device_connected + + # 检查设备连接 + result = subprocess.run(f'{self._adb_cmd()} -d devices', shell=True, capture_output=True, text=True) + lines = result.stdout.strip().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_cmd()} -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) + except Exception as e: + self.log(f"刷新设备状态失败: {str(e)}", "ERROR") + finally: + 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}?{urlencode({'vin': vin})}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + 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}?{urlencode({'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_cmd(), 1) + if self.debug_mode: + self.log(f"CMD: {command}", "CMD") + try: + result = subprocess.run(command, shell=True, capture_output=True, text=True, encoding='utf-8') + if 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") + success_count = 0 + try: + self.run_adb_command('adb -d shell setprop vecentek.model 1') + + 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.update_progress(total, total, "安装完成", is_push=True) + + if success_count == total: + self.log(f"安装完成:全部 {total} 个成功", "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, "安装完成", f"成功安装 {total} 个APK!") + elif success_count > 0: + self.log(f"安装完成:{success_count}/{total} 成功", "WARNING") + self.run_on_ui_thread(messagebox.showwarning, "部分成功", f"成功: {success_count}\n失败: {total - success_count}") + else: + self.log("安装失败", "ERROR") + self.run_on_ui_thread(messagebox.showerror, "安装失败", "所有APK安装失败!") + except Exception as e: + self.log(f"安装过程异常: {str(e)}", "ERROR") + self.run_on_ui_thread(messagebox.showerror, "安装失败", f"安装过程异常:{str(e)}") + finally: + self.run_adb_command('adb -d shell setprop vecentek.model 0') + 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) + try: + self.run_adb_command('adb -d shell setprop vecentek.model 1') + success, _ = self.run_adb_command(f'adb -d install -r "{file_path}"') + self.update_progress(100, 100, f"完成", is_push=True) + if success: + self.log("✓ 安装成功", "SUCCESS") + else: + self.log("✗ 安装失败", "ERROR") + except Exception as e: + self.log(f"安装过程异常: {str(e)}", "ERROR") + finally: + self.run_adb_command('adb -d shell setprop vecentek.model 0') + 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") + self.run_on_ui_thread( + messagebox.showinfo, + "设置成功", + f"系统语言已设置为 {language_name}\n\n⚠️ 请重启设备使其生效。" + ) + else: + self.log(f"✗ 语言设置失败: {output}", "ERROR") + self.run_on_ui_thread(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_cmd()} -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") + self.run_on_ui_thread(messagebox.showinfo, "成功", "系统升级已成功禁用!") + else: + self.log("禁用系统升级失败", "ERROR") + self.run_on_ui_thread(messagebox.showerror, "错误", f"禁用失败:{output}") + self.show_progress(False, is_push=False) + + threading.Thread(target=disable, daemon=True).start() + + def _on_vin_input_focus_in(self, event): + """输入框获得焦点时清除占位符""" + if self.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}?{urlencode({'vin': vin})}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + def update_ui(): + if data.get('success'): + pwd = data.get('data', {}).get('devicePassword', '未知') + self.pwd_result_label.config( + text=f"密码: *#{pwd}#*", + fg=self.colors['success'] + ) + self.log(f"密码查询成功 VIN={vin} -> {pwd}", "SUCCESS") + else: + msg = data.get('message', '查询失败') + self.pwd_result_label.config( + text=f"失败: {msg}", + fg=self.colors['error'] + ) + self.log(f"密码查询失败: {msg}", "ERROR") + + self.run_on_ui_thread(update_ui) + + except Exception as e: + def update_ui_error(): + self.pwd_result_label.config( + text=f"请求失败", + fg=self.colors['error'] + ) + self.log(f"密码查询请求失败: {str(e)}", "ERROR") + self.run_on_ui_thread(update_ui_error) + + threading.Thread(target=do_query, daemon=True).start() + + def _toggle_debug(self, event=None): + """切换调试模式(隐藏入口,Ctrl+Shift+D)""" + if self.debug_mode: + self.debug_mode = False + self.log("调试模式已关闭", "WARNING") + self.status_text.config(text="就绪") + self.refresh_device_status() + return + + pwd = simpledialog.askstring("调试模式", "请输入调试密码:", show='*', parent=self.root) + if pwd == "zxch5200": + self.debug_mode = True + self.update_device_status(True, "", True) + self.log("🔧 调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志", "WARNING") + self.status_text.config(text="🔧 调试模式") + elif pwd is not None: + messagebox.showwarning("错误", "密码错误") + + def _debug_test_extract(self, event=None): + """调试模式下仅测试资源包解压,不检查设备和授权""" + if not self.debug_mode: + messagebox.showwarning("调试模式", "请先按 Ctrl+Shift+D 开启调试模式") + return + + pwd = simpledialog.askstring("测试解压", "请输入 package.bin 解压密码:", show='*', parent=self.root) + if not pwd: + return + + def do_extract(): + old_password = self.extract_password + old_apps_dir = self.apps_dir + old_priv_apps_dir = self.priv_apps_dir + old_temp_dir = self.temp_dir + self.extract_password = pwd + try: + self.show_progress(True, is_push=False) + if self.extract_package_silent(): + self.log("测试解压成功", "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + "测试解压成功", + f"资源已解压到:\n{self.temp_dir}" + ) + else: + self.log("测试解压失败", "ERROR") + self.run_on_ui_thread(messagebox.showerror, "测试解压失败", "请查看日志中的 7za 输出") + finally: + self.show_progress(False, is_push=False) + self.extract_password = old_password + self.apps_dir = old_apps_dir + self.priv_apps_dir = old_priv_apps_dir + self.temp_dir = old_temp_dir + + threading.Thread(target=do_extract, daemon=True).start() + + 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") + success_count = 0 + try: + self.run_adb_command('adb -d shell setprop vecentek.model 1') + + for i, file_path in enumerate(file_paths, 1): + apk_name = Path(file_path).stem + self.update_progress(i, count, f"安装中 ({apk_name})", is_push=True) + 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.update_progress(count, count, "安装完成", is_push=True) + + if success_count == count: + self.log(f"安装完成:全部 {count} 个成功", "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, "安装完成", f"成功安装 {count} 个APK!") + elif success_count > 0: + self.log(f"安装完成:{success_count}/{count} 成功", "WARNING") + self.run_on_ui_thread(messagebox.showwarning, "部分成功", f"成功: {success_count}\n失败: {count - success_count}") + else: + self.log("安装失败", "ERROR") + self.run_on_ui_thread(messagebox.showerror, "安装失败", "所有APK安装失败!") + except Exception as e: + self.log(f"安装过程异常: {str(e)}", "ERROR") + self.run_on_ui_thread(messagebox.showerror, "安装失败", f"安装过程异常:{str(e)}") + finally: + self.run_adb_command('adb -d shell setprop vecentek.model 0') + self.show_progress(False, is_push=True) + + 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/pack_s05_fixed.bat b/pack_s05_fixed.bat new file mode 100644 index 0000000..c8c3a77 --- /dev/null +++ b/pack_s05_fixed.bat @@ -0,0 +1,94 @@ +@echo off +chcp 65001 >nul +set NAME=深蓝S05刷入工具_fixed +set SRC=S05_fixed.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=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --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=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --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