diff --git a/A07/Qiyuan_A07_Multi-lan-installer.py b/A07/Qiyuan_A07_Multi-lan-installer.py index f13adde..890d6a8 100644 --- a/A07/Qiyuan_A07_Multi-lan-installer.py +++ b/A07/Qiyuan_A07_Multi-lan-installer.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import os +import atexit import shlex import sys import subprocess @@ -24,6 +25,27 @@ import shutil import time +STARTUP_T = { + 'zh': { + 'python_version_error': '错误:需要Python 3.6或更高版本', + 'startup_failed_title': '错误', + 'startup_failed_console': '启动失败: {error}', + 'startup_failed_dialog': '程序启动失败: {error}', + }, + 'en': { + 'python_version_error': 'Error: Python 3.6 or later is required', + 'startup_failed_title': 'Error', + 'startup_failed_console': 'Startup failed: {error}', + 'startup_failed_dialog': 'Program startup failed: {error}', + } +} + + +def startup_t(key): + lang = 'en' if os.environ.get('LANGUAGE_INSTALLER_LANG', '').lower().startswith('en') else 'zh' + return STARTUP_T.get(lang, STARTUP_T['zh']).get(key, key) + + def get_app_dir(): return Path(sys.executable).resolve().parent if getattr(sys, 'frozen', False) else Path(__file__).resolve().parent @@ -60,16 +82,32 @@ def find_tool(file_name, fallback=None): path = find_resource(file_name) if path.exists(): return str(path) - return fallback or str(path) + return fallback or str(path) + + +def set_windows_app_user_model_id(): + if sys.platform != 'win32': + return + try: + import ctypes + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + "yibin.keyi.qiyuan.a07.language.installer" + ) + except Exception: + pass + + class ADKAPKGUI: A07_SHELL_PASSWORD = "omo75A322@" SYSTEM_MOUNT_CANDIDATES = ("/system", "/system_root", "/") def __init__(self): + set_windows_app_user_model_id() self.root = tk.Tk() - self.root.title("启源A07多语言安装") + self.root.title("") self.root.geometry("650x640") self.root.resizable(True, True) + self.set_window_icon() # 设置颜色主题 self.colors_dark = { @@ -128,14 +166,187 @@ class ADKAPKGUI: 'auth_yes': '已授权', 'auth_no': '未授权', 'btn_refresh': '🔄 检查', - 'hint_factory': '🔧 启源A07:首次 adb shell 将自动输入登录密码,并通过 Magisk SU 解锁系统分区', 'theme_dark': '🌙 暗色', 'theme_light': '☀️ 亮色', - 'lang_zh': '中', - 'lang_en': 'EN', + 'lang_zh': '中文', + 'lang_en': 'English', 'switch_lang': '语言 / Language', 'switch_theme': '切换主题', - 'about_company': '宜宾科宜科技有限公司 - 智能设备管理平台', + 'key_query_label': '登录密码获取:', + 'vin_placeholder': '输入VIN或者VIN后六位', + 'auth_code_placeholder': '输入授权码', + 'btn_query_pwd': '获取密码', + 'key_need_input': '请输入VIN和授权码', + 'key_querying': '正在获取密码...', + 'key_query_success': '密码: {password}', + 'key_query_failed': '失败: {message}', + 'key_request_failed': '请求失败', + 'key_dial_tip': '打开车辆拨号App,拨号*#*#666,获取授权码。', + 'tip_1': '1. 安装语言过程中请保持车辆和电脑的电量充足,不可中途停止。', + 'tip_2': '2. 获取权限以后,车辆自动重启以后再进入语言刷入。', + 'tip_3': '3. 部分语言需要重启后生效,可以一切工作完成以后再重启。', + 'dialog_warning': '警告', + 'dialog_error': '错误', + 'dialog_info': '提示', + 'dialog_success': '成功', + 'dialog_device_not_connected_title': '设备未连接', + 'dialog_device_not_connected_body': '请先连接设备并点击「检查」按钮刷新状态!', + 'log_cleared': '日志已清空', + 'log_device_connected': '设备已连接', + 'log_device_disconnected': '设备已断开连接', + 'log_device_not_connected': '设备未连接', + 'log_current_vin': '当前车辆VIN: {vin}', + 'log_vin_unavailable': '无法获取VIN', + 'log_refresh_failed': '刷新设备状态失败: {error}', + 'log_root_ready': 'A07 system 分区已解锁,可以开始刷入', + 'log_root_failed': '获取权限失败', + 'log_cache_invalid': '已解压缓存无效: {reason}', + 'log_no_available_apk': '未找到可用 APK', + 'log_zero_apk': '发现 0KB APK: {preview}{suffix}', + 'extract_wrong_password': '解压密码错误,请重新确认 package.bin 密码', + 'extract_data_error': '资源包数据错误,可能是密码错误或 package.bin 损坏', + 'extract_broken': '资源包损坏或不完整,请检查 package.bin', + 'extract_prepare_failed_detail': '资源准备失败: {error}', + 'extract_prepare_failed': '资源准备失败,请检查解压密码是否正确', + 'progress_resource_loading': '资源加载中...', + 'progress_resource_done': '资源加载完成', + 'log_package_missing_path': '错误:未找到资源包 ({path})', + 'log_extract_password_missing': '错误:解压密码未设置', + 'log_7za_missing': '错误:未找到 7za.exe ({path})', + 'log_resource_preparing': '正在准备资源包...', + 'log_resource_dir_missing': '警告:未找到对应目录', + 'log_resource_invalid': '解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。', + 'log_resource_ready_debug': '资源准备完成 (app: {app}, priv-app: {priv}, system_ext: {system_ext})', + 'log_resource_ready': '资源准备完成', + 'log_resource_prepare_exception': '资源准备失败: {error}', + 'log_resource_prepare_retry': '资源准备失败,请检查网络连接后重试', + 'log_package_missing': '未找到资源包文件', + 'log_adb_missing': '未找到adb命令,请将ADB文件放入本目录', + 'log_cache_invalid_cleaned': '缓存资源无效,已清理: {reason}', + 'log_debug_skip_auth': '调试模式: 跳过授权验证', + 'log_auth_checking': '正在验证授权...', + 'log_auth_ok': '授权验证通过', + 'log_vehicle_name': '车辆名称: {vehicle_name}', + 'log_auth_failed': '授权验证失败', + 'log_connect_adb_first': '请先连接adb!', + 'log_prepare_failed_no_vehicle': '数据准备失败: 未获取到车型', + 'log_prepare_failed_reason': '数据准备失败: {reason}', + 'log_prepare_failed_error': '数据准备失败: {error}', + 'err_unknown': '未知错误', + 'push_fail': 'push失败: {error}', + 'copy_fail': 'cp失败: {error}', + 'dialog_need_vin': '请先刷新设备状态并获取VIN码', + 'dialog_flash_warning_title': '重要提示', + 'dialog_flash_warning_body': '刷入过程中请勿:\n ● 重启车机\n ● 退出本程序\n ● 关闭电脑\n\n否则可能导致车机系统损坏!', + 'log_flash_start': '开始刷入语言包,请勿断电或重启电脑和车机。', + 'dialog_auth_failed_title': '授权失败', + 'dialog_auth_failed_body': '设备未授权', + 'dialog_resource_failed_body': '资源准备失败!', + 'dialog_resource_dir_missing': '资源目录未找到', + 'log_system_unlock_failed': 'system 分区解锁失败', + 'log_lang_pkg_missing': '未找到语言包文件', + 'log_readonly_system': 'system 分区仍为只读,请重新点击「获取权限」后再试', + 'progress_flashing': '正在刷入...', + 'progress_flash_done': '刷入完成', + 'progress_aborted': '已终止', + 'log_flash_done': '刷入完成,共 {total} 个语言包', + 'log_flash_reboot_required': '语言包已刷入完成,请务必重启设备,system/priv-app 需要开机扫描后才会显示', + 'log_flash_partial': '部分刷入成功({success}/{total})', + 'log_flash_scan_required': '已刷入的系统应用需要重启设备后才会显示', + 'dialog_select_apk_folder': '选择包含APK文件的文件夹', + 'dialog_no_apk_in_folder': '所选文件夹中没有APK文件!', + 'dialog_confirm_install_title': '确认安装', + 'dialog_confirm_install_folder': '找到 {count} 个APK文件\n\n是否开始批量安装?', + 'log_batch_install_start': '开始批量安装 {count} 个APK...', + 'progress_installing': '安装中...', + 'progress_installing_apk': '安装中 ({apk})', + 'progress_install_done': '安装完成', + 'log_install_all_success': '安装完成:全部 {count} 个成功', + 'dialog_install_done_title': '安装完成', + 'dialog_install_all_success': '成功安装 {count} 个APK!', + 'log_install_partial': '安装完成:{success}/{total} 成功', + 'dialog_install_partial_title': '部分成功', + 'dialog_install_partial_body': '成功: {success}\n失败: {failed}', + 'log_install_failed': '安装失败', + 'dialog_install_failed_title': '安装失败', + 'dialog_install_all_failed': '所有APK安装失败!', + 'log_install_exception': '安装过程异常: {error}', + 'dialog_install_exception': '安装过程异常:{error}', + 'dialog_select_apk_file': '选择APK文件', + 'filetype_apk': 'APK文件', + 'filetype_all': '所有文件', + 'progress_done': '完成', + 'log_single_install_success': '✓ 安装成功', + 'log_single_install_failed': '✗ 安装失败', + 'quick_lang_title': '快捷语言设置', + 'quick_lang_header': '选择目标语言', + 'quick_lang_hint': '点击按钮即可将系统语言切换为对应语言,重启后生效', + 'quick_lang_system_button': '⚙️ 打开系统语言设置(手动选择)', + 'quick_lang_zh': '🇨🇳 中文', + 'quick_lang_en': '🇺🇸 英语', + 'quick_lang_ru': '🇷🇺 俄语', + 'quick_lang_fr': '🇫🇷 法语', + 'quick_lang_es': '🇪🇸 西班牙语', + 'quick_lang_pt': '🇵🇹 葡萄牙语', + 'quick_lang_it': '🇮🇹 意大利语', + 'quick_lang_ar': '🇸🇦 阿拉伯语', + 'log_setting_language': '正在设置系统语言为: {language} ({locale})', + 'log_language_set_success': '✓ 语言已设置为 {language}', + 'dialog_language_success_title': '设置成功', + 'dialog_language_success_body': '系统语言已设置为 {language}\n\n⚠️ 请重启设备使其生效。', + 'log_language_set_failed': '✗ 语言设置失败: {error}', + 'dialog_language_failed_title': '设置失败', + 'dialog_language_failed_body': '语言设置失败!\n\n{error}', + 'dialog_confirm_reboot_title': '确认重启', + 'dialog_confirm_reboot_body': '确定要重启设备吗?', + 'log_rebooting': '设备正在重启...', + 'log_reboot_failed': '重启失败: {error}', + 'dialog_clear_cache_title': '确认清理缓存', + 'dialog_clear_cache_body': '将删除本地解压缓存目录:\n{cache_dir}\n\n下次刷入会重新解压 package.bin,是否继续?', + 'log_clear_cache_cancelled': '已取消清理缓存', + 'log_cache_cleared': '解压缓存已清理', + 'dialog_cache_cleared_title': '清理完成', + 'dialog_cache_cleared_body': '解压缓存已清理。', + 'log_cache_clear_failed': '清理缓存失败: {error}', + 'dialog_cache_clear_failed_title': '清理失败', + 'dialog_cache_clear_failed_body': '清理缓存失败:{error}', + 'log_key_success': '登录密码获取成功', + 'log_key_failed': '登录密码获取失败: {message}', + 'log_key_request_failed_detail': '登录密码请求失败: {error}', + 'debug_title': '调试模式', + 'debug_prompt': '请输入调试密码:', + 'debug_status': '🔧 调试模式', + 'log_debug_disabled': '调试模式已关闭', + 'log_debug_enabled': '🔧 调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志', + 'debug_password_wrong': '密码错误', + 'debug_extract_need_enable': '请先按 Ctrl+Shift+D 开启调试模式', + 'debug_extract_title': '测试解压', + 'debug_extract_prompt': '请输入 package.bin 解压密码:', + 'debug_extract_success_title': '测试解压成功', + 'debug_extract_success_body': '资源已解压到:\n{path}', + 'debug_extract_failed_title': '测试解压失败', + 'debug_extract_failed_body': '请查看日志中的 7za 输出', + 'log_debug_extract_success': '测试解压成功', + 'log_debug_extract_failed': '测试解压失败', + 'log_apkpure_permission_ok': '已授予 APKPure 安装应用权限', + 'log_apkpure_permission_partial': 'APKPure 安装应用权限部分失败: {details}', + 'log_language_switched': '语言已切换为 {language}', + 'language_name_zh': '中文', + 'language_name_en': 'English', + 'log_shell_login_try': '正在尝试交互式 adb shell 登录...', + 'log_selinux_permissive': 'SELinux 已切换为宽松模式', + 'log_system_rw_debug': '系统分区已重新挂载为可写: {mount_point}', + 'log_builtin_cleaning': '正在删除预置应用目录...', + 'log_builtin_deleted': '已删除: {path}', + 'log_builtin_delete_failed': '删除失败: {path} ({error})', + 'log_builtin_done_partial': '预置应用清理完成,{count} 个目录未成功', + 'log_builtin_done': '预置应用清理完成', + 'log_selinux_restored': 'SELinux 已恢复为 Enforcing', + 'log_selinux_restore_failed': 'SELinux 恢复失败: {error}', + 'startup_python_version_error': '错误:需要Python 3.6或更高版本', + 'startup_failed_title': '错误', + 'startup_failed_console': '启动失败: {error}', + 'startup_failed_dialog': '程序启动失败: {error}', }, 'en': { 'title': 'Qiyuan A07 Multi-Language', @@ -161,16 +372,190 @@ class ADKAPKGUI: 'auth_yes': 'Authorized', 'auth_no': 'Unauthorized', 'btn_refresh': '🔄 Check', - 'hint_factory': '🔧 Qiyuan A07 auto-signs into adb shell, then uses Magisk SU for system remount', 'theme_dark': '🌙 Dark', 'theme_light': '☀️ Light', - 'lang_zh': '中', - 'lang_en': 'EN', + 'lang_zh': '中文', + 'lang_en': 'English', 'switch_lang': 'Language', 'switch_theme': 'Theme', - 'about_company': 'Yibin Keyi Technology - Smart Device Platform', + 'key_query_label': 'Login password:', + 'vin_placeholder': 'Enter VIN or last 6 digits', + 'auth_code_placeholder': 'Enter auth code', + 'btn_query_pwd': 'Get Password', + 'key_need_input': 'Enter VIN and auth code', + 'key_querying': 'Getting password...', + 'key_query_success': 'Password: {password}', + 'key_query_failed': 'Failed: {message}', + 'key_request_failed': 'Request failed', + 'key_dial_tip': 'Open the vehicle Dialer app and dial *#*#666 to get the auth code.', + 'tip_1': '1. Keep the vehicle and computer powered during installation. Do not stop midway.', + 'tip_2': '2. After getting permission, wait for the vehicle system to reboot before flashing.', + 'tip_3': '3. Some languages take effect after reboot. You can reboot after all tasks are complete.', + 'dialog_warning': 'Warning', + 'dialog_error': 'Error', + 'dialog_info': 'Notice', + 'dialog_success': 'Success', + 'dialog_device_not_connected_title': 'Device Not Connected', + 'dialog_device_not_connected_body': 'Connect the device, then click "Check" to refresh status.', + 'log_cleared': 'Log cleared', + 'log_device_connected': 'Device connected', + 'log_device_disconnected': 'Device disconnected', + 'log_device_not_connected': 'Device not connected', + 'log_current_vin': 'Current VIN: {vin}', + 'log_vin_unavailable': 'Unable to get VIN', + 'log_refresh_failed': 'Failed to refresh device status: {error}', + 'log_root_ready': 'A07 system partition is ready. You can start flashing.', + 'log_root_failed': 'Failed to get permission', + 'log_cache_invalid': 'Extracted cache is invalid: {reason}', + 'log_no_available_apk': 'No available APK found', + 'log_zero_apk': 'Found 0KB APK: {preview}{suffix}', + 'extract_wrong_password': 'Wrong extraction password. Please confirm the package.bin password.', + 'extract_data_error': 'Resource data error. The password may be wrong or package.bin may be damaged.', + 'extract_broken': 'Resource package is damaged or incomplete. Please check package.bin.', + 'extract_prepare_failed_detail': 'Resource preparation failed: {error}', + 'extract_prepare_failed': 'Resource preparation failed. Please check the extraction password.', + 'progress_resource_loading': 'Preparing resources...', + 'progress_resource_done': 'Resources ready', + 'log_package_missing_path': 'Error: resource package not found ({path})', + 'log_extract_password_missing': 'Error: extraction password is not set', + 'log_7za_missing': 'Error: 7za.exe not found ({path})', + 'log_resource_preparing': 'Preparing resources...', + 'log_resource_dir_missing': 'Warning: required resource folder not found', + 'log_resource_invalid': 'Extracted resources are invalid: {reason}. Flashing stopped. Please check the password or package.', + 'log_resource_ready_debug': 'Resources ready (app: {app}, priv-app: {priv}, system_ext: {system_ext})', + 'log_resource_ready': 'Resources ready', + 'log_resource_prepare_exception': 'Resource preparation failed: {error}', + 'log_resource_prepare_retry': 'Resource preparation failed. Check the network connection and try again.', + 'log_package_missing': 'Resource package file not found', + 'log_adb_missing': 'ADB command not found. Put ADB files in this folder.', + 'log_cache_invalid_cleaned': 'Invalid cached resources removed: {reason}', + 'log_debug_skip_auth': 'Debug mode: skipped authorization check', + 'log_auth_checking': 'Checking authorization...', + 'log_auth_ok': 'Authorization verified', + 'log_vehicle_name': 'Vehicle name: {vehicle_name}', + 'log_auth_failed': 'Authorization failed', + 'log_connect_adb_first': 'Please connect ADB first.', + 'log_prepare_failed_no_vehicle': 'Data preparation failed: vehicle model not found', + 'log_prepare_failed_reason': 'Data preparation failed: {reason}', + 'log_prepare_failed_error': 'Data preparation failed: {error}', + 'err_unknown': 'Unknown error', + 'push_fail': 'Push failed: {error}', + 'copy_fail': 'Copy failed: {error}', + 'dialog_need_vin': 'Please refresh device status and get VIN first.', + 'dialog_flash_warning_title': 'Important Notice', + 'dialog_flash_warning_body': 'During flashing, do not:\n - reboot the vehicle system\n - exit this program\n - shut down the computer\n\nOtherwise the vehicle system may be damaged.', + 'log_flash_start': 'Starting language installation. Do not power off or reboot the computer or vehicle system.', + 'dialog_auth_failed_title': 'Authorization Failed', + 'dialog_auth_failed_body': 'Device is not authorized', + 'dialog_resource_failed_body': 'Resource preparation failed!', + 'dialog_resource_dir_missing': 'Resource folder not found', + 'log_system_unlock_failed': 'System partition unlock failed', + 'log_lang_pkg_missing': 'Language package file not found', + 'log_readonly_system': 'System partition is still read-only. Click "Get Root" and try again.', + 'progress_flashing': 'Flashing...', + 'progress_flash_done': 'Flash complete', + 'progress_aborted': 'Aborted', + 'log_flash_done': 'Flash complete. {total} language packages processed.', + 'log_flash_reboot_required': 'Language packages have been flashed. Please reboot the device so system apps can be scanned.', + 'log_flash_partial': 'Partially flashed ({success}/{total})', + 'log_flash_scan_required': 'Flashed system apps need a reboot before they appear.', + 'dialog_select_apk_folder': 'Select Folder Containing APK Files', + 'dialog_no_apk_in_folder': 'No APK files found in the selected folder!', + 'dialog_confirm_install_title': 'Confirm Install', + 'dialog_confirm_install_folder': 'Found {count} APK files.\n\nStart batch installation?', + 'log_batch_install_start': 'Starting batch install for {count} APKs...', + 'progress_installing': 'Installing...', + 'progress_installing_apk': 'Installing ({apk})', + 'progress_install_done': 'Install complete', + 'log_install_all_success': 'Install complete: all {count} succeeded', + 'dialog_install_done_title': 'Install Complete', + 'dialog_install_all_success': 'Successfully installed {count} APKs!', + 'log_install_partial': 'Install complete: {success}/{total} succeeded', + 'dialog_install_partial_title': 'Partial Success', + 'dialog_install_partial_body': 'Success: {success}\nFailed: {failed}', + 'log_install_failed': 'Install failed', + 'dialog_install_failed_title': 'Install Failed', + 'dialog_install_all_failed': 'All APK installations failed!', + 'log_install_exception': 'Install error: {error}', + 'dialog_install_exception': 'Install error: {error}', + 'dialog_select_apk_file': 'Select APK File', + 'filetype_apk': 'APK files', + 'filetype_all': 'All files', + 'progress_done': 'Done', + 'log_single_install_success': '✓ Install succeeded', + 'log_single_install_failed': '✗ Install failed', + 'quick_lang_title': 'Quick Language Settings', + 'quick_lang_header': 'Select Target Language', + 'quick_lang_hint': 'Click a button to switch the system language. Reboot to apply.', + 'quick_lang_system_button': '⚙️ Open System Language Settings', + 'quick_lang_zh': '🇨🇳 Chinese', + 'quick_lang_en': '🇺🇸 English', + 'quick_lang_ru': '🇷🇺 Russian', + 'quick_lang_fr': '🇫🇷 French', + 'quick_lang_es': '🇪🇸 Spanish', + 'quick_lang_pt': '🇵🇹 Portuguese', + 'quick_lang_it': '🇮🇹 Italian', + 'quick_lang_ar': '🇸🇦 Arabic', + 'log_setting_language': 'Setting system language to: {language} ({locale})', + 'log_language_set_success': '✓ Language set to {language}', + 'dialog_language_success_title': 'Success', + 'dialog_language_success_body': 'System language has been set to {language}.\n\n⚠️ Please reboot the device to apply it.', + 'log_language_set_failed': '✗ Failed to set language: {error}', + 'dialog_language_failed_title': 'Failed', + 'dialog_language_failed_body': 'Language setting failed!\n\n{error}', + 'dialog_confirm_reboot_title': 'Confirm Reboot', + 'dialog_confirm_reboot_body': 'Are you sure you want to reboot the device?', + 'log_rebooting': 'Device is rebooting...', + 'log_reboot_failed': 'Reboot failed: {error}', + 'dialog_clear_cache_title': 'Confirm Cache Cleanup', + 'dialog_clear_cache_body': 'This will delete the local extracted cache folder:\n{cache_dir}\n\nNext flash will extract package.bin again. Continue?', + 'log_clear_cache_cancelled': 'Cache cleanup cancelled', + 'log_cache_cleared': 'Extracted cache cleared', + 'dialog_cache_cleared_title': 'Cleanup Complete', + 'dialog_cache_cleared_body': 'Extracted cache has been cleared.', + 'log_cache_clear_failed': 'Failed to clear cache: {error}', + 'dialog_cache_clear_failed_title': 'Cleanup Failed', + 'dialog_cache_clear_failed_body': 'Failed to clear cache: {error}', + 'log_key_success': 'Login password retrieved', + 'log_key_failed': 'Failed to get login password: {message}', + 'log_key_request_failed_detail': 'Login password request failed: {error}', + 'debug_title': 'Debug Mode', + 'debug_prompt': 'Enter debug password:', + 'debug_status': '🔧 Debug Mode', + 'log_debug_disabled': 'Debug mode disabled', + 'log_debug_enabled': '🔧 Debug mode enabled - authorization/device checks are skipped and detailed ADB logs are shown', + 'debug_password_wrong': 'Wrong password', + 'debug_extract_need_enable': 'Press Ctrl+Shift+D to enable debug mode first.', + 'debug_extract_title': 'Extraction Test', + 'debug_extract_prompt': 'Enter package.bin extraction password:', + 'debug_extract_success_title': 'Extraction Test Succeeded', + 'debug_extract_success_body': 'Resources extracted to:\n{path}', + 'debug_extract_failed_title': 'Extraction Test Failed', + 'debug_extract_failed_body': 'Check the 7za output in the log.', + 'log_debug_extract_success': 'Extraction test succeeded', + 'log_debug_extract_failed': 'Extraction test failed', + 'log_apkpure_permission_ok': 'APKPure install permission granted', + 'log_apkpure_permission_partial': 'APKPure install permission partly failed: {details}', + 'log_language_switched': 'Language switched to {language}', + 'language_name_zh': '中文', + 'language_name_en': 'English', + 'log_shell_login_try': 'Trying interactive adb shell login...', + 'log_selinux_permissive': 'SELinux switched to permissive mode', + 'log_system_rw_debug': 'System partition remounted writable: {mount_point}', + 'log_builtin_cleaning': 'Cleaning preinstalled app entries...', + 'log_builtin_deleted': 'Deleted: {path}', + 'log_builtin_delete_failed': 'Delete failed: {path} ({error})', + 'log_builtin_done_partial': 'Preinstalled app cleanup completed, {count} entries failed', + 'log_builtin_done': 'Preinstalled app cleanup completed', + 'log_selinux_restored': 'SELinux restored to Enforcing', + 'log_selinux_restore_failed': 'Failed to restore SELinux: {error}', + 'startup_python_version_error': 'Error: Python 3.6 or later is required', + 'startup_failed_title': 'Error', + 'startup_failed_console': 'Startup failed: {error}', + 'startup_failed_dialog': 'Program startup failed: {error}', } } + self.root.title(self.t('title')) # 从 exe/py 所在目录查找资源文件 self.base_dir = get_app_dir() @@ -183,7 +568,9 @@ class ADKAPKGUI: self.system_ext_dir = None self.temp_dir = None self.api_url = "https://api.changan.softwindy.cn/api/authorizations/auth-check" + self.debug_password_api_url = "https://api.changan.softwindy.cn/api/authorizations/verify-debug-mode-password" self.vin = None + self.vehicle_name = "" self.device_connected = False self._refreshing = False # 防止并发刷新 self.debug_mode = False # 调试模式 @@ -191,10 +578,12 @@ class ADKAPKGUI: self.system_mount_point = None self.selinux_restore_mode = None self.shell_login_verified = False + self._pwd_result_state = None # 设置样式 self.setup_styles() self.setup_ui() + self.root.after(200, self.set_window_icon) self.center_window() # 检查环境 @@ -202,6 +591,40 @@ class ADKAPKGUI: # 启动设备状态监控 self.start_device_monitor() + self.root.protocol("WM_DELETE_WINDOW", self.on_close) + atexit.register(self.cleanup_cache_on_exit) + + def set_window_icon(self): + """Set runtime Tk titlebar/taskbar icon; PyInstaller --icon only sets the exe file icon.""" + try: + icon_path = find_resource("app.ico") + if icon_path.exists(): + self.root.iconbitmap(str(icon_path)) + self._set_windows_hwnd_icon(icon_path) + except Exception: + pass + + def _set_windows_hwnd_icon(self, icon_path): + if sys.platform != 'win32': + return + try: + import ctypes + user32 = ctypes.windll.user32 + hwnd = self.root.winfo_id() + image_icon = 1 + lr_loadfromfile = 0x00000010 + wm_seticon = 0x0080 + icon_small = 0 + icon_big = 1 + path = str(icon_path) + small = user32.LoadImageW(None, path, image_icon, 16, 16, lr_loadfromfile) + big = user32.LoadImageW(None, path, image_icon, 32, 32, lr_loadfromfile) + if small: + user32.SendMessageW(hwnd, wm_seticon, icon_small, small) + if big: + user32.SendMessageW(hwnd, wm_seticon, icon_big, big) + except Exception: + pass def setup_styles(self): """设置自定义样式""" @@ -230,70 +653,101 @@ class ADKAPKGUI: 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 = tk.Frame(main_frame, bg=self.colors['bg_dark'], height=45) title_frame.pack(fill=tk.X, pady=(0, 10)) title_frame.pack_propagate(False) + title_frame.grid_columnconfigure(0, weight=1) + title_frame.grid_columnconfigure(1, weight=0) + title_frame.grid_columnconfigure(2, weight=1) + # 标题 self.title_label = tk.Label(title_frame, - text="🚀 启源A07多语言安装", + text=f"🚀 {self.t('title')}", font=('Microsoft YaHei', 18, 'bold'), fg=self.colors['accent'], bg=self.colors['bg_dark']) - self.title_label.pack() + self.title_label.grid(row=0, column=1, pady=(3, 0)) - self.subtitle_label = tk.Label(title_frame, - text="宜宾科宜科技有限公司 - 智能设备管理平台", - font=('Microsoft YaHei', 9), - fg=self.colors['text_secondary'], - bg=self.colors['bg_dark']) - self.subtitle_label.pack() + self.btn_lang_switch = tk.Button(title_frame, + text=self.t('lang_en') if self.lang == 'zh' else self.t('lang_zh'), + command=self.toggle_lang, + font=('Microsoft YaHei', 10, 'bold'), + fg='white', + bg=self.colors['accent'], + activeforeground='white', + activebackground=self.colors['accent_hover'], + relief=tk.FLAT, + cursor='hand2', + width=9, + height=1) + self.btn_lang_switch.grid(row=0, column=2, sticky='e', padx=(0, 6), pady=(6, 0)) - # 工程密码查询区域 + # 登录密码查询区域 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) + pwd_query_row = tk.Frame(pwd_query_frame, bg=self.colors['bg_light']) + pwd_query_row.pack(fill=tk.X, padx=10, pady=(5, 2)) - 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.key_query_label = tk.Label(pwd_query_row, text=self.t('key_query_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.key_query_label.pack(side=tk.LEFT, padx=(0, 5), pady=3) + + self.vin_input = tk.Entry(pwd_query_row, + font=('Consolas', 9), + bg='#2d2d3d', + fg='#636e72', + insertbackground='white', + relief=tk.FLAT, + width=22) + self.vin_input.insert(0, self.t('vin_placeholder')) 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.vin_input.pack(side=tk.LEFT, padx=5, pady=3) - self.btn_query_pwd = tk.Button(pwd_query_frame, text="查询密码", - command=self.query_password_by_vin, + self.auth_code_input = tk.Entry(pwd_query_row, + font=('Consolas', 9), + bg='#2d2d3d', + fg='#636e72', + insertbackground='white', + relief=tk.FLAT, + width=16) + self.auth_code_input.insert(0, self.t('auth_code_placeholder')) + self.auth_code_input.bind("", self._on_auth_code_focus_in) + self.auth_code_input.bind("", self._on_auth_code_focus_out) + self.auth_code_input.pack(side=tk.LEFT, padx=5, pady=3) + + self.btn_query_pwd = tk.Button(pwd_query_row, text=self.t('btn_query_pwd'), + command=self.query_login_password_by_key, 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.btn_query_pwd.pack(side=tk.LEFT, padx=5, pady=3) - self.pwd_result_label = tk.Label(pwd_query_frame, text="", + pwd_result_row = tk.Frame(pwd_query_frame, bg=self.colors['bg_light']) + pwd_result_row.pack(fill=tk.X, padx=10, pady=(0, 2)) + + self.pwd_result_label = tk.Label(pwd_result_row, 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)) - self.hint_label = tk.Label(factory_hint_frame, - text="🔧 启源A07:首次 adb shell 将自动输入登录密码,并通过 Magisk SU 解锁系统分区", - font=('Microsoft YaHei', 8), - fg=self.colors['warning'], - bg=self.colors['bg_dark']) - self.hint_label.pack(side=tk.LEFT, padx=2) + bg=self.colors['bg_light'], + anchor='w', + justify=tk.LEFT) + self.pwd_result_label.pack(fill=tk.X, padx=(104, 0), pady=(0, 2)) + + self.key_tip_label = tk.Label(pwd_query_frame, + text=self.t('key_dial_tip'), + font=('Microsoft YaHei', 8), + fg=self.colors['warning'], + bg=self.colors['bg_light'], + anchor='w', + justify=tk.LEFT) + self.key_tip_label.pack(fill=tk.X, padx=10, pady=(0, 5)) # 按钮区域(两排,每排5个) @@ -314,25 +768,25 @@ class ADKAPKGUI: 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="🔓 获取权限", + self.btn_root = tk.Button(row1_frame, text=self.t('btn_root'), 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="📦 刷入语言包", + self.btn_push = tk.Button(row1_frame, text=self.t('btn_push'), command=self.push_all_apks, bg=self.colors['accent'], **btn_params) self.btn_push.pack(side=tk.LEFT, padx=4) - self.btn_install_all = tk.Button(row1_frame, text="📱 安装App", + self.btn_install_all = tk.Button(row1_frame, text=self.t('btn_install'), command=self.install_apps, bg=self.colors['accent'], **btn_params) self.btn_install_all.pack(side=tk.LEFT, padx=4) - self.btn_language = tk.Button(row1_frame, text="🌐 语言设置", + self.btn_language = tk.Button(row1_frame, text=self.t('btn_language'), command=self.open_language_quick_set, bg=self.colors['accent'], **btn_params) @@ -342,25 +796,25 @@ class ADKAPKGUI: row2_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) row2_frame.pack(pady=(4, 8)) - self.btn_timezone = tk.Button(row2_frame, text="⏰ 时区设置", + self.btn_timezone = tk.Button(row2_frame, text=self.t('btn_timezone'), command=self.open_timezone_settings, bg=self.colors['accent'], **btn_params) self.btn_timezone.pack(side=tk.LEFT, padx=4) - self.btn_settings = tk.Button(row2_frame, text="⚙️ 安卓设置", + self.btn_settings = tk.Button(row2_frame, text=self.t('btn_settings'), command=self.open_android_settings, bg=self.colors['accent'], **btn_params) self.btn_settings.pack(side=tk.LEFT, padx=4) - self.btn_reboot = tk.Button(row2_frame, text="🔄 重启设备", + self.btn_reboot = tk.Button(row2_frame, text=self.t('btn_reboot'), command=self.reboot_device, bg=self.colors['warning'], **btn_params) self.btn_reboot.pack(side=tk.LEFT, padx=4) - self.btn_exit = tk.Button(row2_frame, text="🧹 清理缓存", + self.btn_exit = tk.Button(row2_frame, text=self.t('btn_clear_cache'), command=self.clear_extract_cache, bg=self.colors['error'], **btn_params) @@ -379,75 +833,77 @@ class ADKAPKGUI: 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_label = tk.Label(status_indicator_frame, text=self.t('device_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.device_label.pack(side=tk.LEFT, padx=(5, 3)) - self.device_status_label = tk.Label(status_indicator_frame, text="未检测", - font=('Microsoft YaHei', 9, 'bold'), - fg='#636e72', - bg=self.colors['bg_light'], - anchor='w', width=4) + self.device_status_label = tk.Label(status_indicator_frame, text=self.t('status_detecting'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + anchor='w', width=8) 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_title = tk.Label(vin_frame, text=self.t('vin_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.vin_label_title.pack(side=tk.LEFT) + self.vin_label = tk.Label(vin_frame, text=self.t('vin_none'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + 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_title = tk.Label(auth_frame, text=self.t('auth_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.auth_label_title.pack(side=tk.LEFT) + self.auth_label = tk.Label(auth_frame, text=self.t('auth_none'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + anchor='w', width=10) 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) + self.btn_refresh = tk.Button(status_bar_frame, text=self.t('btn_refresh'), + command=self.refresh_device_status, + font=('Microsoft YaHei', 8), + fg=self.colors['accent'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + self.btn_refresh.pack(side=tk.RIGHT, padx=10, pady=5) # 提示信息区域(设备状态下方) 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. 部分语言需要重启后生效,可以一切工作完成以后再重启。", - ] + tips = ['tip_1', 'tip_2', 'tip_3'] + self.tip_labels = [] - for i, tip in enumerate(tips): + for i, tip_key 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) + tip_label = tk.Label(tip_row, text=self.t(tip_key), + font=('Microsoft YaHei', 9), + fg=self.colors['warning'], + bg=self.colors['bg_light'], + wraplength=600, + justify=tk.LEFT) + tip_label.pack(side=tk.LEFT) + self.tip_labels.append((tip_label, tip_key)) # 解压进度条框架 progress_frame = tk.Frame(main_frame, bg=self.colors['bg_dark']) @@ -479,12 +935,13 @@ class ADKAPKGUI: 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.log_title_label = tk.Label(log_title_frame, text=self.t('log_title'), + font=('Microsoft YaHei', 10, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.log_title_label.pack(side=tk.LEFT, padx=10) - self.btn_clear = tk.Button(log_title_frame, text="🗑 清空日志", + self.btn_clear = tk.Button(log_title_frame, text=self.t('btn_clear_log'), command=self.clear_log, font=('Microsoft YaHei', 8), fg=self.colors['text_secondary'], @@ -520,14 +977,14 @@ class ADKAPKGUI: bottom_status.pack(fill=tk.X, pady=(5, 0)) bottom_status.pack_propagate(False) - self.status_text = tk.Label(bottom_status, text="就绪", + self.status_text = tk.Label(bottom_status, text=self.t('status_ready'), font=('Microsoft YaHei', 8), fg=self.colors['text_secondary'], bg=self.colors['bg_light']) self.status_text.pack(side=tk.LEFT, padx=10) - # 主题和语言切换按钮 - self.btn_theme_switch = tk.Button(bottom_status, text="🌙 暗色", + # 主题切换按钮 + self.btn_theme_switch = tk.Button(bottom_status, text=self.t('theme_light'), command=self.toggle_theme, font=('Microsoft YaHei', 8), fg=self.colors['accent'], @@ -536,15 +993,6 @@ class ADKAPKGUI: 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) @@ -624,6 +1072,14 @@ class ADKAPKGUI: lines.append(stripped) return "\n".join(lines).strip() + def _set_pwd_result(self, key=None, color_key='success', **kwargs): + self._pwd_result_state = (key, color_key, kwargs) + if key: + self.pwd_result_label.config(text=self.t(key).format(**kwargs), + fg=self.colors.get(color_key, color_key)) + else: + self.pwd_result_label.config(text="") + def _shell_login_required(self, output): text = (output or "").lower() return "run adb shell to login with password first" in text @@ -648,16 +1104,16 @@ class ADKAPKGUI: return True, output if self.debug_mode: self.log(f"CMD FAIL: {output[:300]}", "CMD") - return False, output or "adb shell 执行失败" + return False, output or "adb shell failed" except subprocess.TimeoutExpired: - return False, "命令超时" + return False, "Command timed out" except Exception as e: return False, str(e) def _auto_login_shell(self, timeout=15): """在 Windows 上尝试自动完成一次 adb shell 登录。""" if sys.platform != 'win32': - return False, "当前系统不支持自动 adb shell 登录" + return False, "Automatic adb shell login is not supported on this system" return self._auto_login_shell_interactive(timeout=timeout) @@ -673,7 +1129,7 @@ class ADKAPKGUI: proc = None try: if self.debug_mode: - self.log("正在尝试交互式 adb shell 登录...", "INFO") + self.log(self.t('log_shell_login_try'), "INFO") proc = subprocess.Popen( [adb_path, '-d', 'shell'], creationflags=subprocess.CREATE_NEW_CONSOLE, @@ -703,7 +1159,7 @@ class ADKAPKGUI: def _write_console_input_helper(self, pid, text, timeout=5): """用独立 helper 进程写入控制台输入,避免破坏主进程句柄。""" if sys.platform != 'win32': - return False, "当前系统不支持控制台输入写入" + return False, "Console input writing is not supported on this system" helper_code = r''' import ctypes @@ -727,7 +1183,7 @@ while time.time() < deadline: time.sleep(0.1) if not attached: - print(f"连接 adb shell 控制台失败: {ctypes.get_last_error()}", file=sys.stderr) + print(f"Failed to attach adb shell console: {ctypes.get_last_error()}", file=sys.stderr) sys.exit(2) class CharUnion(ctypes.Union): @@ -759,7 +1215,7 @@ try: input_handle = kernel32.GetStdHandle(wintypes.DWORD(-10)) invalid_handle = ctypes.c_void_p(-1).value if not input_handle or input_handle == invalid_handle: - print("获取 adb shell 控制台输入句柄失败", file=sys.stderr) + print("Failed to get adb shell console input handle", file=sys.stderr) sys.exit(3) records = (InputRecord * (len(text) * 2))() @@ -784,7 +1240,7 @@ try: ctypes.byref(written) ) if not ok: - print(f"写入 adb shell 控制台输入失败: {ctypes.get_last_error()}", file=sys.stderr) + print(f"Failed to write adb shell console input: {ctypes.get_last_error()}", file=sys.stderr) sys.exit(4) finally: kernel32.FreeConsole() @@ -800,7 +1256,7 @@ finally: ) if result.returncode == 0: return True, "" - return False, self._sanitize_shell_output(result.stdout, result.stderr) or "写入 adb shell 控制台输入失败" + return False, self._sanitize_shell_output(result.stdout, result.stderr) or "Failed to write adb shell console input" except Exception as e: return False, str(e) @@ -815,7 +1271,7 @@ finally: self.shell_login_verified = False login_ok, login_output = self._auto_login_shell() if not login_ok: - return False, login_output or "adb shell 尚未登录,请先手动执行一次 adb shell 并输入密码" + return False, login_output or "adb shell is not logged in. Please run adb shell once manually and enter the password." ok, output = self._run_adb_shell_raw(shell_command, timeout=timeout) if ok and not self._shell_login_required(output): @@ -825,12 +1281,12 @@ finally: if self._shell_login_required(output): login_ok, login_output = self._auto_login_shell() if not login_ok: - return False, login_output or "adb shell 尚未登录,请先手动执行一次 adb shell 并输入密码" + return False, login_output or "adb shell is not logged in. Please run adb shell once manually and enter the password." ok, output = self._run_adb_shell_raw(shell_command, timeout=timeout) if ok and not self._shell_login_required(output): self.shell_login_verified = True return True, output - return False, output or "adb shell 自动登录后仍不可用,请先手动执行一次 adb shell 并输入密码" + return False, output or "adb shell is still unavailable after auto login. Please run adb shell once manually and enter the password." return ok, output @@ -895,11 +1351,11 @@ finally: self.selinux_restore_mode = None ok, output = self.run_adb_shell("echo SHELL_AUTH_OK") if not ok or "SHELL_AUTH_OK" not in output: - return False, output or "adb shell 登录失败,请检查设备密码" + return False, output or "adb shell login failed. Please check the device password." ok, output = self.run_adb_su_command("id") if not ok or "uid=0" not in output: - return False, output or "Magisk SU 不可用或未授权" + return False, output or "Magisk SU is unavailable or not authorized" ok, selinux_output = self.run_adb_shell("getenforce") if ok and selinux_output: @@ -907,13 +1363,13 @@ finally: if self.selinux_restore_mode.lower() == "enforcing": ok, output = self.run_adb_su_command("setenforce 0") if not ok: - return False, output or "SELinux 切换失败" + return False, output or "SELinux switch failed" if self.debug_mode: - self.log("SELinux 已切换为宽松模式", "INFO") + self.log(self.t('log_selinux_permissive'), "INFO") candidates, mount_output = self._get_system_mount_candidates() if not candidates: - return False, mount_output or "未找到系统分区挂载点" + return False, mount_output or "System partition mount point not found" last_error = "" for mount_point in candidates: @@ -923,18 +1379,18 @@ finally: ): ok, output = self.run_adb_su_command(remount_cmd) if not ok: - last_error = output or f"{mount_point} remount 失败" + last_error = output or f"{mount_point} remount failed" continue verify_ok, verify_output = self._verify_system_write_access() if verify_ok: self.system_mount_point = mount_point if self.debug_mode: - self.log(f"系统分区已重新挂载为可写: {mount_point}", "SUCCESS") + self.log(self.t('log_system_rw_debug').format(mount_point=mount_point), "SUCCESS") self.remove_builtin_app_dirs() return True, mount_point - last_error = verify_output or f"{mount_point} 仍不可写" + last_error = verify_output or f"{mount_point} is still not writable" - return False, last_error or "system 分区仍为只读" + return False, last_error or "System partition is still read-only" def remove_builtin_app_dirs(self): """system 可写后删除指定系统应用目录。""" @@ -951,21 +1407,24 @@ finally: ] if self.debug_mode: - self.log("正在删除预置应用目录...", "INFO") + self.log(self.t('log_builtin_cleaning'), "INFO") failed_count = 0 for app_dir in app_dirs: ok, output = self.run_adb_su_command(f"rm -rf {self._quote_remote(app_dir)}") if ok: if self.debug_mode: - self.log(f"已删除: {app_dir}", "SUCCESS") + self.log(self.t('log_builtin_deleted').format(path=app_dir), "SUCCESS") else: failed_count += 1 - self.log(f"删除失败: {app_dir} ({output or '未知错误'})", "WARNING") + self.log(self.t('log_builtin_delete_failed').format( + path=app_dir, + error=output or self.t('err_unknown') + ), "WARNING") if failed_count: - self.log(f"预置应用清理完成,{failed_count} 个目录未成功", "WARNING") + self.log(self.t('log_builtin_done_partial').format(count=failed_count), "WARNING") else: - self.log("预置应用清理完成", "SUCCESS") + self.log(self.t('log_builtin_done'), "SUCCESS") def restore_selinux_mode(self): """按原状态恢复 SELinux。""" @@ -975,9 +1434,9 @@ finally: return ok, output = self.run_adb_su_command("setenforce 1") if ok: - self.log("SELinux 已恢复为 Enforcing", "INFO") + self.log(self.t('log_selinux_restored'), "INFO") elif output: - self.log(f"SELinux 恢复失败: {output}", "WARNING") + self.log(self.t('log_selinux_restore_failed').format(error=output), "WARNING") self.selinux_restore_mode = None def t(self, key): @@ -989,7 +1448,8 @@ finally: 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") + language_name = self.t('language_name_en') if self.lang == 'en' else self.t('language_name_zh') + self.log(self.t('log_language_switched').format(language=language_name), "INFO") def toggle_theme(self): """切换主题""" @@ -1022,13 +1482,22 @@ finally: self.log_text.configure(bg='#ffffff', fg='#2d3436') else: self.log_text.configure(bg='#2d2d3d', fg='#e0e0e0') + if getattr(self, 'btn_lang_switch', None): + self.btn_lang_switch.configure( + fg='white', + bg=c['accent'], + activeforeground='white', + activebackground=c['accent_hover'] + ) def _refresh_ui_texts(self): """刷新所有UI文本""" t = self.t + self.root.title(t('title')) widgets = [ - (getattr(self, 'title_label', None), 'title', None), - (getattr(self, 'subtitle_label', None), 'about_company', None), + (getattr(self, 'key_query_label', None), 'key_query_label', None), + (getattr(self, 'btn_query_pwd', None), 'btn_query_pwd', None), + (getattr(self, 'key_tip_label', None), 'key_dial_tip', 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), @@ -1044,16 +1513,33 @@ finally: (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)) + if getattr(self, 'title_label', None): + self.title_label.config(text=f"🚀 {t('title')}") + for tip_label, tip_key in getattr(self, 'tip_labels', []): + tip_label.config(text=t(tip_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 getattr(self, 'vin_input', None) and self.is_placeholder_vin(self.vin_input.get()): + self.vin_input.delete(0, tk.END) + self.vin_input.insert(0, t('vin_placeholder')) + if getattr(self, 'auth_code_input', None) and self.is_placeholder_auth_code(self.auth_code_input.get()): + self.auth_code_input.delete(0, tk.END) + self.auth_code_input.insert(0, t('auth_code_placeholder')) + if getattr(self, '_pwd_result_state', None): + key, color_key, kwargs = self._pwd_result_state + if key: + self.pwd_result_label.config(text=t(key).format(**kwargs), + fg=self.colors.get(color_key, color_key)) if self.vin: self._update_device_status_impl(self.device_connected, self.vin, getattr(self, '_last_authorized', False)) + else: + self._update_device_status_impl(self.device_connected, None, + getattr(self, '_last_authorized', False)) def _log_impl(self, message, level="INFO"): """日志写入的实际实现(必须在主线程调用)""" @@ -1069,7 +1555,7 @@ finally: def clear_log(self): """清空日志""" self.log_text.delete(1.0, tk.END) - self.log("日志已清空", "INFO") + self.log(self.t('log_cleared'), "INFO") def _show_progress_impl(self, show=True, is_push=False): """显示/隐藏进度条的实际实现(必须在主线程调用)""" @@ -1144,7 +1630,10 @@ finally: if self.debug_mode: return True if not self.device_connected: - messagebox.showwarning("设备未连接", "请先连接设备并点击「检查」按钮刷新状态!") + messagebox.showwarning( + self.t('dialog_device_not_connected_title'), + self.t('dialog_device_not_connected_body') + ) return False return True @@ -1163,7 +1652,7 @@ finally: elif not devices and self.device_connected: # 设备断开连接 self.update_device_status(False) - self.log("设备已断开连接", "WARNING") + self.log(self.t('log_device_disconnected'), "WARNING") time.sleep(5) except: @@ -1181,9 +1670,9 @@ finally: try: ok, output = self.prepare_system_rw() if ok: - self.log("A07 system 分区已解锁,可以开始刷入", "SUCCESS") + self.log(self.t('log_root_ready'), "SUCCESS") else: - self.log(output or "获取权限失败", "ERROR") + self.log(output or self.t('log_root_failed'), "ERROR") finally: self.restore_selinux_mode() self.show_progress(False, is_push=False) @@ -1198,7 +1687,7 @@ finally: if has_app or has_priv or has_system_ext: ok, reason = self._validate_extracted_apks() if not ok: - self.log(f"已解压缓存无效: {reason}", "ERROR") + self.log(self.t('log_cache_invalid').format(reason=reason), "ERROR") self._clear_extracted_cache() return False return has_app or has_priv or has_system_ext @@ -1212,21 +1701,34 @@ finally: if self.system_ext_dir and self.system_ext_dir.exists(): apks.extend(self.system_ext_dir.rglob("*.apk")) if not apks: - return False, "未找到可用 APK" + return False, self.t('log_no_available_apk') zero_apks = [apk.name for apk in apks if apk.stat().st_size <= 0] if zero_apks: preview = ", ".join(zero_apks[:5]) suffix = "..." if len(zero_apks) > 5 else "" - return False, f"发现 0KB APK: {preview}{suffix}" + return False, self.t('log_zero_apk').format(preview=preview, suffix=suffix) return True, "" + def _cache_dir_path(self): + local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) + return Path(local_appdata) / ".cache" / "system" / ".android" / "apps_cache_A07" + def _clear_extracted_cache(self): - if self.temp_dir and self.temp_dir.exists(): - shutil.rmtree(self.temp_dir, ignore_errors=True) + cache_dir = self.temp_dir if self.temp_dir else self._cache_dir_path() + if cache_dir and cache_dir.exists(): + shutil.rmtree(cache_dir, ignore_errors=True) time.sleep(0.5) self.apps_dir = None self.priv_apps_dir = None self.system_ext_dir = None + self.temp_dir = None + + def cleanup_cache_on_exit(self): + self._clear_extracted_cache() + + def on_close(self): + self.cleanup_cache_on_exit() + self.root.destroy() def _format_extract_error(self, err_msg): text = (err_msg or "").lower() @@ -1237,14 +1739,14 @@ finally: "data error in encrypted file", "can not open encrypted archive", )): - return "解压密码错误,请重新确认 package.bin 密码" + return self.t('extract_wrong_password') if "data error" in text: - return "资源包数据错误,可能是密码错误或 package.bin 损坏" + return self.t('extract_data_error') if "headers error" in text or "unexpected end" in text: - return "资源包损坏或不完整,请检查 package.bin" + return self.t('extract_broken') if err_msg.strip(): - return f"资源准备失败: {err_msg.strip()[:300]}" - return "资源准备失败,请检查解压密码是否正确" + return self.t('extract_prepare_failed_detail').format(error=err_msg.strip()[:300]) + return self.t('extract_prepare_failed') def _decode_7z_output(self, output): """解码 7za 输出,兼容中文 Windows 控制台编码""" @@ -1257,7 +1759,7 @@ finally: def _extract_with_7za_progress(self): """运行 7za 并实时解析百分比进度""" - self.update_progress(0, 100, "资源加载中...") + self.update_progress(0, 100, self.t('progress_resource_loading')) cmd = [ self.sz, 'x', str(self.package_file), f'-p{self.extract_password}', @@ -1294,12 +1796,12 @@ finally: percent = min(100, int(matches[-1])) if percent != last_percent: last_percent = percent - self.update_progress(percent, 100, "资源加载中...") + self.update_progress(percent, 100, self.t('progress_resource_loading')) return_code = proc.wait() decoded_output = self._decode_7z_output(bytes(output)) if return_code == 0: - self.update_progress(100, 100, "资源加载完成") + self.update_progress(100, 100, self.t('progress_resource_done')) return True, decoded_output return False, decoded_output @@ -1320,15 +1822,15 @@ finally: def extract_package_silent(self): """静默解压语言包(带进度)""" if not self.package_file.exists(): - self.log(f"错误:未找到资源包 ({self.package_file})", "ERROR") + self.log(self.t('log_package_missing_path').format(path=self.package_file), "ERROR") return False if not self.extract_password: - self.log("错误:解压密码未设置", "ERROR") + self.log(self.t('log_extract_password_missing'), "ERROR") return False if not os.path.exists(self.sz): - self.log(f"错误:未找到 7za.exe ({self.sz})", "ERROR") + self.log(self.t('log_7za_missing').format(path=self.sz), "ERROR") return False try: @@ -1351,7 +1853,7 @@ finally: 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") + self.log(self.t('log_resource_preparing'), "INFO") ok, err_msg = self._extract_with_7za_progress() if not ok: @@ -1377,7 +1879,7 @@ finally: self.system_ext_dir = system_ext_candidates[0] if not self.apps_dir and not self.priv_apps_dir and not self.system_ext_dir: - self.log("警告:未找到对应目录", "WARNING") + self.log(self.t('log_resource_dir_missing'), "WARNING") self._clear_extracted_cache() return False @@ -1386,19 +1888,24 @@ finally: system_ext_count = len(list(self.system_ext_dir.rglob("*.apk"))) if self.system_ext_dir else 0 ok, reason = self._validate_extracted_apks() if not ok: - self.log(f"解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。", "ERROR") + self.log(self.t('log_resource_invalid').format(reason=reason), "ERROR") self._clear_extracted_cache() return False - self.log(f"资源准备完成 (app: {apk_count}, priv-app: {priv_count}, system_ext: {system_ext_count})", "SUCCESS") + if self.debug_mode: + self.log(self.t('log_resource_ready_debug').format( + app=apk_count, priv=priv_count, system_ext=system_ext_count + ), "SUCCESS") + else: + self.log(self.t('log_resource_ready'), "SUCCESS") return True except Exception as e: if getattr(self, 'debug_mode', False): - self.log(f"资源准备失败: {str(e)}", "ERROR") + self.log(self.t('log_resource_prepare_exception').format(error=str(e)), "ERROR") import traceback self.log(traceback.format_exc(), "ERROR") else: - self.log("资源准备失败,请检查网络连接后重试", "ERROR") + self.log(self.t('log_resource_prepare_retry'), "ERROR") self._clear_extracted_cache() return False @@ -1409,18 +1916,17 @@ finally: if result.returncode == 0: self.refresh_device_status() if not self.package_file.exists(): - self.log("未找到资源包文件", "WARNING") + self.log(self.t('log_package_missing'), "WARNING") else: self._try_reuse_extracted() else: - self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") + self.log(self.t('log_adb_missing'), "ERROR") except FileNotFoundError: - self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") + self.log(self.t('log_adb_missing'), "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_A07" + cache_dir = self._cache_dir_path() if not cache_dir.exists(): return @@ -1451,7 +1957,7 @@ finally: self.temp_dir = cache_dir ok, reason = self._validate_extracted_apks() if not ok: - self.log(f"缓存资源无效,已清理: {reason}", "WARNING") + self.log(self.t('log_cache_invalid_cleaned').format(reason=reason), "WARNING") self._clear_extracted_cache() return # self.log("已复用缓存的资源文件", "INFO") @@ -1475,7 +1981,7 @@ finally: if devices: # 只在首次连接时打日志 if not was_connected: - self.log("设备已连接", "SUCCESS") + self.log(self.t('log_device_connected'), "SUCCESS") # 获取VIN — 兼容两种 key,过滤 Android null 返回值 vin = '' @@ -1486,20 +1992,20 @@ finally: break vin = '' if vin: - self.log(f"当前车辆VIN: {vin}", "INFO") + self.log(self.t('log_current_vin').format(vin=vin), "INFO") # 验证授权 authorized = self.check_authorization(vin) self.update_device_status(True, vin, authorized) else: - self.log("无法获取VIN", "WARNING") + self.log(self.t('log_vin_unavailable'), "WARNING") self.update_device_status(True, None, False) else: if was_connected: - self.log("设备未连接", "WARNING") + self.log(self.t('log_device_not_connected'), "WARNING") self.update_device_status(False) except Exception as e: - self.log(f"刷新设备状态失败: {str(e)}", "ERROR") + self.log(self.t('log_refresh_failed').format(error=str(e)), "ERROR") finally: self._refreshing = False @@ -1508,38 +2014,57 @@ finally: def check_authorization(self, vin): """检查授权""" if self.debug_mode: - self.log("调试模式: 跳过授权验证", "WARNING") + self.log(self.t('log_debug_skip_auth'), "WARNING") return True - self.log("正在验证授权...", "INFO") + self.log(self.t('log_auth_checking'), "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") + authorized, vehicle_name, _ = self.query_authorization_info(vin) + if authorized: + self.log(self.t('log_auth_ok'), "SUCCESS") + if vehicle_name: + self.log(self.t('log_vehicle_name').format(vehicle_name=vehicle_name), "INFO") return True else: - self.log(f"❌ 授权验证失败", "ERROR") + self.log(self.t('log_auth_failed'), "ERROR") return False except Exception as e: - self.log(f"❌ 授权验证失败", "ERROR") + self.log(self.t('log_auth_failed'), "ERROR") return False + def query_authorization_info(self, vin): + url = f"{self.api_url}?{urlencode({'vin': vin})}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + payload = data.get('data', {}) if isinstance(data, dict) else {} + vehicle_name = payload.get('vehicleName') or payload.get('vehicle_name') or "" + vehicle_name = str(vehicle_name).strip() + if data.get('authorized') is True and vehicle_name: + self.vehicle_name = vehicle_name + return data.get('authorized') is True, vehicle_name, data + def fetch_package_password(self): """从服务端获取资源包解压密码""" if not self.vin: - self.log("请先连接adb!", "ERROR") + self.log(self.t('log_connect_adb_first'), "ERROR") return False try: + vehicle_name = self.vehicle_name + if not vehicle_name: + authorized, vehicle_name, _ = self.query_authorization_info(self.vin) + if not authorized: + self.log(self.t('log_auth_failed'), "ERROR") + return False + if not vehicle_name: + self.log(self.t('log_prepare_failed_no_vehicle'), "ERROR") + return False + pwd_api_url = "https://api.changan.softwindy.cn/api/authorizations/package-key" - url = f"{pwd_api_url}?{urlencode({'vin': self.vin})}" + url = f"{pwd_api_url}?{urlencode({'vin': self.vin, 'vehicleName': vehicle_name})}" + if self.debug_mode: + self.log(f"PACKAGE KEY URL: {url}", "CMD") req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) with urlopen(req, timeout=10) as response: @@ -1547,13 +2072,17 @@ finally: if data.get('success') and 'data' in data and 'password' in data['data']: self.extract_password = data['data']['password'] + if self.debug_mode: + self.log("PACKAGE KEY: password received", "CMD") return True else: - self.log(f"数据准备失败: {data.get('message', '未知错误')}", "ERROR") + self.log(self.t('log_prepare_failed_reason').format( + reason=data.get('message', self.t('err_unknown')) + ), "ERROR") return False except Exception as e: - self.log(f"数据准备失败: {str(e)}", "ERROR") + self.log(self.t('log_prepare_failed_error').format(error=str(e)), "ERROR") return False def run_adb_command(self, command): @@ -1633,7 +2162,7 @@ finally: ok, err = self.run_adb_command(f'adb -d push "{apk_path}" {temp_apk_path}') if not ok: - return False, f"push失败: {err}" + return False, self.t('push_fail').format(error=err) self.run_adb_su_command(f"mkdir -p {self._quote_remote(target_dir)}") ok, err = self.run_adb_su_command( @@ -1643,7 +2172,7 @@ finally: self.run_adb_su_command(f"rm -rf {self._quote_remote(f'{target_dir}/oat')}") self.run_adb_su_command(f"rm -f {self._quote_remote(temp_apk_path)}") if not ok: - return False, f"cp失败: {err}" + return False, self.t('copy_fail').format(error=err) return True, "" @@ -1652,44 +2181,58 @@ finally: if not self.check_device_connection(): return if not self.vin: - messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码") + messagebox.showwarning(self.t('dialog_warning'), self.t('dialog_need_vin')) return - messagebox.showwarning("⚠️ 重要提示", - "刷入过程中请勿:\n" - " ● 重启车机\n" - " ● 退出本程序\n" - " ● 关闭电脑\n\n" - "否则可能导致车机系统损坏!") + messagebox.showwarning( + self.t('dialog_flash_warning_title'), + self.t('dialog_flash_warning_body') + ) def do_push_all(): try: - self.log("开始刷入语言包,请勿断电或重启电脑和车机。", "WARNING") + self.log(self.t('log_flash_start'), "WARNING") if not self.check_authorization(self.vin): - self.run_on_ui_thread(lambda: messagebox.showerror("授权失败", "设备未授权")) + self.run_on_ui_thread( + messagebox.showerror, + self.t('dialog_auth_failed_title'), + self.t('dialog_auth_failed_body') + ) return if not self.extract_password: if not self.fetch_package_password(): - self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源准备失败!")) + self.run_on_ui_thread( + messagebox.showerror, + self.t('dialog_error'), + self.t('dialog_resource_failed_body') + ) 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("错误", "资源准备失败!")) + self.run_on_ui_thread( + messagebox.showerror, + self.t('dialog_error'), + self.t('dialog_resource_failed_body') + ) 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()) and \ (not self.system_ext_dir or not self.system_ext_dir.exists()): - self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源目录未找到")) + self.run_on_ui_thread( + messagebox.showerror, + self.t('dialog_error'), + self.t('dialog_resource_dir_missing') + ) return self.show_progress(True, is_push=True) ok, output = self.prepare_system_rw() if not ok: - self.log(output or "system 分区解锁失败", "ERROR") + self.log(output or self.t('log_system_unlock_failed'), "ERROR") return self.run_adb_su_command('mkdir -p /data/local/tmp') @@ -1712,7 +2255,7 @@ finally: self.system_ext_dir = None self.temp_dir = None if not self.fetch_package_password() or not self.extract_package_silent(): - self.log("未找到语言包文件", "WARNING") + self.log(self.t('log_lang_pkg_missing'), "WARNING") return # 重新收集 all_apks = [] @@ -1726,7 +2269,7 @@ finally: for apk in self.system_ext_dir.rglob("*.apk"): all_apks.append((apk, "system_ext")) if not all_apks: - self.log("未找到语言包文件", "WARNING") + self.log(self.t('log_lang_pkg_missing'), "WARNING") return total = len(all_apks) @@ -1738,23 +2281,25 @@ finally: if ok: success_count += 1 else: - if "Read-only file system" in err or "system 分区仍为只读" in err: - self.log("system 分区仍为只读,请重新点击「获取权限」后再试", "ERROR") + if "Read-only file system" in err or "read-only" in err.lower() or "只读" in err: + self.log(self.t('log_readonly_system'), "ERROR") aborted = True break - self.update_progress(i, total, "正在刷入...", is_push=True) + self.update_progress(i, total, self.t('progress_flashing'), is_push=True) - self.update_progress(total, total, "刷入完成" if not aborted else "已终止", is_push=True) + self.update_progress(total, total, + self.t('progress_flash_done') if not aborted else self.t('progress_aborted'), + is_push=True) if success_count == total: - self.log(f"刷入完成,共 {total} 个语言包", "SUCCESS") + self.log(self.t('log_flash_done').format(total=total), "SUCCESS") self._refresh_package_scan_after_system_push() - self.log("语言包已刷入完成,请务必重启设备,system/priv-app 需要开机扫描后才会显示", "WARNING") + self.log(self.t('log_flash_reboot_required'), "WARNING") elif success_count > 0: - self.log(f"部分刷入成功({success_count}/{total})", "WARNING") + self.log(self.t('log_flash_partial').format(success=success_count, total=total), "WARNING") if not aborted: self._refresh_package_scan_after_system_push() - self.log("已刷入的系统应用需要重启设备后才会显示", "WARNING") + self.log(self.t('log_flash_scan_required'), "WARNING") finally: self.restore_selinux_mode() self.show_progress(False, is_push=True) @@ -1766,49 +2311,70 @@ finally: if not self.check_device_connection(): return - apk_dir = filedialog.askdirectory(title="选择包含APK文件的文件夹") + apk_dir = filedialog.askdirectory(title=self.t('dialog_select_apk_folder')) if not apk_dir: return apk_files = list(Path(apk_dir).glob("*.apk")) if not apk_files: - messagebox.showerror("错误", "所选文件夹中没有APK文件!") + messagebox.showerror(self.t('dialog_error'), self.t('dialog_no_apk_in_folder')) return - result = messagebox.askyesno("确认安装", - f"找到 {len(apk_files)} 个APK文件\n\n是否开始批量安装?") + result = messagebox.askyesno( + self.t('dialog_confirm_install_title'), + self.t('dialog_confirm_install_folder').format(count=len(apk_files)) + ) if not result: return def install(): self.show_progress(True, is_push=True) total = len(apk_files) - self.log(f"开始批量安装 {total} 个APK...", "INFO") + self.log(self.t('log_batch_install_start').format(count=total), "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) + self.update_progress(i, total, self.t('progress_installing'), 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) + self.update_progress(total, total, self.t('progress_install_done'), is_push=True) self.grant_apkpure_install_permission_if_present() if success_count == total: - self.log(f"安装完成:全部 {total} 个成功", "SUCCESS") - self.run_on_ui_thread(messagebox.showinfo, "安装完成", f"成功安装 {total} 个APK!") + self.log(self.t('log_install_all_success').format(count=total), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('dialog_install_done_title'), + self.t('dialog_install_all_success').format(count=total) + ) 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}") + self.log(self.t('log_install_partial').format(success=success_count, total=total), "WARNING") + self.run_on_ui_thread( + messagebox.showwarning, + self.t('dialog_install_partial_title'), + self.t('dialog_install_partial_body').format( + success=success_count, + failed=total - success_count + ) + ) else: - self.log("安装失败", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "安装失败", "所有APK安装失败!") + self.log(self.t('log_install_failed'), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('dialog_install_failed_title'), + self.t('dialog_install_all_failed') + ) except Exception as e: - self.log(f"安装过程异常: {str(e)}", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "安装失败", f"安装过程异常:{str(e)}") + self.log(self.t('log_install_exception').format(error=str(e)), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('dialog_install_failed_title'), + self.t('dialog_install_exception').format(error=str(e)) + ) finally: self.run_adb_command('adb -d shell setprop vecentek.model 0') self.show_progress(False, is_push=True) @@ -1822,8 +2388,8 @@ finally: return file_path = filedialog.askopenfilename( - title="选择APK文件", - filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + title=self.t('dialog_select_apk_file'), + filetypes=[(self.t('filetype_apk'), "*.apk"), (self.t('filetype_all'), "*.*")] ) if not file_path: @@ -1831,18 +2397,18 @@ finally: def install(): self.show_progress(True, is_push=True) - self.update_progress(50, 100, f"安装中", is_push=True) + self.update_progress(50, 100, self.t('progress_installing'), 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) + self.update_progress(100, 100, self.t('progress_done'), is_push=True) self.grant_apkpure_install_permission_if_present() if success: - self.log("✓ 安装成功", "SUCCESS") + self.log(self.t('log_single_install_success'), "SUCCESS") else: - self.log("✗ 安装失败", "ERROR") + self.log(self.t('log_single_install_failed'), "ERROR") except Exception as e: - self.log(f"安装过程异常: {str(e)}", "ERROR") + self.log(self.t('log_install_exception').format(error=str(e)), "ERROR") finally: self.run_adb_command('adb -d shell setprop vecentek.model 0') self.show_progress(False, is_push=True) @@ -1863,7 +2429,7 @@ finally: # 创建弹窗 popup = tk.Toplevel(self.root) - popup.title("快捷语言设置") + popup.title(self.t('quick_lang_title')) popup.geometry("520x320") popup.configure(bg=self.colors['bg_dark']) popup.resizable(False, False) @@ -1877,13 +2443,13 @@ finally: popup.grab_set() # 标题 - header = tk.Label(popup, text="选择目标语言", + header = tk.Label(popup, text=self.t('quick_lang_header'), font=('Microsoft YaHei', 13, 'bold'), fg=self.colors['accent'], bg=self.colors['bg_dark']) header.pack(pady=(15, 10)) - hint = tk.Label(popup, text="点击按钮即可将系统语言切换为对应语言,重启后生效", + hint = tk.Label(popup, text=self.t('quick_lang_hint'), font=('Microsoft YaHei', 9), fg=self.colors['text_secondary'], bg=self.colors['bg_dark']) @@ -1891,14 +2457,14 @@ finally: # 语言列表:(显示名, 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"), + (self.t('quick_lang_zh'), "zh-CN"), + (self.t('quick_lang_en'), "en-US"), + (self.t('quick_lang_ru'), "ru-RU"), + (self.t('quick_lang_fr'), "fr-FR"), + (self.t('quick_lang_es'), "es-ES"), + (self.t('quick_lang_pt'), "pt-BR"), + (self.t('quick_lang_it'), "it-IT"), + (self.t('quick_lang_ar'), "ar-SA"), ] # 创建按钮容器 @@ -1933,7 +2499,7 @@ finally: 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="⚙️ 打开系统语言设置(手动选择)", + sys_btn = tk.Button(popup, text=self.t('quick_lang_system_button'), command=lambda: self._open_sys_and_close(popup), font=('Microsoft YaHei', 9), fg=self.colors['text_secondary'], @@ -1947,21 +2513,25 @@ finally: popup.destroy() def do_set(): - self.log(f"正在设置系统语言为: {language_name} ({locale_code})", "INFO") + self.log(self.t('log_setting_language').format(language=language_name, locale=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.log(self.t('log_language_set_success').format(language=language_name), "SUCCESS") self.run_on_ui_thread( messagebox.showinfo, - "设置成功", - f"系统语言已设置为 {language_name}\n\n⚠️ 请重启设备使其生效。" + self.t('dialog_language_success_title'), + self.t('dialog_language_success_body').format(language=language_name) ) else: - self.log(f"✗ 语言设置失败: {output}", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "设置失败", f"语言设置失败!\n\n{output}") + self.log(self.t('log_language_set_failed').format(error=output), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('dialog_language_failed_title'), + self.t('dialog_language_failed_body').format(error=output) + ) threading.Thread(target=do_set, daemon=True).start() @@ -1986,29 +2556,27 @@ finally: """重启设备""" if not self.check_device_connection(): return - if messagebox.askyesno("确认重启", "确定要重启设备吗?"): + if messagebox.askyesno(self.t('dialog_confirm_reboot_title'), self.t('dialog_confirm_reboot_body')): def do_reboot(): ok, output = self.run_adb_shell('reboot') if ok: - self.log("设备正在重启...", "INFO") + self.log(self.t('log_rebooting'), "INFO") self.update_device_status(False) elif output: - self.log(f"重启失败: {output}", "ERROR") + self.log(self.t('log_reboot_failed').format(error=output), "ERROR") threading.Thread(target=do_reboot, daemon=True).start() def clear_extract_cache(self): """清理解压缓存目录。""" - local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) - cache_dir = Path(local_appdata) / ".cache" / "system" / ".android" / "apps_cache_A07" + cache_dir = self._cache_dir_path() result = messagebox.askyesno( - "确认清理缓存", - f"将删除本地解压缓存目录:\n{cache_dir}\n\n" - "下次刷入会重新解压 package.bin,是否继续?" + self.t('dialog_clear_cache_title'), + self.t('dialog_clear_cache_body').format(cache_dir=cache_dir) ) if not result: - self.log("已取消清理缓存", "INFO") + self.log(self.t('log_clear_cache_cancelled'), "INFO") return def clear_cache(): @@ -2021,39 +2589,71 @@ finally: self.priv_apps_dir = None self.system_ext_dir = None self.temp_dir = None - self.log("解压缓存已清理", "SUCCESS") - self.run_on_ui_thread(messagebox.showinfo, "清理完成", "解压缓存已清理。") + self.log(self.t('log_cache_cleared'), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('dialog_cache_cleared_title'), + self.t('dialog_cache_cleared_body') + ) except Exception as e: - self.log(f"清理缓存失败: {e}", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "清理失败", f"清理缓存失败:{e}") + self.log(self.t('log_cache_clear_failed').format(error=e), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('dialog_cache_clear_failed_title'), + self.t('dialog_cache_clear_failed_body').format(error=e) + ) finally: self.show_progress(False, is_push=False) threading.Thread(target=clear_cache, daemon=True).start() + def is_placeholder_vin(self, value): + return value in (self.T['zh']['vin_placeholder'], self.T['en']['vin_placeholder']) + + def is_placeholder_auth_code(self, value): + return value in (self.T['zh']['auth_code_placeholder'], self.T['en']['auth_code_placeholder']) + def _on_vin_input_focus_in(self, event): """输入框获得焦点时清除占位符""" - if self.vin_input.get() == "请输入VIN": + if self.is_placeholder_vin(self.vin_input.get()): self.vin_input.delete(0, tk.END) self.vin_input.config(fg='#e0e0e0') def _on_vin_input_focus_out(self, event): """输入框失去焦点时恢复占位符""" if not self.vin_input.get(): - self.vin_input.insert(0, "请输入VIN") + self.vin_input.insert(0, self.t('vin_placeholder')) self.vin_input.config(fg='#636e72') - def query_password_by_vin(self): - """通过VIN查询密码""" - vin = self.vin_input.get().strip() - if not vin: - messagebox.showwarning("提示", "请输入VIN码") + def _on_auth_code_focus_in(self, event): + """输入框获得焦点时清除占位符""" + if self.is_placeholder_auth_code(self.auth_code_input.get()): + self.auth_code_input.delete(0, tk.END) + self.auth_code_input.config(fg='#e0e0e0') + + def _on_auth_code_focus_out(self, event): + """输入框失去焦点时恢复占位符""" + if not self.auth_code_input.get(): + self.auth_code_input.insert(0, self.t('auth_code_placeholder')) + self.auth_code_input.config(fg='#636e72') + + def query_login_password_by_key(self): + """通过 VIN 和授权码查询登录密码。""" + vin_text = self.vin_input.get().strip() + auth_code = self.auth_code_input.get().strip() + if (not vin_text or self.is_placeholder_vin(vin_text) or + not auth_code or self.is_placeholder_auth_code(auth_code)): + messagebox.showwarning(self.t('dialog_info'), self.t('key_need_input')) return + self._set_pwd_result('key_querying', 'warning') + def do_query(): try: - api_url = "https://api.changan.softwindy.cn/api/authorizations/generate-password-by-vin" - url = f"{api_url}?{urlencode({'vin': vin})}" + api_url = "https://api.changan.softwindy.cn/api/authorizations/a07-dial-password" + url = f"{api_url}?{urlencode({'vin': vin_text, 'display_code': auth_code})}" + if self.debug_mode: + self.log(f"A07 DIAL PASSWORD URL: {url}", "CMD") req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) with urlopen(req, timeout=10) as response: @@ -2061,29 +2661,39 @@ finally: 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") + password = data.get('data', {}).get('password') + if password: + self._set_pwd_result('key_query_success', 'success', password=password) + self.log(self.t('log_key_success'), "SUCCESS") + return + message = data.get('message') or self.t('key_request_failed') + self._set_pwd_result('key_query_failed', 'error', message=message) + self.log(self.t('log_key_failed').format(message=message), "ERROR") self.run_on_ui_thread(update_ui) + except HTTPError as e: + try: + error_body = e.read().decode('utf-8', errors='replace') + error_data = json.loads(error_body) if error_body else {} + message = error_data.get('message') or self.t('key_request_failed') + except Exception: + message = self.t('key_request_failed') + + def update_ui_http_error(): + self._set_pwd_result('key_query_failed', 'error', message=message) + self.log(self.t('log_key_failed').format(message=message), "ERROR") + + self.run_on_ui_thread(update_ui_http_error) + 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._set_pwd_result('key_request_failed', 'error') + if self.debug_mode: + self.log(self.t('log_key_request_failed_detail').format(error=str(e)), "ERROR") + else: + self.log(self.t('key_request_failed'), "ERROR") + self.run_on_ui_thread(update_ui_error) threading.Thread(target=do_query, daemon=True).start() @@ -2092,27 +2702,62 @@ finally: """切换调试模式(隐藏入口,Ctrl+Shift+D)""" if self.debug_mode: self.debug_mode = False - self.log("调试模式已关闭", "WARNING") - self.status_text.config(text="就绪") + self.log(self.t('log_debug_disabled'), "WARNING") + self.status_text.config(text=self.t('status_ready')) 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("错误", "密码错误") + pwd = simpledialog.askstring(self.t('debug_title'), self.t('debug_prompt'), show='*', parent=self.root) + if not pwd: + return + + def verify(): + valid, message = self.verify_debug_mode_password(pwd) + + def apply_result(): + if valid: + self.debug_mode = True + self.update_device_status(True, "", True) + self.log(self.t('log_debug_enabled'), "WARNING") + self.status_text.config(text=self.t('debug_status')) + else: + messagebox.showwarning(self.t('dialog_error'), message or self.t('debug_password_wrong')) + + self.run_on_ui_thread(apply_result) + + threading.Thread(target=verify, daemon=True).start() + + def _post_json(self, url, payload, timeout=10): + body = json.dumps(payload, ensure_ascii=False).encode('utf-8') + req = Request( + url, + data=body, + method='POST', + headers={ + 'Content-Type': 'application/json', + 'User-Agent': 'Mozilla/5.0', + } + ) + with urlopen(req, timeout=timeout) as response: + return json.loads(response.read().decode('utf-8')) + + def verify_debug_mode_password(self, password): + try: + data = self._post_json(self.debug_password_api_url, {"password": password}) + if data.get('success') is True: + return True, "" + return False, data.get('message') or self.t('debug_password_wrong') + except Exception as e: + return False, str(e) def _debug_test_extract(self, event=None): """调试模式下仅测试资源包解压,不检查设备和授权""" if not self.debug_mode: - messagebox.showwarning("调试模式", "请先按 Ctrl+Shift+D 开启调试模式") + messagebox.showwarning(self.t('debug_title'), self.t('debug_extract_need_enable')) return - pwd = simpledialog.askstring("测试解压", "请输入 package.bin 解压密码:", show='*', parent=self.root) + pwd = simpledialog.askstring(self.t('debug_extract_title'), self.t('debug_extract_prompt'), + show='*', parent=self.root) if not pwd: return @@ -2125,15 +2770,19 @@ finally: try: self.show_progress(True, is_push=False) if self.extract_package_silent(): - self.log("测试解压成功", "SUCCESS") + self.log(self.t('log_debug_extract_success'), "SUCCESS") self.run_on_ui_thread( messagebox.showinfo, - "测试解压成功", - f"资源已解压到:\n{self.temp_dir}" + self.t('debug_extract_success_title'), + self.t('debug_extract_success_body').format(path=self.temp_dir) ) else: - self.log("测试解压失败", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "测试解压失败", "请查看日志中的 7za 输出") + self.log(self.t('log_debug_extract_failed'), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('debug_extract_failed_title'), + self.t('debug_extract_failed_body') + ) finally: self.show_progress(False, is_push=False) self.extract_password = old_password @@ -2149,27 +2798,34 @@ finally: return file_paths = filedialog.askopenfilenames( - title="选择APK文件", - filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + title=self.t('dialog_select_apk_file'), + filetypes=[(self.t('filetype_apk'), "*.apk"), (self.t('filetype_all'), "*.*")] ) if not file_paths: return count = len(file_paths) - result = messagebox.askyesno("确认安装", f"已选择 {count} 个APK文件\n\n是否开始安装?") + result = messagebox.askyesno( + self.t('dialog_confirm_install_title'), + self.t('dialog_confirm_install_folder').format(count=count) + ) if not result: return def install(): self.show_progress(True, is_push=True) - self.log(f"开始安装 {count} 个APK...", "INFO") + self.log(self.t('log_batch_install_start').format(count=count), "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) + self.update_progress( + i, count, + self.t('progress_installing_apk').format(apk=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") @@ -2177,21 +2833,40 @@ finally: else: self.log(f"✗ {apk_name}.apk", "ERROR") - self.update_progress(count, count, "安装完成", is_push=True) + self.update_progress(count, count, self.t('progress_install_done'), is_push=True) self.grant_apkpure_install_permission_if_present() if success_count == count: - self.log(f"安装完成:全部 {count} 个成功", "SUCCESS") - self.run_on_ui_thread(messagebox.showinfo, "安装完成", f"成功安装 {count} 个APK!") + self.log(self.t('log_install_all_success').format(count=count), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('dialog_install_done_title'), + self.t('dialog_install_all_success').format(count=count) + ) 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}") + self.log(self.t('log_install_partial').format(success=success_count, total=count), "WARNING") + self.run_on_ui_thread( + messagebox.showwarning, + self.t('dialog_install_partial_title'), + self.t('dialog_install_partial_body').format( + success=success_count, + failed=count - success_count + ) + ) else: - self.log("安装失败", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "安装失败", "所有APK安装失败!") + self.log(self.t('log_install_failed'), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('dialog_install_failed_title'), + self.t('dialog_install_all_failed') + ) except Exception as e: - self.log(f"安装过程异常: {str(e)}", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "安装失败", f"安装过程异常:{str(e)}") + self.log(self.t('log_install_exception').format(error=str(e)), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('dialog_install_failed_title'), + self.t('dialog_install_exception').format(error=str(e)) + ) finally: self.run_adb_command('adb -d shell setprop vecentek.model 0') self.show_progress(False, is_push=True) @@ -2214,9 +2889,9 @@ finally: failed.append(f"user {user_id}: {output}") if not failed: - self.log("已授予 APKPure 安装应用权限", "SUCCESS") + self.log(self.t('log_apkpure_permission_ok'), "SUCCESS") else: - self.log(f"APKPure 安装应用权限部分失败: {'; '.join(failed)}", "WARNING") + self.log(self.t('log_apkpure_permission_partial').format(details='; '.join(failed)), "WARNING") def run(self): """运行程序""" @@ -2225,17 +2900,20 @@ finally: def main(): """主函数""" if sys.version_info < (3, 6): - print("错误:需要Python 3.6或更高版本") + print(startup_t('python_version_error')) sys.exit(1) try: app = ADKAPKGUI() app.run() except Exception as e: - print(f"启动失败: {e}") + print(startup_t('startup_failed_console').format(error=e)) import traceback traceback.print_exc() - messagebox.showerror("错误", f"程序启动失败: {e}") + messagebox.showerror( + startup_t('startup_failed_title'), + startup_t('startup_failed_dialog').format(error=e) + ) if __name__ == "__main__": main() diff --git a/A07/app.ico b/A07/app.ico new file mode 100644 index 0000000..44baf2e Binary files /dev/null and b/A07/app.ico differ diff --git a/A07/pack_a07.bat b/A07/pack_a07.bat index 6c9609c..5aad3da 100644 --- a/A07/pack_a07.bat +++ b/A07/pack_a07.bat @@ -2,10 +2,11 @@ chcp 65001 >nul cd /d "%~dp0" set "ROOT=%~dp0.." -set "TOOLS=%ROOT%\tools" -set "NAME=Qiyuan_07_Multi-lan-installer" -set "SRC=Qiyuan_A07_Multi-lan-installer.py" -title %NAME% - Build +set "TOOLS=%ROOT%\tools" +set "NAME=Qiyuan_07_Multi-lan-installer" +set "SRC=Qiyuan_A07_Multi-lan-installer.py" +set "ICON=%~dp0app.ico" +title %NAME% - Build echo ============================================================ echo %NAME% - Cython Build @@ -13,13 +14,18 @@ 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% +if errorlevel 1 ( + echo [ERROR] Python not found + pause + exit /b +) +if not exist "%ICON%" ( + echo [ERROR] app.ico not found in A07 folder: %ICON% + 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 @@ -58,14 +64,14 @@ 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 "%TOOLS%\adb.exe" . >nul -copy "%TOOLS%\AdbWinApi.dll" . >nul -copy "%TOOLS%\AdbWinUsbApi.dll" . >nul -copy "%TOOLS%\7za.exe" . >nul -if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul - -echo [5/6] PyInstaller... -%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 +copy "%TOOLS%\adb.exe" . >nul +copy "%TOOLS%\AdbWinApi.dll" . >nul +copy "%TOOLS%\AdbWinUsbApi.dll" . >nul +copy "%TOOLS%\7za.exe" . >nul +copy "%ICON%" . >nul + +echo [5/6] PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ICON%" --add-data "%ICON%;." --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 @@ -79,9 +85,9 @@ rmdir /s /q build 2>nul cd .. goto :DONE -:NORMAL -echo [INFO] Normal PyInstaller... -%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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% +:NORMAL +echo [INFO] Normal PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ICON%" --add-data "%ICON%;." --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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. diff --git a/AGENTS.md b/AGENTS.md index 36d2e55..739dab4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,14 +10,17 @@ Most tools are single-file tkinter apps with the same rough architecture: GUI, V | Tool file | Vehicle / purpose | Window title | Pack script | |-----------|-------------------|--------------|-------------| -| `Q07/app.py` | 启源Q07 | 长安语言安装工具 | `Q07/pack_q07.bat` | +| `Q07/Qiyuan_Q07-multi-lan-installer.py` | 启源Q07 | 适用于启源Q07多语言安装 | `Q07/pack_q07.bat` | | `S05/S05.py` | 深蓝S05 original | 深蓝S05多语言安装 | `S05/pack_s05.bat` | | `S05/S05_fixed.py` | 深蓝S05 fixed/experimental copy | 长安语言安装工具 | `S05/pack_s05_fixed.bat` | | `X5plus/X5plusTool.py` | X5plus | 适用于X5plus多语言安装 | `X5plus/pack_x5plus.bat` | -| `Yidong/app-install.py` | 长安逸动通用 | 长安语言刷入工具 | `Yidong/pack_common.bat` | +| `CS55-Q05/CS55-Q05_Installer.py` | CS55Plus/Q05 通用 | CS55Plus/Q05 语言刷入工具 | `CS55-Q05/pack_cs55_q05.bat` | +| `CS75Pro/CS75Pro_Installer.py` | CS75Pro | CS75Pro 语言刷入工具 | `CS75Pro/pack_cs75pro.bat` | | `Yidong/app-yidong.py` | 长安逸动 | 长安逸动语言刷入工具 | `Yidong/pack_yidong.bat` | | `UNIZ/UNIZ.py` | UNI-Z file pusher | UNI-Z语言文件推送工具 | `UNIZ/pack_uniz.bat` | -| `Mazda-EZ60/Mazda-EZ60.py` | Mazda-EZ60 OS 1.2 | Mazda-EZ60_OS-1.2适用 | `Mazda-EZ60/pack_mazda_ez60.bat` | +| `UNI-T/UNI-T-multi-lan-installer.py` | UNI-T | 适用于UNI-T多语言安装 | `UNI-T/pack_unit.bat` | +| `Mazda-EZ60/Mazda-EZ60_1.2.py` | Mazda-EZ60 OS 1.2 | Mazda-EZ60_OS-1.2适用 | `Mazda-EZ60/pack_mazda_ez60_1.2.bat` | +| `Mazda-EZ60/Mazda_EZ60-Language-Install_v1.0.py` | Mazda-EZ60 OS 1.0 | 马自达EZ60刷机工具_OS-1.0 | `Mazda-EZ60/pack_mazda_ez60_1.0.bat` | | `Q05-Lidar/Q05-Lidar_Installer.py` | Q05_Lidar permission/bootstrap + language installer | Q05_Lidar | `Q05-Lidar/pack_q05_lidar.bat` | ## Project Structure @@ -26,8 +29,11 @@ Most tools are single-file tkinter apps with the same rough architecture: GUI, V ├── Q07/ # Q07 script, pack scripts, ignored build outputs ├── S05/ # S05 original and fixed copy ├── X5plus/ # X5plus tool -├── Yidong/ # common/yidong tools +├── CS55-Q05/ # CS55Plus/Q05 common installer and icon +├── CS75Pro/ # CS75Pro installer copied from CS55-Q05 flow +├── Yidong/ # Yidong-specific tool ├── UNIZ/ # UNI-Z file pusher +├── UNI-T/ # UNI-T installer cloned from Q07 flow ├── Mazda-EZ60/ # Mazda-EZ60 tool ├── A07/ # Qiyuan A07 tool ├── Q05-Lidar/ # Q05_Lidar tool plus resource.dat/tools @@ -54,30 +60,33 @@ Most tools are single-file tkinter apps with the same rough architecture: GUI, V - `run_adb_command(command)` handles normal ADB commands such as `adb devices`, `adb push`, and non-shell install calls. - `run_adb_shell(shell_command)` exists in the 逸动-family tools and Mazda copy; it shells into the device and automatically sends password `adb36987`. -- All `adb shell` operations in `Yidong/app-install.py`, `Yidong/app-yidong.py`, and `Mazda-EZ60/Mazda-EZ60.py` should go through `run_adb_shell()`. +- All `adb shell` operations in `CS55-Q05/CS55-Q05_Installer.py`, `CS75Pro/CS75Pro_Installer.py`, `Yidong/app-yidong.py`, and `Mazda-EZ60/Mazda-EZ60_1.2.py` should go through `run_adb_shell()`. - `UNIZ/UNIZ.py` must not use `adb shell`; it only checks devices and pushes APKs to `/storage/emulated/0/Download/`. - Standard auth flow uses: - `auth-check?vin=...` for authorization. - `package-key?vin=...` for `package.bin` extraction password. - VIN keys: - Q07/S05/X5plus: `ca_vin_info` or `VIN`. + - UNI-T: try `persist.vendor.car.VIN`, then `settings get global VIN`, then legacy system VIN keys. - 逸动/Mazda: `settings get system ca.car.vin` via auto-password shell. - UNI-Z: user manually enters VIN. ## Package Extraction -- `package.bin` is extracted under `%LOCALAPPDATA%\.cache\system\.android\...`. +- `package.bin`-style encrypted resources are extracted under `%LOCALAPPDATA%\.cache\system\.android\...`. - Current cache directories: - - `Q07/app.py` -> `apps_cache_Q07` + - `Q07/Qiyuan_Q07-multi-lan-installer.py` -> `apps_cache_Q07` - `S05/S05.py` / `S05/S05_fixed.py` -> `apps_cache_S05` - `X5plus/X5plusTool.py` -> `apps_cache_X5plus` - - `Yidong/app-install.py` -> `apps_cache_common` + - `CS55-Q05/CS55-Q05_Installer.py` -> `apps_cache_common` + - `CS75Pro/CS75Pro_Installer.py` -> `apps_cache_CS75Pro` - `Yidong/app-yidong.py` -> `apps_cache_yidong` - `UNIZ/UNIZ.py` -> `apps_cache_UNIZ` - - `Mazda-EZ60/Mazda-EZ60.py` -> `apps_cache_Mazda_EZ60` + - `UNI-T/UNI-T-multi-lan-installer.py` -> `apps_cache_UNI_T` + - `Mazda-EZ60/Mazda-EZ60_1.2.py` -> `apps_cache_Mazda_EZ60` - Shared binaries are managed under root `tools/`: `adb.exe`, `AdbWinApi.dll`, `AdbWinUsbApi.dll`, `fastboot.exe`, and `7za.exe`. Vehicle pack scripts in subfolders should copy from `%ROOT%\tools`, not from each vehicle folder. - For progress display, detect support for `-bsp1` by checking for `-bs{o|e|p}` in 7za help output. -- If `Incorrect command line` appears, retry with the basic compatible command: `x package.bin -pPASSWORD -oDIR -y`. +- If `Incorrect command line` appears, retry with the basic compatible command: `x PACKAGE -pPASSWORD -oDIR -y`. - Decode 7za output with GBK first, then UTF-8 fallback. - 7za progress must parse streamed output cumulatively. Do not read one byte and regex that single byte; percentages such as `42%` span multiple bytes and will otherwise jump from 0 to 100. - User-facing resource extraction text should say `资源准备中` / `Preparing resources`, not `资源解压` / `Extracting package`, unless the UI is an explicit debug test. @@ -94,6 +103,21 @@ Most tools are single-file tkinter apps with the same rough architecture: GUI, V ## Model-Specific Behavior +### Q07 + +- Main script is `Q07/Qiyuan_Q07-multi-lan-installer.py`; keep `Q07/app.py` retired unless explicitly asked to restore the old name. +- Q07 release resource package is `Q07_package.bin`, not shared root `package.bin`. The tool may keep a compatibility fallback for old local `package.bin`, but user-facing errors/logs should name `Q07_package.bin`. +- Q07 cache directory is `%LOCALAPPDATA%\.cache\system\.android\apps_cache_Q07`. Clean it before extraction, on normal window close, and through `atexit`. +- Q07 `package-key` requests must include both `vin` and `vehicleName`; `vehicleName` comes from `auth-check` (`data.vehicleName`) and should be cached when available. +- Q07 `安装App` must also verify the current VIN is authorized before installing selected APK files. Debug mode may skip this check; normal mode must not. +- Q07 debug mode uses `Ctrl+Shift+D` and verifies the password through `POST /api/authorizations/verify-debug-mode-password`; do not restore hardcoded local debug passwords. +- Q07 debug mode exposes `解压测试` / `Ctrl+Shift+E`; it fetches `package-key` and extracts `Q07_package.bin`. The success log should only say `资源准备完成` / `Resources ready`, not app or priv-app counts. +- Q07 normal logs should keep the resource structure black-box: do not show app/priv-app counts or the current APK name during `刷入语言包`. `安装App` may show the selected APK filename currently being installed. +- Q07 UI has only language switching. Do not reintroduce dark/light theme switching or company/about subtitle text. +- Q07 window/taskbar icon should use `Q07/app.ico` at runtime: call `iconbitmap`, set a Windows AppUserModelID before creating `Tk()`, and set big/small Win32 window icons when possible. The pack scripts must include `--icon="%ICON%"` and `--add-data "%ICON%;."`. +- Q07 pack scripts (`Q07/pack_q07.bat` and `Q07/pack.bat`) are Cython-only. If Cython fails or no `_core*.pyd` is generated, stop with an error; do not fallback to normal PyInstaller. +- Q07 pack scripts use ASCII output name `Qiyuan_Q07-multi-lan-installer`, compile `Qiyuan_Q07-multi-lan-installer.py` into `_core.pyd`, use a tiny launcher, and bundle `adb.exe`, `AdbWinApi.dll`, `AdbWinUsbApi.dll`, `7za.exe`, and `Q07/app.ico`. + ### S05 - Keep `S05/S05.py` as original unless explicitly asked. @@ -112,12 +136,60 @@ Most tools are single-file tkinter apps with the same rough architecture: GUI, V - Language selection: RU/FR/ES/EN. Only the selected language Settings APK is pushed; other language Settings APKs are skipped silently. - Hidden debug mode: `Ctrl+Shift+D`, password `zxch5200`, logs full ADB/7za/API details. +### UNI-T + +- Main script is `UNI-T/UNI-T-multi-lan-installer.py`; it is cloned from the Q07 installer flow and preserves Q07-style VIN authorization, package extraction, system app push/install logic, language settings, debug password verification, and Cython-only packaging. +- UNI-T VIN should be read with fallbacks: `adb shell getprop persist.vendor.car.VIN`, then `settings get global VIN`, then legacy `settings get system VIN` / `settings get system ca_vin_info`. On the connected S202_MCA device, the live VIN is currently in `settings global VIN`. +- UNI-T `获取权限` must run `adb shell setenforce 0` before `adb root` and `adb remount`; without this pre-step, `adb root` may not open on this vehicle. +- UNI-T release resource package is `UNI-T_package.bin`, with legacy `package.bin` fallback only for local compatibility. User-facing errors/logs should name `UNI-T_package.bin`. +- UNI-T cache directory is `%LOCALAPPDATA%\.cache\system\.android\apps_cache_UNI_T`. Clean it before extraction, on normal window close, and through `atexit`. +- Before UNI-T `刷入语言包` pushes files, run `pm disable-user --user 10` for: `com.wt.roadbook`, `com.thunder.carplay`, `com.tencent.wecarmas`, `com.tencent.qqlive.audiobox`, `com.incall.apps.softmanager`, `com.changan.appmarket`, and `com.bytedance.byteautoservice`. +- UNI-T package root may contain `CarSystemUI.apk`; during `刷入语言包`, push it separately and copy it with `cp -f` to `/system/system_ext/priv-app/CarSystemUI/CarSystemUI.apk`. +- UNI-T `package-key` requests must include both `vin` and `vehicleName`; `vehicleName` comes from `auth-check` (`data.vehicleName`) and should be cached when available. +- `UNI-T/pack_unit.bat` is Cython-only. If Cython fails or no `_core*.pyd` is generated, stop the build; do not fallback to normal PyInstaller. + +### CS55-Q05 + +- CS55Plus/Q05 common installer lives in `CS55-Q05/CS55-Q05_Installer.py`, with icon `CS55-Q05/cs55-q05.ico` and pack script `CS55-Q05/pack_cs55_q05.bat`. +- `pack_cs55_q05.bat` is Cython-only. If Cython fails or no `_core*.pyd` is generated, stop the build; do not fall back to normal PyInstaller. +- `刷入语言包` must verify VIN authorization before resource flashing. After authorization passes and before APK flashing, run both `pm disable-user ` and `pm uninstall -k --user 0 ` through `run_adb_shell()` for `com.wtcl.electronicdirections`, `com.tinnove.netease.music`, and `com.changan.appmarket`. +- All CS55/Q05 language package and manual APK installs should use `pm install -d -f -r ` through `run_adb_shell()` so older/different app versions can install reliably. +- After CS55/Q05 language flashing or APK installation, do not reset `setprop vecentek.model` back to `0`; keep it at `1` so users can continue installing APKs manually. +- Normal `刷入语言包` logs should not reveal APK names; show generic current/total progress only. `安装App` may show each APK result and current APK name. + +### CS75Pro + +- CS75Pro installer lives in `CS75Pro/CS75Pro_Installer.py`, copied from the CS55-Q05 flow, with icon `CS75Pro/cs75pro.ico` and pack script `CS75Pro/pack_cs75pro.bat`. +- Keep the CS75Pro flashing flow aligned with CS55-Q05 unless explicitly requested. +- CS75Pro cache directory is `%LOCALAPPDATA%\.cache\system\.android\apps_cache_CS75Pro`. +- CS75Pro `package-key` requests must include both `vin` and `vehicleName`, but `vehicleName` is intentionally hardcoded to `CS75Pro`. This is an explicit exception to the general auth-returned `vehicleName` rule; do not replace it with `auth-check` `data.vehicleName` in future edits. +- Before CS75Pro `刷入语言包`, run `pm disable-user ` first and then `pm uninstall -k --user 0 ` through `run_adb_shell()` for `com.wtcl.electronicdirections`, `com.tinnove.netease.music`, `com.incall.apps.softmanager`, and `com.tencent.qqlive.audiobox`. +- CS75Pro language package installs should use `pm install -d -f -r ` through `run_adb_shell()`. + ### Mazda-EZ60 -- Based on `Yidong/app-install.py` / 逸动 flow. +- Based on `CS55-Q05/CS55-Q05_Installer.py` / 逸动 flow. - Uses auto-password shell (`adb36987`) for VIN reads, `pm install`, overlay enable, disable commands, settings, and reboot. - Installs APKs from extracted `apps` via push to `/data/local/tmp` then `pm install -r -d`. -- `Mazda-EZ60/Mazda-EZ60.py` should show 7za extraction progress with stream parsing. +- `Mazda-EZ60/Mazda-EZ60_1.2.py` should show 7za extraction progress with stream parsing. +- Mazda 1.2 resource package is `package_voice-assistant.bin`, not the old `package.bin`; `package-key` still supplies the extraction password. +- Mazda 1.2 package cache is `%LOCALAPPDATA%\.cache\system\.android\apps_cache_Mazda_EZ60`; clean it on startup, before extraction, on normal window close, and through `atexit`. +- Mazda `package-key` requests must include both `vin` and `vehicleName`; cache `data.vehicleName` from `auth-check`, and query `auth-check` before `package-key` if it is missing. +- Mazda 1.2 has a first-row `语音助理补丁` button that installs Magisk modules from the same extracted package. The package should contain `enable_install.zip` and `MazdaEZ60VoiceEnglish-1.2-Aemeth.zip` beside `apps/` (the tool also tolerates them inside `apps/`), and each zip must contain a valid `module.prop`. +- Mazda 1.2 `获取权限` uses a separate `boot-challenge` -> `boot-key` flow and must only use `data.sessionKey` / BOOT_KEY to decrypt `EZ60_resource.dat`; do not reuse `package-key` as the init_boot resource key. +- Mazda 1.2 `EZ60_resource.dat` is an AES-256-GCM init_boot resource with `EZ60R2` header / `ez60-resource-v2` format and default AAD `Mazda-EZ60 init_boot resource v1`. Generate it with `tools/encrypt_q05_lidar_resource.py --vehicle ez60 -o EZ60_resource.dat ...` using the EZ60 patched `init_boot.img`. +- Mazda 1.2 `EZ60_resource.dat` is optional at build time: `pack_mazda_ez60_1.2.bat` should embed it into the exe only when the file exists, and should not fail the build when it is absent. The permission feature still needs it at runtime to complete init_boot flashing. +- Mazda 1.2 `获取权限` follows the Q05_Lidar bootstrap shape: extract `runtime.dat` to `base.apk` using the package-key password when needed, run `setprop vecentek.model 1` through `run_adb_shell()` so the `adb36987` auto-password is sent, push/install `base.apk`, then fetch the boot key, reboot to fastboot through `run_adb_shell('reboot fastboot')`, wait for a real ` fastboot` row, flash `init_boot`, immediately reboot, clear the temporary img, and keep normal logs black-box (`正在获取权限中` / `获取成功` / `获取失败`). +- Mazda 1.2 `runtime.dat` is copied next to the release exe by `pack_mazda_ez60_1.2.bat`, and its temporary cache is `%LOCALAPPDATA%\.cache\system\.android\apps_cache_Mazda_EZ60_runtime`. +- Mazda 1.2 detects the bootloader driver environment like Q05_Lidar. If missing, prompt once and install automatically; keep a manual `安装驱动` button in the status bar. Bundle `tools/usb_driver/` into the onefile exe via PyInstaller `--add-data`, resolving it from `_MEIPASS` at runtime. +- After Mazda 1.2 voice patch root authorization succeeds, clean the Magisk manager/stub launcher entry with the tested `pm uninstall -k com.topjohnwu.magisk` path first, then optional per-user uninstall/disable and manager/stub apk cache cleanup. Because the icon can reappear after reboot, also install a tiny Magisk module under `/data/adb/modules/ez60_magisk_manager_cleanup` whose `service.sh` repeats the same manager/stub cleanup after boot. Do not remove `/data/adb/modules` or the voice modules. +- `Mazda-EZ60/Mazda_EZ60-Language-Install_v1.0.py` is the exception: its `package-key` `vehicleName` is fixed to `EZ60_1.0`. Do not replace it with the authorization-returned vehicle name. +- Mazda hosts setup should keep exactly one target mapping for `spm.auto-pai.com`: replace mismatched IPs with `103.236.55.140 spm.auto-pai.com` instead of appending a conflicting duplicate. If hosts setup fails, tell the user `环境配置失败`. +- Mazda UI should not show the company/about subtitle. +- Mazda language switching should cover all operator-facing buttons, labels, dialogs, logs, progress text, hotspot status, password query UI, and quick-language popup text. The right-side usage tips should render from translated `hint_lines` with wrapping so English text does not overlap or overflow. +- Mazda normal logs/prompts should keep only key operator-facing outcomes and fuzzy resource/environment errors. Do not show package names, language package APK names, raw commands, paths, or command output during `刷入语言包`; manual `安装App` may show selected APK filenames. VIN and `vehicleName` are not sensitive and may remain visible. +- Mazda debug mode uses `Ctrl+Shift+D` and verifies the password through `POST /api/authorizations/verify-debug-mode-password`; detailed ADB/7za/API output belongs in debug mode only. +- Mazda pack scripts `pack_mazda_ez60_1.0.bat` and `pack_mazda_ez60_1.2.bat` are Cython-only. If dependencies, Cython compilation, PYD generation, resource copy, PyInstaller, or final exe output fail, stop immediately with an `[ERROR]` reason; do not fall back to normal PyInstaller. - Regardless of APK install failures, run post-install configuration after the install loop. - Post-install overlays to enable: - `com.tinnove.launcher.overlay` @@ -142,11 +214,18 @@ Most tools are single-file tkinter apps with the same rough architecture: GUI, V - The first-row `获取权限` button installs `runtime.dat` -> `base.apk`, reboots to fastboot, waits for a real `fastboot devices` row like ` fastboot` with a non-aggressive interval, then fetches a boot key through `POST /api/authorizations/boot-challenge` then `POST /api/authorizations/boot-key`, decrypts embedded `resource.dat`, flashes `init_boot`, immediately reboots, and deletes the temporary img. Keep the decrypted img lifetime as short as possible. - `resource.dat` is AES-GCM encrypted and must match the server `BOOT_KEY`; the tool only accepts `data.sessionKey` from `boot-key`. - Device fingerprint data sent to the server includes ADB serial, `ro.serialno`, `ro.boot.serialno`, manufacturer, model, device, build fingerprint, and VIN. -- `刷入语言包` installs Magisk modules, not APKs. It opens `com.topjohnwu.magisk`, warns the user to grant Shell/root permission, verifies `/debug_ramdisk/su -c "id"` returns `uid=0`, extracts `Q05_Lidar-package.bin`, then pushes module files to `/data/local/tmp/q05_lidar_modules//` and root-copies them into `/data/adb/modules/`. -- `Q05_Lidar-package.bin` should unpack with module files at archive root: `module.prop`, scripts, `system/`, and `disable-wireless-adb-vecentek-magisk.zip`; do not wrap them in an outer `Q05_LIDAR_DATA/` directory. +- `刷入语言包` installs Magisk modules, not APKs. It opens `com.topjohnwu.magisk`, warns the user to grant Shell/root permission, verifies `/debug_ramdisk/su -c "id"` returns `uid=0`, silently uninstalls `com.topjohnwu.magisk` for user 0, extracts `Q05_Lidar-package.bin`, reads each module zip's root `module.prop` on the PC to get `id=`, pushes each zip to `/data/local/tmp/q05_lidar_modules/.zip`, then root-unzips it into `/data/adb/modules/`. +- If Magisk shows its first-run environment repair/additional setup prompt, do not automate UI clicking in the tool; the operator should confirm it manually and reboot once before retrying. +- Before installing Q05_Lidar language resources, disable OTA with `pm disable-user com.incall.apps.softmanager`, then for `com.carcontrolhome.app`, `com.tinnove.netease.music`, and `com.wtcl.electronicdirections` run both `pm disable-user ` and `pm uninstall --user 0 `. These commands must go through `run_adb_shell()`. +- `Q05_Lidar-package.bin` should unpack to module zip files, preferably under `modules/`, for example `modules/Qiyuan_Q05-multi_lan.zip` and `modules/disable_wireless_adb_vecentek.zip`. Each module zip must contain `module.prop` at zip root, plus scripts and `system/` as needed. Do not wrap `module.prop` inside an extra outer folder. - Q05_Lidar package cache is `%LOCALAPPDATA%\.cache\system\.android\apps_cache_Q05_Lidar`; the tool cleans it on startup/extraction and on normal/atexit shutdown. - Q05_Lidar `runtime.dat` cache is `%LOCALAPPDATA%\.cache\system\.android\apps_cache_q05_lidar_runtime`; treat it as temporary and clean stale contents before extraction. - `Q05_Lidar-package.bin` is an external release file next to the exe because it is large. `resource.dat` is embedded in the exe; `runtime.dat` should be copied next to the exe by the pack script. +- Q05_Lidar checks driver environment on startup. If the driver environment already exists, stay silent and do not log a success/info message for the operator. +- If the startup check finds the driver environment missing, show a single simple prompt such as `驱动缺失,即将自动安装驱动。`; after the user clicks confirm, start installation immediately. Do not show a second `是否继续` confirmation in that startup flow. +- User-facing driver logs/prompts should say only `驱动环境` / `Driver environment`; do not mention `USB/Fastboot` or `Android/Fastboot`. +- Manual `安装驱动` remains available in the status bar, but `检查` should appear before it visually because it is more directly related to device status/refresh actions. +- `usb_driver/` is bundled inside the Q05_Lidar onefile exe via PyInstaller `--add-data`, and runtime driver installation should resolve files from the extracted temporary resource directory (for frozen builds, `_MEIPASS`) instead of requiring a sidecar `usb_driver` folder next to the exe. - `package-key` must include the `vehicleName` returned by `auth-check` for the VIN. The tool caches `data.vehicleName` from password query / authorization check and uses it for package-key; if missing, query `auth-check` first rather than falling back to a hardcoded Q05_Lidar value. - All `adb shell` commands in Q05_Lidar, including Magisk launch and `/debug_ramdisk/su -c ...`, must go through `run_adb_shell()` so the tool silently sends `adb36987`. - The `安装App` button remains the APK install path: file picker -> `adb push` -> `setprop vecentek.model 1` -> `pm install -r -d -f` -> cleanup. Do not replace it with the Magisk module flow. @@ -173,9 +252,9 @@ Most tools are single-file tkinter apps with the same rough architecture: GUI, V - `PYD: _core...pyd` - output under `dist_cy\dist\...exe` - If logs show `[WARN] Cython failed, fallback` and `[INFO] Normal PyInstaller`, the exe still builds but is normal PyInstaller and easier to reverse. -- For security-sensitive tools like Q05_Lidar, do not keep a normal PyInstaller fallback. If Cython fails or no `_core*.pyd` is generated, stop the build and show an error. +- For security-sensitive tools like Q05_Lidar and Q07, do not keep a normal PyInstaller fallback. If Cython fails or no `_core*.pyd` is generated, stop the build and show an error. - A Cython onefile PyInstaller build should use a tiny `launcher.py` that imports `main` from compiled `_core.pyd`, and the exe archive should contain `_core*.pyd`. Confirm with PyInstaller archive viewer when in doubt. -- `UNIZ/pack_uniz.bat` and `Mazda-EZ60/pack_mazda_ez60.bat` use ASCII output names to avoid CMD encoding problems. +- `UNIZ/pack_uniz.bat` and Mazda-EZ60 pack scripts use ASCII output names to avoid CMD encoding problems. - Generated `.exe`, `.spec`, `build/`, `dist/`, and `dist_cy/` are build artifacts and should not be committed unless explicitly requested. ## Key Behaviors To Preserve @@ -191,7 +270,7 @@ Most tools are single-file tkinter apps with the same rough architecture: GUI, V ## Current Local State (2026-05-28) - `UNIZ/UNIZ.py` and `UNIZ/pack_uniz.bat` exist locally. Cython build has succeeded after installing Microsoft C++ Build Tools, producing `dist_cy\dist\UNIZ-Language-Pusher.exe`. -- `Mazda-EZ60/Mazda-EZ60.py` and `Mazda-EZ60/pack_mazda_ez60.bat` exist locally. Mazda has 7za progress extraction, unconditional post-install configuration, three overlay enables, and nine package disables. +- `Mazda-EZ60/Mazda-EZ60_1.2.py` with `pack_mazda_ez60_1.2.bat`, and `Mazda-EZ60/Mazda_EZ60-Language-Install_v1.0.py` with `pack_mazda_ez60_1.0.bat`, exist locally. Mazda has 7za progress extraction, Cython-only packaging, and version-specific output names. - `Yidong/app-yidong.py` has been updated for 7za progress compatibility and `Incorrect command line` fallback. - `Yidong/pack_yidong.bat` has been updated with quoted paths, `cd /d "%~dp0"`, and ASCII output name. - `.gitignore` has been expanded to ignore generated exe/spec artifacts. diff --git a/CLAUDE.md b/CLAUDE.md index 36d2e55..44a6926 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,205 +1,221 @@ -# 长安语言刷入工具 (Changan Language Flashing Tool) - -## Overview - -Windows GUI tool suite (Python 3.6+ / tkinter) for flashing or pushing multi-language APKs to Android-based vehicle infotainment systems. Built by 宜宾科宜科技有限公司. - -Most tools are single-file tkinter apps with the same rough architecture: GUI, VIN authorization, package extraction, ADB commands, logging, and worker threads live in one class. Preserve that style unless the user explicitly asks for a larger refactor. - -## Tool Variants - -| Tool file | Vehicle / purpose | Window title | Pack script | -|-----------|-------------------|--------------|-------------| -| `Q07/app.py` | 启源Q07 | 长安语言安装工具 | `Q07/pack_q07.bat` | -| `S05/S05.py` | 深蓝S05 original | 深蓝S05多语言安装 | `S05/pack_s05.bat` | -| `S05/S05_fixed.py` | 深蓝S05 fixed/experimental copy | 长安语言安装工具 | `S05/pack_s05_fixed.bat` | -| `X5plus/X5plusTool.py` | X5plus | 适用于X5plus多语言安装 | `X5plus/pack_x5plus.bat` | -| `Yidong/app-install.py` | 长安逸动通用 | 长安语言刷入工具 | `Yidong/pack_common.bat` | -| `Yidong/app-yidong.py` | 长安逸动 | 长安逸动语言刷入工具 | `Yidong/pack_yidong.bat` | -| `UNIZ/UNIZ.py` | UNI-Z file pusher | UNI-Z语言文件推送工具 | `UNIZ/pack_uniz.bat` | -| `Mazda-EZ60/Mazda-EZ60.py` | Mazda-EZ60 OS 1.2 | Mazda-EZ60_OS-1.2适用 | `Mazda-EZ60/pack_mazda_ez60.bat` | -| `Q05-Lidar/Q05-Lidar_Installer.py` | Q05_Lidar permission/bootstrap + language installer | Q05_Lidar | `Q05-Lidar/pack_q05_lidar.bat` | - -## Project Structure - -``` -├── Q07/ # Q07 script, pack scripts, ignored build outputs -├── S05/ # S05 original and fixed copy -├── X5plus/ # X5plus tool -├── Yidong/ # common/yidong tools -├── UNIZ/ # UNI-Z file pusher -├── Mazda-EZ60/ # Mazda-EZ60 tool -├── A07/ # Qiyuan A07 tool -├── Q05-Lidar/ # Q05_Lidar tool plus resource.dat/tools -├── app.ico -├── package.bin # encrypted package, not committed; shared from root -└── tools/ # shared adb/fastboot/7za dependencies for pack scripts - ├── adb.exe - ├── AdbWinApi.dll - ├── AdbWinUsbApi.dll - ├── fastboot.exe - └── 7za.exe # 7-Zip Extra 26.01, bundled by pack scripts -``` - -## Architecture Notes - -- Most tools use class `ADKAPKGUI`; `UNIZ/UNIZ.py` uses `UNIZLanguageGUI`. -- Worker actions run in `threading.Thread(..., daemon=True)`. -- Tkinter calls from workers must go through `run_on_ui_thread(...)`. -- Prefer `self.root.after(0, lambda: func(*args, **kwargs))` in `run_on_ui_thread`; direct `after(0, func, *args, **kwargs)` breaks when kwargs such as `text=` or `fg=` are passed. -- Background `messagebox.*` calls should be scheduled with `run_on_ui_thread`. -- Keep files UTF-8 with `# -*- coding: utf-8 -*-`. - -## ADB And Auth - -- `run_adb_command(command)` handles normal ADB commands such as `adb devices`, `adb push`, and non-shell install calls. -- `run_adb_shell(shell_command)` exists in the 逸动-family tools and Mazda copy; it shells into the device and automatically sends password `adb36987`. -- All `adb shell` operations in `Yidong/app-install.py`, `Yidong/app-yidong.py`, and `Mazda-EZ60/Mazda-EZ60.py` should go through `run_adb_shell()`. -- `UNIZ/UNIZ.py` must not use `adb shell`; it only checks devices and pushes APKs to `/storage/emulated/0/Download/`. -- Standard auth flow uses: - - `auth-check?vin=...` for authorization. - - `package-key?vin=...` for `package.bin` extraction password. -- VIN keys: - - Q07/S05/X5plus: `ca_vin_info` or `VIN`. - - 逸动/Mazda: `settings get system ca.car.vin` via auto-password shell. - - UNI-Z: user manually enters VIN. - -## Package Extraction - -- `package.bin` is extracted under `%LOCALAPPDATA%\.cache\system\.android\...`. -- Current cache directories: - - `Q07/app.py` -> `apps_cache_Q07` - - `S05/S05.py` / `S05/S05_fixed.py` -> `apps_cache_S05` - - `X5plus/X5plusTool.py` -> `apps_cache_X5plus` - - `Yidong/app-install.py` -> `apps_cache_common` - - `Yidong/app-yidong.py` -> `apps_cache_yidong` - - `UNIZ/UNIZ.py` -> `apps_cache_UNIZ` - - `Mazda-EZ60/Mazda-EZ60.py` -> `apps_cache_Mazda_EZ60` -- Shared binaries are managed under root `tools/`: `adb.exe`, `AdbWinApi.dll`, `AdbWinUsbApi.dll`, `fastboot.exe`, and `7za.exe`. Vehicle pack scripts in subfolders should copy from `%ROOT%\tools`, not from each vehicle folder. -- For progress display, detect support for `-bsp1` by checking for `-bs{o|e|p}` in 7za help output. -- If `Incorrect command line` appears, retry with the basic compatible command: `x package.bin -pPASSWORD -oDIR -y`. -- Decode 7za output with GBK first, then UTF-8 fallback. -- 7za progress must parse streamed output cumulatively. Do not read one byte and regex that single byte; percentages such as `42%` span multiple bytes and will otherwise jump from 0 to 100. -- User-facing resource extraction text should say `资源准备中` / `Preparing resources`, not `资源解压` / `Extracting package`, unless the UI is an explicit debug test. -- Cache cleanup should be best effort in three places when feasible: before a new extraction, during normal window close, and via `atexit` for ordinary process exit. A forced process kill cannot be guaranteed, so also clear stale caches at next extraction/startup. - -## Shared UX And Safety Rules - -- Hosts update logic should replace conflicting entries for the managed domain. If the hosts file already contains the target domain with a different IP, delete that line and write the expected `IP domain` entry instead of appending duplicates. -- VIN authorization logs should be explicit for operator-facing flows: print the current VIN, print `data.vehicleName` when `auth-check` returns it, print authorization success, and print a clear unauthorized/failure log when denied. -- `package-key` requests should use the vehicle name returned by `auth-check` (`data.vehicleName`) whenever available. Do not hardcode a model name if the authorization API already returned the exact vehicle name for the VIN. -- Normal users should not see low-level sensitive process details such as `fastboot`, `init_boot`, boot keys, or image names during permission/bootstrap flows. Use black-box text such as `正在获取权限中`, `获取成功`, and `获取失败`; leave command details for debug mode only. -- Process logs should stay minimal in normal mode. Detailed ADB/7za/API command logs belong behind debug mode. -- All Tkinter UI updates and `messagebox.*` calls from workers must go through `run_on_ui_thread(...)`. - -## Model-Specific Behavior - -### S05 - -- Keep `S05/S05.py` as original unless explicitly asked. -- Use `S05/S05_fixed.py` for experimental/fixed S05 changes. -- Do not add `chmod`, `chown`, or `restorecon` to the S05 system-app push path unless explicitly requested; the target system inherits permissions. -- `S05/S05_fixed.py` includes debug extract test `Ctrl+Shift+E` and 7za progress support. - -### UNI-Z - -- Endpoint for visible passwords: `/api/authorizations/get-uni-z-pwd`. -- Display `factoryPwd` as factory mode password and `password` as debug password. -- If `authorized == false` or `password` is empty, show unauthorized state and do not proceed. -- `password` from `get-uni-z-pwd` is not the package extraction password. -- Before push, call `/api/authorizations/package-key?vin=...` to get the real `package.bin` password. -- Push only to `/storage/emulated/0/Download/`; no `adb shell`. -- Language selection: RU/FR/ES/EN. Only the selected language Settings APK is pushed; other language Settings APKs are skipped silently. -- Hidden debug mode: `Ctrl+Shift+D`, password `zxch5200`, logs full ADB/7za/API details. - -### Mazda-EZ60 - -- Based on `Yidong/app-install.py` / 逸动 flow. -- Uses auto-password shell (`adb36987`) for VIN reads, `pm install`, overlay enable, disable commands, settings, and reboot. -- Installs APKs from extracted `apps` via push to `/data/local/tmp` then `pm install -r -d`. -- `Mazda-EZ60/Mazda-EZ60.py` should show 7za extraction progress with stream parsing. -- Regardless of APK install failures, run post-install configuration after the install loop. -- Post-install overlays to enable: - - `com.tinnove.launcher.overlay` - - `com.tinnove.scenemode.overlay` - - `com.incall.dvr.overlay` -- Post-install packages to disable: - - `com.carinno.p1` - - `com.wtcl.electronicdirections` - - `com.ximalaya.ting.android.car` - - `com.tinnove.netease.music` - - `com.migu.miguplay.car` - - `cn.cmvideo.car.play` - - `com.tinnove.carshow` - - `com.tinnove.changba` - - `com.qiyi.video.iv` -- User cancelled the `Ctrl+Shift+E` direct extract test request for Mazda; do not add it unless asked again. - -### Q05_Lidar - -- This is the Q05_Lidar-specific tool and must not be confused with any ordinary Q05 variant or package. -- Based on the shared installer visual style, but its resource structure and flashing flow are Q05_Lidar-specific. -- The first-row `获取权限` button installs `runtime.dat` -> `base.apk`, reboots to fastboot, waits for a real `fastboot devices` row like ` fastboot` with a non-aggressive interval, then fetches a boot key through `POST /api/authorizations/boot-challenge` then `POST /api/authorizations/boot-key`, decrypts embedded `resource.dat`, flashes `init_boot`, immediately reboots, and deletes the temporary img. Keep the decrypted img lifetime as short as possible. -- `resource.dat` is AES-GCM encrypted and must match the server `BOOT_KEY`; the tool only accepts `data.sessionKey` from `boot-key`. -- Device fingerprint data sent to the server includes ADB serial, `ro.serialno`, `ro.boot.serialno`, manufacturer, model, device, build fingerprint, and VIN. -- `刷入语言包` installs Magisk modules, not APKs. It opens `com.topjohnwu.magisk`, warns the user to grant Shell/root permission, verifies `/debug_ramdisk/su -c "id"` returns `uid=0`, extracts `Q05_Lidar-package.bin`, then pushes module files to `/data/local/tmp/q05_lidar_modules//` and root-copies them into `/data/adb/modules/`. -- `Q05_Lidar-package.bin` should unpack with module files at archive root: `module.prop`, scripts, `system/`, and `disable-wireless-adb-vecentek-magisk.zip`; do not wrap them in an outer `Q05_LIDAR_DATA/` directory. -- Q05_Lidar package cache is `%LOCALAPPDATA%\.cache\system\.android\apps_cache_Q05_Lidar`; the tool cleans it on startup/extraction and on normal/atexit shutdown. -- Q05_Lidar `runtime.dat` cache is `%LOCALAPPDATA%\.cache\system\.android\apps_cache_q05_lidar_runtime`; treat it as temporary and clean stale contents before extraction. -- `Q05_Lidar-package.bin` is an external release file next to the exe because it is large. `resource.dat` is embedded in the exe; `runtime.dat` should be copied next to the exe by the pack script. -- `package-key` must include the `vehicleName` returned by `auth-check` for the VIN. The tool caches `data.vehicleName` from password query / authorization check and uses it for package-key; if missing, query `auth-check` first rather than falling back to a hardcoded Q05_Lidar value. -- All `adb shell` commands in Q05_Lidar, including Magisk launch and `/debug_ramdisk/su -c ...`, must go through `run_adb_shell()` so the tool silently sends `adb36987`. -- The `安装App` button remains the APK install path: file picker -> `adb push` -> `setprop vecentek.model 1` -> `pm install -r -d -f` -> cleanup. Do not replace it with the Magisk module flow. -- Temporary debug mode exists only for development and should be removed before release when requested. Press `Ctrl+Shift+D`; the password is verified through `POST /api/authorizations/verify-debug-mode-password`. -- In Q05_Lidar debug mode, hidden buttons appear for: - - `指纹测试`: collect and log device fingerprint fields plus local SHA256 summary. - - `解密测试`: if VIN/device is available, fetch boot key; otherwise prompt for a pasted `BOOT_KEY`/`sessionKey`, decrypt `resource.dat` locally to a temporary img, log size/SHA256, then delete it. - - `解压测试`: fetch `package-key`, extract `Q05_Lidar-package.bin`, and verify the main Magisk module plus `disable_wireless_adb_vecentek` module can be identified. -- Do not log the actual boot key/session key in debug mode. - -### Yidong/app-yidong.py - -- Uses `apps_cache_yidong`. -- Has 7za compatibility handling for progress switches and `Incorrect command line` fallback. -- `Yidong/pack_yidong.bat` output name is ASCII: `Changan-Yidong-Language-Installer.exe`, to avoid CMD codepage issues with Chinese `NAME`. - -## Build Notes - -- Pack scripts install/use `pyinstaller`, `cython`, and usually `pyzipper`. -- Cython success requires Microsoft C++ Build Tools. -- Cython success signs in logs: - - `building '_core' extension` - - `_core.cpXXX-win_amd64.pyd` - - `PYD: _core...pyd` - - output under `dist_cy\dist\...exe` -- If logs show `[WARN] Cython failed, fallback` and `[INFO] Normal PyInstaller`, the exe still builds but is normal PyInstaller and easier to reverse. -- For security-sensitive tools like Q05_Lidar, do not keep a normal PyInstaller fallback. If Cython fails or no `_core*.pyd` is generated, stop the build and show an error. -- A Cython onefile PyInstaller build should use a tiny `launcher.py` that imports `main` from compiled `_core.pyd`, and the exe archive should contain `_core*.pyd`. Confirm with PyInstaller archive viewer when in doubt. -- `UNIZ/pack_uniz.bat` and `Mazda-EZ60/pack_mazda_ez60.bat` use ASCII output names to avoid CMD encoding problems. -- Generated `.exe`, `.spec`, `build/`, `dist/`, and `dist_cy/` are build artifacts and should not be committed unless explicitly requested. - -## Key Behaviors To Preserve - -1. Keep original tools untouched when a fixed or model-specific copy exists. -2. Preserve VIN-based authorization for normal flashing tools. -3. Fetch `package-key` from the server instead of hardcoding package passwords. -4. Keep all shell commands in 逸动/Mazda tools behind `run_adb_shell()`. -5. Keep UNI-Z shell-free. -6. Use `run_on_ui_thread()` for all tkinter UI updates from worker threads. -7. Keep shared Android/7za binaries under root `tools/` and have pack scripts copy from there. - -## Current Local State (2026-05-28) - -- `UNIZ/UNIZ.py` and `UNIZ/pack_uniz.bat` exist locally. Cython build has succeeded after installing Microsoft C++ Build Tools, producing `dist_cy\dist\UNIZ-Language-Pusher.exe`. -- `Mazda-EZ60/Mazda-EZ60.py` and `Mazda-EZ60/pack_mazda_ez60.bat` exist locally. Mazda has 7za progress extraction, unconditional post-install configuration, three overlay enables, and nine package disables. -- `Yidong/app-yidong.py` has been updated for 7za progress compatibility and `Incorrect command line` fallback. -- `Yidong/pack_yidong.bat` has been updated with quoted paths, `cd /d "%~dp0"`, and ASCII output name. -- `.gitignore` has been expanded to ignore generated exe/spec artifacts. -- There may be untracked local build outputs and generated specs; inspect `git status --ignored` before committing. - -## Known Issues - -- Some older tools still have minimal exception handling and bare `except: pass`. -- `test_extract.py` hardcodes a password and should not be treated as production flow. -- `on_disable_upgrade` behavior is Windows/vehicle specific. -- Pure PyInstaller fallback is easy to reverse; prefer successful Cython builds for release. +# 长安语言刷入工具 (Changan Language Flashing Tool) + +## Overview + +Windows GUI tool suite (Python 3.6+ / tkinter) for flashing or pushing multi-language APKs to Android-based vehicle infotainment systems. Built by 宜宾科宜科技有限公司. + +Most tools are single-file tkinter apps with the same rough architecture: GUI, VIN authorization, package extraction, ADB commands, logging, and worker threads live in one class. Preserve that style unless the user explicitly asks for a larger refactor. + +## Tool Variants + +| Tool file | Vehicle / purpose | Window title | Pack script | +|-----------|-------------------|--------------|-------------| +| `Q07/app.py` | 启源Q07 | 长安语言安装工具 | `Q07/pack_q07.bat` | +| `S05/S05.py` | 深蓝S05 original | 深蓝S05多语言安装 | `S05/pack_s05.bat` | +| `S05/S05_fixed.py` | 深蓝S05 fixed/experimental copy | 长安语言安装工具 | `S05/pack_s05_fixed.bat` | +| `X5plus/X5plusTool.py` | X5plus | 适用于X5plus多语言安装 | `X5plus/pack_x5plus.bat` | +| `CS55-Q05/CS55-Q05_Installer.py` | CS55Plus/Q05 通用 | CS55Plus/Q05 语言刷入工具 | `CS55-Q05/pack_cs55_q05.bat` | +| `Yidong/app-yidong.py` | 长安逸动 | 长安逸动语言刷入工具 | `Yidong/pack_yidong.bat` | +| `UNIZ/UNIZ.py` | UNI-Z file pusher | UNI-Z语言文件推送工具 | `UNIZ/pack_uniz.bat` | +| `Mazda-EZ60/Mazda-EZ60_1.2.py` | Mazda-EZ60 OS 1.2 | Mazda-EZ60_OS-1.2适用 | `Mazda-EZ60/pack_mazda_ez60_1.2.bat` | +| `Mazda-EZ60/Mazda_EZ60-Language-Install_v1.0.py` | Mazda-EZ60 OS 1.0 | 马自达EZ60刷机工具_OS-1.0 | `Mazda-EZ60/pack_mazda_ez60_1.0.bat` | +| `Q05-Lidar/Q05-Lidar_Installer.py` | Q05_Lidar permission/bootstrap + language installer | Q05_Lidar | `Q05-Lidar/pack_q05_lidar.bat` | + +## Project Structure + +``` +├── Q07/ # Q07 script, pack scripts, ignored build outputs +├── S05/ # S05 original and fixed copy +├── X5plus/ # X5plus tool +├── CS55-Q05/ # CS55Plus/Q05 common installer and icon +├── Yidong/ # Yidong-specific tool +├── UNIZ/ # UNI-Z file pusher +├── Mazda-EZ60/ # Mazda-EZ60 tool +├── A07/ # Qiyuan A07 tool +├── Q05-Lidar/ # Q05_Lidar tool plus resource.dat/tools +├── app.ico +├── package.bin # encrypted package, not committed; shared from root +└── tools/ # shared adb/fastboot/7za dependencies for pack scripts + ├── adb.exe + ├── AdbWinApi.dll + ├── AdbWinUsbApi.dll + ├── fastboot.exe + └── 7za.exe # 7-Zip Extra 26.01, bundled by pack scripts +``` + +## Architecture Notes + +- Most tools use class `ADKAPKGUI`; `UNIZ/UNIZ.py` uses `UNIZLanguageGUI`. +- Worker actions run in `threading.Thread(..., daemon=True)`. +- Tkinter calls from workers must go through `run_on_ui_thread(...)`. +- Prefer `self.root.after(0, lambda: func(*args, **kwargs))` in `run_on_ui_thread`; direct `after(0, func, *args, **kwargs)` breaks when kwargs such as `text=` or `fg=` are passed. +- Background `messagebox.*` calls should be scheduled with `run_on_ui_thread`. +- Keep files UTF-8 with `# -*- coding: utf-8 -*-`. + +## ADB And Auth + +- `run_adb_command(command)` handles normal ADB commands such as `adb devices`, `adb push`, and non-shell install calls. +- `run_adb_shell(shell_command)` exists in the 逸动-family tools and Mazda copy; it shells into the device and automatically sends password `adb36987`. +- All `adb shell` operations in `CS55-Q05/CS55-Q05_Installer.py`, `Yidong/app-yidong.py`, and `Mazda-EZ60/Mazda-EZ60_1.2.py` should go through `run_adb_shell()`. +- `UNIZ/UNIZ.py` must not use `adb shell`; it only checks devices and pushes APKs to `/storage/emulated/0/Download/`. +- Standard auth flow uses: + - `auth-check?vin=...` for authorization. + - `package-key?vin=...` for `package.bin` extraction password. +- VIN keys: + - Q07/S05/X5plus: `ca_vin_info` or `VIN`. + - 逸动/Mazda: `settings get system ca.car.vin` via auto-password shell. + - UNI-Z: user manually enters VIN. + +## Package Extraction + +- `package.bin` is extracted under `%LOCALAPPDATA%\.cache\system\.android\...`. +- Current cache directories: + - `Q07/app.py` -> `apps_cache_Q07` + - `S05/S05.py` / `S05/S05_fixed.py` -> `apps_cache_S05` + - `X5plus/X5plusTool.py` -> `apps_cache_X5plus` + - `CS55-Q05/CS55-Q05_Installer.py` -> `apps_cache_common` + - `Yidong/app-yidong.py` -> `apps_cache_yidong` + - `UNIZ/UNIZ.py` -> `apps_cache_UNIZ` + - `Mazda-EZ60/Mazda-EZ60_1.2.py` -> `apps_cache_Mazda_EZ60` +- Shared binaries are managed under root `tools/`: `adb.exe`, `AdbWinApi.dll`, `AdbWinUsbApi.dll`, `fastboot.exe`, and `7za.exe`. Vehicle pack scripts in subfolders should copy from `%ROOT%\tools`, not from each vehicle folder. +- For progress display, detect support for `-bsp1` by checking for `-bs{o|e|p}` in 7za help output. +- If `Incorrect command line` appears, retry with the basic compatible command: `x package.bin -pPASSWORD -oDIR -y`. +- Decode 7za output with GBK first, then UTF-8 fallback. +- 7za progress must parse streamed output cumulatively. Do not read one byte and regex that single byte; percentages such as `42%` span multiple bytes and will otherwise jump from 0 to 100. +- User-facing resource extraction text should say `资源准备中` / `Preparing resources`, not `资源解压` / `Extracting package`, unless the UI is an explicit debug test. +- Cache cleanup should be best effort in three places when feasible: before a new extraction, during normal window close, and via `atexit` for ordinary process exit. A forced process kill cannot be guaranteed, so also clear stale caches at next extraction/startup. + +## Shared UX And Safety Rules + +- Hosts update logic should replace conflicting entries for the managed domain. If the hosts file already contains the target domain with a different IP, delete that line and write the expected `IP domain` entry instead of appending duplicates. +- VIN authorization logs should be explicit for operator-facing flows: print the current VIN, print `data.vehicleName` when `auth-check` returns it, print authorization success, and print a clear unauthorized/failure log when denied. +- `package-key` requests should use the vehicle name returned by `auth-check` (`data.vehicleName`) whenever available. Do not hardcode a model name if the authorization API already returned the exact vehicle name for the VIN. +- Normal users should not see low-level sensitive process details such as `fastboot`, `init_boot`, boot keys, or image names during permission/bootstrap flows. Use black-box text such as `正在获取权限中`, `获取成功`, and `获取失败`; leave command details for debug mode only. +- Process logs should stay minimal in normal mode. Detailed ADB/7za/API command logs belong behind debug mode. +- All Tkinter UI updates and `messagebox.*` calls from workers must go through `run_on_ui_thread(...)`. + +## Model-Specific Behavior + +### S05 + +- Keep `S05/S05.py` as original unless explicitly asked. +- Use `S05/S05_fixed.py` for experimental/fixed S05 changes. +- Do not add `chmod`, `chown`, or `restorecon` to the S05 system-app push path unless explicitly requested; the target system inherits permissions. +- `S05/S05_fixed.py` includes debug extract test `Ctrl+Shift+E` and 7za progress support. + +### UNI-Z + +- Endpoint for visible passwords: `/api/authorizations/get-uni-z-pwd`. +- Display `factoryPwd` as factory mode password and `password` as debug password. +- If `authorized == false` or `password` is empty, show unauthorized state and do not proceed. +- `password` from `get-uni-z-pwd` is not the package extraction password. +- Before push, call `/api/authorizations/package-key?vin=...` to get the real `package.bin` password. +- Push only to `/storage/emulated/0/Download/`; no `adb shell`. +- Language selection: RU/FR/ES/EN. Only the selected language Settings APK is pushed; other language Settings APKs are skipped silently. +- Hidden debug mode: `Ctrl+Shift+D`, password `zxch5200`, logs full ADB/7za/API details. + +### CS55-Q05 + +- CS55Plus/Q05 common installer lives in `CS55-Q05/CS55-Q05_Installer.py`, with icon `CS55-Q05/cs55-q05.ico` and pack script `CS55-Q05/pack_cs55_q05.bat`. +- `pack_cs55_q05.bat` is Cython-only. If Cython fails or no `_core*.pyd` is generated, stop the build; do not fall back to normal PyInstaller. +- Normal `刷入语言包` logs should not reveal APK names; show generic current/total progress only. `安装App` may show each APK result and current APK name. + +### Mazda-EZ60 + +- Based on `CS55-Q05/CS55-Q05_Installer.py` / 逸动 flow. +- Uses auto-password shell (`adb36987`) for VIN reads, `pm install`, overlay enable, disable commands, settings, and reboot. +- Installs APKs from extracted `apps` via push to `/data/local/tmp` then `pm install -r -d`. +- `Mazda-EZ60/Mazda-EZ60_1.2.py` should show 7za extraction progress with stream parsing. +- Mazda 1.2 package cache is `%LOCALAPPDATA%\.cache\system\.android\apps_cache_Mazda_EZ60`; clean it on startup, before extraction, on normal window close, and through `atexit`. +- Mazda `package-key` requests must include both `vin` and `vehicleName`; cache `data.vehicleName` from `auth-check`, and query `auth-check` before `package-key` if it is missing. +- Mazda hosts setup should keep exactly one target mapping for `spm.auto-pai.com`: replace mismatched IPs with `103.236.55.140 spm.auto-pai.com` instead of appending a conflicting duplicate. If hosts setup fails, tell the user `环境配置失败`. +- Mazda UI should not show the company/about subtitle. +- Mazda language switching should cover all operator-facing buttons, labels, dialogs, logs, progress text, hotspot status, password query UI, and quick-language popup text. The right-side usage tips should render from translated `hint_lines` with wrapping so English text does not overlap or overflow. +- Mazda normal logs/prompts should keep only key operator-facing outcomes and fuzzy resource/environment errors. Do not show package names, language package APK names, raw commands, paths, or command output during `刷入语言包`; manual `安装App` may show selected APK filenames. VIN and `vehicleName` are not sensitive and may remain visible. +- Mazda debug mode uses `Ctrl+Shift+D` and verifies the password through `POST /api/authorizations/verify-debug-mode-password`; detailed ADB/7za/API output belongs in debug mode only. +- Mazda pack scripts `pack_mazda_ez60_1.0.bat` and `pack_mazda_ez60_1.2.bat` are Cython-only. If dependencies, Cython compilation, PYD generation, resource copy, PyInstaller, or final exe output fail, stop immediately with an `[ERROR]` reason; do not fall back to normal PyInstaller. +- Regardless of APK install failures, run post-install configuration after the install loop. +- Post-install overlays to enable: + - `com.tinnove.launcher.overlay` + - `com.tinnove.scenemode.overlay` + - `com.incall.dvr.overlay` +- Post-install packages to disable: + - `com.carinno.p1` + - `com.wtcl.electronicdirections` + - `com.ximalaya.ting.android.car` + - `com.tinnove.netease.music` + - `com.migu.miguplay.car` + - `cn.cmvideo.car.play` + - `com.tinnove.carshow` + - `com.tinnove.changba` + - `com.qiyi.video.iv` +- User cancelled the `Ctrl+Shift+E` direct extract test request for Mazda; do not add it unless asked again. + +### Q05_Lidar + +- This is the Q05_Lidar-specific tool and must not be confused with any ordinary Q05 variant or package. +- Based on the shared installer visual style, but its resource structure and flashing flow are Q05_Lidar-specific. +- The first-row `获取权限` button installs `runtime.dat` -> `base.apk`, reboots to fastboot, waits for a real `fastboot devices` row like ` fastboot` with a non-aggressive interval, then fetches a boot key through `POST /api/authorizations/boot-challenge` then `POST /api/authorizations/boot-key`, decrypts embedded `resource.dat`, flashes `init_boot`, immediately reboots, and deletes the temporary img. Keep the decrypted img lifetime as short as possible. +- `resource.dat` is AES-GCM encrypted and must match the server `BOOT_KEY`; the tool only accepts `data.sessionKey` from `boot-key`. +- Device fingerprint data sent to the server includes ADB serial, `ro.serialno`, `ro.boot.serialno`, manufacturer, model, device, build fingerprint, and VIN. +- `刷入语言包` installs Magisk modules, not APKs. It opens `com.topjohnwu.magisk`, warns the user to grant Shell/root permission, verifies `/debug_ramdisk/su -c "id"` returns `uid=0`, extracts `Q05_Lidar-package.bin`, then pushes module files to `/data/local/tmp/q05_lidar_modules//` and root-copies them into `/data/adb/modules/`. +- `Q05_Lidar-package.bin` should unpack with module files at archive root: `module.prop`, scripts, `system/`, and `disable-wireless-adb-vecentek-magisk.zip`; do not wrap them in an outer `Q05_LIDAR_DATA/` directory. +- Q05_Lidar package cache is `%LOCALAPPDATA%\.cache\system\.android\apps_cache_Q05_Lidar`; the tool cleans it on startup/extraction and on normal/atexit shutdown. +- Q05_Lidar `runtime.dat` cache is `%LOCALAPPDATA%\.cache\system\.android\apps_cache_q05_lidar_runtime`; treat it as temporary and clean stale contents before extraction. +- `Q05_Lidar-package.bin` is an external release file next to the exe because it is large. `resource.dat` is embedded in the exe; `runtime.dat` should be copied next to the exe by the pack script. +- `package-key` must include the `vehicleName` returned by `auth-check` for the VIN. The tool caches `data.vehicleName` from password query / authorization check and uses it for package-key; if missing, query `auth-check` first rather than falling back to a hardcoded Q05_Lidar value. +- All `adb shell` commands in Q05_Lidar, including Magisk launch and `/debug_ramdisk/su -c ...`, must go through `run_adb_shell()` so the tool silently sends `adb36987`. +- The `安装App` button remains the APK install path: file picker -> `adb push` -> `setprop vecentek.model 1` -> `pm install -r -d -f` -> cleanup. Do not replace it with the Magisk module flow. +- Temporary debug mode exists only for development and should be removed before release when requested. Press `Ctrl+Shift+D`; the password is verified through `POST /api/authorizations/verify-debug-mode-password`. +- In Q05_Lidar debug mode, hidden buttons appear for: + - `指纹测试`: collect and log device fingerprint fields plus local SHA256 summary. + - `解密测试`: if VIN/device is available, fetch boot key; otherwise prompt for a pasted `BOOT_KEY`/`sessionKey`, decrypt `resource.dat` locally to a temporary img, log size/SHA256, then delete it. + - `解压测试`: fetch `package-key`, extract `Q05_Lidar-package.bin`, and verify the main Magisk module plus `disable_wireless_adb_vecentek` module can be identified. +- Do not log the actual boot key/session key in debug mode. + +### Yidong/app-yidong.py + +- Uses `apps_cache_yidong`. +- Has 7za compatibility handling for progress switches and `Incorrect command line` fallback. +- `Yidong/pack_yidong.bat` output name is ASCII: `Changan-Yidong-Language-Installer.exe`, to avoid CMD codepage issues with Chinese `NAME`. + +## Build Notes + +- Pack scripts install/use `pyinstaller`, `cython`, and usually `pyzipper`. +- Cython success requires Microsoft C++ Build Tools. +- Cython success signs in logs: + - `building '_core' extension` + - `_core.cpXXX-win_amd64.pyd` + - `PYD: _core...pyd` + - output under `dist_cy\dist\...exe` +- If logs show `[WARN] Cython failed, fallback` and `[INFO] Normal PyInstaller`, the exe still builds but is normal PyInstaller and easier to reverse. +- For security-sensitive tools like Q05_Lidar, do not keep a normal PyInstaller fallback. If Cython fails or no `_core*.pyd` is generated, stop the build and show an error. +- A Cython onefile PyInstaller build should use a tiny `launcher.py` that imports `main` from compiled `_core.pyd`, and the exe archive should contain `_core*.pyd`. Confirm with PyInstaller archive viewer when in doubt. +- `UNIZ/pack_uniz.bat` and Mazda-EZ60 pack scripts use ASCII output names to avoid CMD encoding problems. +- Generated `.exe`, `.spec`, `build/`, `dist/`, and `dist_cy/` are build artifacts and should not be committed unless explicitly requested. + +## Key Behaviors To Preserve + +1. Keep original tools untouched when a fixed or model-specific copy exists. +2. Preserve VIN-based authorization for normal flashing tools. +3. Fetch `package-key` from the server instead of hardcoding package passwords. +4. Keep all shell commands in 逸动/Mazda tools behind `run_adb_shell()`. +5. Keep UNI-Z shell-free. +6. Use `run_on_ui_thread()` for all tkinter UI updates from worker threads. +7. Keep shared Android/7za binaries under root `tools/` and have pack scripts copy from there. + +## Current Local State (2026-05-28) + +- `UNIZ/UNIZ.py` and `UNIZ/pack_uniz.bat` exist locally. Cython build has succeeded after installing Microsoft C++ Build Tools, producing `dist_cy\dist\UNIZ-Language-Pusher.exe`. +- `Mazda-EZ60/Mazda-EZ60_1.2.py` with `pack_mazda_ez60_1.2.bat`, and `Mazda-EZ60/Mazda_EZ60-Language-Install_v1.0.py` with `pack_mazda_ez60_1.0.bat`, exist locally. Mazda has 7za progress extraction, Cython-only packaging, and version-specific output names. +- `Yidong/app-yidong.py` has been updated for 7za progress compatibility and `Incorrect command line` fallback. +- `Yidong/pack_yidong.bat` has been updated with quoted paths, `cd /d "%~dp0"`, and ASCII output name. +- `.gitignore` has been expanded to ignore generated exe/spec artifacts. +- There may be untracked local build outputs and generated specs; inspect `git status --ignored` before committing. + +## Known Issues + +- Some older tools still have minimal exception handling and bare `except: pass`. +- `test_extract.py` hardcodes a password and should not be treated as production flow. +- `on_disable_upgrade` behavior is Windows/vehicle specific. +- Pure PyInstaller fallback is easy to reverse; prefer successful Cython builds for release. diff --git a/Yidong/app-install.py b/CS55-Q05/CS55-Q05_Installer.py similarity index 76% rename from Yidong/app-install.py rename to CS55-Q05/CS55-Q05_Installer.py index adc7ff4..dec33a6 100644 --- a/Yidong/app-install.py +++ b/CS55-Q05/CS55-Q05_Installer.py @@ -59,9 +59,24 @@ def find_tool(file_name, fallback=None): path = find_resource(file_name) if path.exists(): return str(path) - return fallback or str(path) + return fallback or str(path) + + +def set_windows_app_user_model_id(): + if sys.platform != 'win32': + return + try: + import ctypes + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + "yibin.keyi.cs55.q05.language.installer" + ) + except Exception: + pass + + class ADKAPKGUI: def __init__(self): + set_windows_app_user_model_id() self.root = tk.Tk() self.root.title("长安语言刷入工具") self.root.geometry("900x620") @@ -101,7 +116,9 @@ class ADKAPKGUI: self.lang = 'zh' self.T = { 'zh': { + 'window_title': 'CS55Plus/Q05 语言刷入工具', 'title': 'CS55Plus/Q05', + 'btn_unlock_install': '🔓 解锁安装权限', 'btn_push': '📦 刷入语言包', 'btn_install': '📱 安装App', 'btn_language': '🌐 语言设置', @@ -111,6 +128,8 @@ class ADKAPKGUI: 'btn_disable_upgrade': '❌ 禁用升级', 'btn_clear_log': '🗑 清空日志', 'btn_query_pwd': '查询密码', + 'pwd_query_label': '工程密码查询:', + 'vin_placeholder': '请输入VIN', 'device_label': '设备:', 'vin_label': 'VIN码:', 'auth_label': '授权:', @@ -127,15 +146,60 @@ class ADKAPKGUI: 'hint_factory': '🔧 关闭车辆WI-FI和4G网络,拨号获取的密码进入工程模式', 'hotspot_title': '📶 电脑热点', 'hotspot_start': '🔧 打开热点设置', + 'hotspot_name_detecting': '名称: 检测中...', + 'hotspot_name_value': '名称: {ssid}', + 'hotspot_name_empty': '名称: 未配置', + 'hotspot_pwd_default': '密码: changan2024', + 'hotspot_pwd_value': '密码: {password}', + 'hotspot_status_value': '状态: {status}', + 'hotspot_status_off': '状态: 未启动', 'hint_title': '💡 使用提示', + 'hint_lines': [ + '1. 确保电脑已开启热点', + '2. 拨号进入工厂模式,点击调试工具', + '3. 需要云端认证时,点击车机状态栏', + ' Wi-Fi图标,连接上方显示的热点', + '4. 连接后点击车机“云端认证”按钮', + '5. 打开ADB后即可正常刷入语言包', + ], 'theme_dark': '🌙 暗色', 'theme_light': '☀️ 亮色', 'lang_zh': '中', 'lang_en': 'EN', - 'about_company': '宜宾科宜科技有限公司 - 出口改装一站式服务', + 'log_lang_changed': '语言已切换为中文', + 'log_cleared': '日志已清空', + 'log_unlock_success': '安装权限已解锁', + 'log_unlock_failed': '安装权限解锁失败: {output}', + 'msg_success_title': '成功', + 'msg_error_title': '错误', + 'msg_warn_title': '警告', + 'msg_device_not_connected_title': '设备未连接', + 'msg_device_not_connected': '请先连接设备并点击「检查」按钮刷新状态!', + 'msg_unlock_success': '安装权限已解锁,可以继续安装或刷入语言包。', + 'msg_unlock_failed': '安装权限解锁失败:{output}', + 'msg_input_vin': '请输入VIN码', + 'msg_need_vin': '请先刷新设备状态并获取VIN码', + 'msg_auth_failed_title': '授权失败', + 'msg_device_unauthorized': '设备未授权', + 'msg_data_prepare_failed': '数据准备失败!', + 'msg_resource_prepare_failed': '资源准备失败!', + 'msg_resource_dir_missing': '资源目录未找到', + 'quick_lang_title': '快捷语言设置', + 'quick_lang_header': '选择目标语言', + 'quick_lang_hint': '点击按钮即可将系统语言切换为对应语言,重启后生效', + 'quick_lang_system': '⚙️ 打开系统语言设置(手动选择)', + 'quick_lang_success_title': '设置成功', + 'quick_lang_success': '系统语言已设置为 {language}\n\n⚠️ 请重启设备使其生效。', + 'quick_lang_failed_title': '设置失败', + 'quick_lang_failed': '语言设置失败!\n\n{output}', + 'quick_lang_names': ['🇨🇳 中文', '英 English', '俄 Русский', '法 Français', '西 Español', '葡 Português', '意 Italiano', '阿 العربية'], + 'log_quick_lang_success': '语言已设置为 {language}', + 'log_quick_lang_failed': '语言设置失败: {output}', }, 'en': { + 'window_title': 'CS55Plus/Q05 Language Installer', 'title': 'CS55Plus/Q05', + 'btn_unlock_install': '🔓 Unlock Install', 'btn_push': '📦 Flash Lang Pkg', 'btn_install': '📱 Install App', 'btn_language': '🌐 Language', @@ -145,6 +209,8 @@ class ADKAPKGUI: 'btn_disable_upgrade': '❌ Disable OTA', 'btn_clear_log': '🗑 Clear Log', 'btn_query_pwd': 'Query Pwd', + 'pwd_query_label': 'Factory password:', + 'vin_placeholder': 'Enter VIN', 'device_label': 'Device:', 'vin_label': 'VIN:', 'auth_label': 'Auth:', @@ -161,12 +227,55 @@ class ADKAPKGUI: 'hint_factory': '🔧 Turn off WiFi & 4G, enter factory mode with dial code', 'hotspot_title': '📶 Hotspot', 'hotspot_start': '🔧 Open Hotspot Settings', + 'hotspot_name_detecting': 'Name: detecting...', + 'hotspot_name_value': 'Name: {ssid}', + 'hotspot_name_empty': 'Name: not configured', + 'hotspot_pwd_default': 'Password: changan2024', + 'hotspot_pwd_value': 'Password: {password}', + 'hotspot_status_value': 'Status: {status}', + 'hotspot_status_off': 'Status: off', 'hint_title': '💡 Tips', + 'hint_lines': [ + '1. Make sure the PC hotspot is enabled', + '2. Enter factory mode from the dialer', + '3. When cloud auth is needed, tap the', + ' Wi-Fi icon and connect to the hotspot', + '4. Tap Cloud Auth on the vehicle screen', + '5. Enable ADB, then flash the language pack', + ], 'theme_dark': '🌙 Dark', 'theme_light': '☀️ Light', 'lang_zh': '中', 'lang_en': 'EN', - 'about_company': 'Yibin Keyi Technology - Export Modification Service', + 'log_lang_changed': 'Language switched to English', + 'log_cleared': 'Log cleared', + 'log_unlock_success': 'Install permission unlocked', + 'log_unlock_failed': 'Install permission unlock failed: {output}', + 'msg_success_title': 'Success', + 'msg_error_title': 'Error', + 'msg_warn_title': 'Warning', + 'msg_device_not_connected_title': 'Device Not Connected', + 'msg_device_not_connected': 'Connect the device and click Check first.', + 'msg_unlock_success': 'Install permission is unlocked. You can continue installing or flashing.', + 'msg_unlock_failed': 'Install permission unlock failed: {output}', + 'msg_input_vin': 'Enter VIN', + 'msg_need_vin': 'Refresh device status and get VIN first.', + 'msg_auth_failed_title': 'Authorization Failed', + 'msg_device_unauthorized': 'Device is not authorized', + 'msg_data_prepare_failed': 'Data preparation failed.', + 'msg_resource_prepare_failed': 'Resource preparation failed.', + 'msg_resource_dir_missing': 'Resource directory not found', + 'quick_lang_title': 'Quick Language', + 'quick_lang_header': 'Choose target language', + 'quick_lang_hint': 'Tap a button to switch system language. Reboot to apply.', + 'quick_lang_system': '⚙️ Open system language settings', + 'quick_lang_success_title': 'Language Set', + 'quick_lang_success': 'System language was set to {language}.\n\n⚠️ Reboot the device to apply it.', + 'quick_lang_failed_title': 'Language Failed', + 'quick_lang_failed': 'Language setting failed.\n\n{output}', + 'quick_lang_names': ['Chinese', 'English', 'Russian', 'French', 'Spanish', 'Portuguese', 'Italian', 'Arabic'], + 'log_quick_lang_success': 'Language set to {language}', + 'log_quick_lang_failed': 'Language setting failed: {output}', } } @@ -182,10 +291,12 @@ class ADKAPKGUI: self.device_connected = False self._refreshing = False # 防止并发刷新 self.debug_mode = False # 调试模式 + self.root.title(self.t('window_title')) # 设置样式 self.setup_styles() self.setup_ui() + self.root.after(200, self.set_window_icon) self.center_window() # 检查环境 @@ -194,6 +305,38 @@ class ADKAPKGUI: # 启动设备状态监控 self.start_device_monitor() + def set_window_icon(self): + """Set the Tk window/taskbar icon at runtime; PyInstaller --icon only sets the exe file icon.""" + try: + icon_path = find_resource("cs55-q05.ico") + if icon_path.exists(): + self.root.iconbitmap(str(icon_path)) + self._set_windows_hwnd_icon(icon_path) + except Exception: + pass + + def _set_windows_hwnd_icon(self, icon_path): + if sys.platform != 'win32': + return + try: + import ctypes + user32 = ctypes.windll.user32 + hwnd = self.root.winfo_id() + image_icon = 1 + lr_loadfromfile = 0x00000010 + wm_seticon = 0x0080 + icon_small = 0 + icon_big = 1 + path = str(icon_path) + small = user32.LoadImageW(None, path, image_icon, 16, 16, lr_loadfromfile) + big = user32.LoadImageW(None, path, image_icon, 32, 32, lr_loadfromfile) + if small: + user32.SendMessageW(hwnd, wm_seticon, icon_small, small) + if big: + user32.SendMessageW(hwnd, wm_seticon, icon_big, big) + except Exception: + pass + def setup_styles(self): """设置自定义样式""" style = ttk.Style() @@ -235,28 +378,22 @@ class ADKAPKGUI: 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() + self.title_label = tk.Label(title_frame, + text="🚀 " + self.t('title'), + font=('Microsoft YaHei', 18, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.title_label.pack() # 工程密码查询区域 pwd_query_frame = tk.Frame(left_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) pwd_query_frame.pack(fill=tk.X, pady=(0, 5), padx=5) - 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.pwd_query_label = tk.Label(pwd_query_frame, text=self.t('pwd_query_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.pwd_query_label.pack(side=tk.LEFT, padx=(10, 5), pady=5) self.vin_input = tk.Entry(pwd_query_frame, font=('Consolas', 9), @@ -265,12 +402,12 @@ class ADKAPKGUI: insertbackground='white', relief=tk.FLAT, width=20) - self.vin_input.insert(0, "请输入VIN") + self.vin_input.insert(0, self.t('vin_placeholder')) 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="查询密码", + self.btn_query_pwd = tk.Button(pwd_query_frame, text=self.t('btn_query_pwd'), command=self.query_password_by_vin, font=('Microsoft YaHei', 8), fg='white', @@ -289,10 +426,11 @@ class ADKAPKGUI: # 工厂模式提示 factory_hint_frame = tk.Frame(left_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) + self.hint_label = tk.Label(factory_hint_frame, text=self.t('hint_factory'), + font=('Microsoft YaHei', 8), + fg=self.colors['warning'], + bg=self.colors['bg_dark']) + self.hint_label.pack(side=tk.LEFT, padx=2) # 按钮区域(两排,每排5个) button_frame = tk.Frame(left_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) @@ -305,26 +443,32 @@ class ADKAPKGUI: 'relief': tk.FLAT, 'cursor': 'hand2', 'height': 1, - 'width': 14 + 'width': 13 } # 第一排按钮 row1_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) row1_frame.pack(pady=(8, 4)) - self.btn_push = tk.Button(row1_frame, text="📦 刷入语言包", + self.btn_push = tk.Button(row1_frame, text=self.t('btn_push'), command=self.push_all_apks, bg=self.colors['accent'], **btn_params) self.btn_push.pack(side=tk.LEFT, padx=4) - self.btn_install_all = tk.Button(row1_frame, text="📱 安装App", + self.btn_unlock_install = tk.Button(row1_frame, text=self.t('btn_unlock_install'), + command=self.unlock_install_permission, + bg=self.colors['warning'], + **btn_params) + self.btn_unlock_install.pack(side=tk.LEFT, padx=4) + + self.btn_install_all = tk.Button(row1_frame, text=self.t('btn_install'), command=self.install_apps, bg=self.colors['accent'], **btn_params) self.btn_install_all.pack(side=tk.LEFT, padx=4) - self.btn_language = tk.Button(row1_frame, text="🌐 语言设置", + self.btn_language = tk.Button(row1_frame, text=self.t('btn_language'), command=self.open_language_quick_set, bg=self.colors['accent'], **btn_params) @@ -334,25 +478,25 @@ class ADKAPKGUI: row2_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) row2_frame.pack(pady=(4, 8)) - self.btn_timezone = tk.Button(row2_frame, text="⏰ 时区设置", + self.btn_timezone = tk.Button(row2_frame, text=self.t('btn_timezone'), command=self.open_timezone_settings, bg=self.colors['accent'], **btn_params) self.btn_timezone.pack(side=tk.LEFT, padx=4) - self.btn_settings = tk.Button(row2_frame, text="⚙️ 安卓设置", + self.btn_settings = tk.Button(row2_frame, text=self.t('btn_settings'), command=self.open_android_settings, bg=self.colors['accent'], **btn_params) self.btn_settings.pack(side=tk.LEFT, padx=4) - self.btn_reboot = tk.Button(row2_frame, text="🔄 重启设备", + self.btn_reboot = tk.Button(row2_frame, text=self.t('btn_reboot'), command=self.reboot_device, bg=self.colors['warning'], **btn_params) self.btn_reboot.pack(side=tk.LEFT, padx=4) - self.btn_exit = tk.Button(row2_frame, text="❌ 禁用升级", + self.btn_exit = tk.Button(row2_frame, text=self.t('btn_disable_upgrade'), command=self.on_disable_upgrade, bg=self.colors['error'], **btn_params) @@ -371,12 +515,13 @@ class ADKAPKGUI: 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_label = tk.Label(status_indicator_frame, text=self.t('device_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.device_label.pack(side=tk.LEFT, padx=(5, 3)) - self.device_status_label = tk.Label(status_indicator_frame, text="未检测", + self.device_status_label = tk.Label(status_indicator_frame, text=self.t('status_detecting'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light']) @@ -385,11 +530,12 @@ class ADKAPKGUI: # 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="未获取", + self.vin_label_title = tk.Label(vin_frame, text=self.t('vin_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.vin_label_title.pack(side=tk.LEFT) + self.vin_label = tk.Label(vin_frame, text=self.t('vin_none'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light']) @@ -398,25 +544,26 @@ class ADKAPKGUI: # 授权状态 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="未验证", + self.auth_label_title = tk.Label(auth_frame, text=self.t('auth_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.auth_label_title.pack(side=tk.LEFT) + self.auth_label = tk.Label(auth_frame, text=self.t('auth_none'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light']) self.auth_label.pack(side=tk.LEFT, padx=(5, 0)) # 刷新按钮 - 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) + self.btn_refresh = tk.Button(status_bar_frame, text=self.t('btn_refresh'), + command=lambda: self.refresh_device_status(force=True), + font=('Microsoft YaHei', 8), + fg=self.colors['accent'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + self.btn_refresh.pack(side=tk.RIGHT, padx=10, pady=5) # 解压进度条框架 progress_frame = tk.Frame(left_frame, bg=self.colors['bg_dark']) @@ -448,12 +595,13 @@ class ADKAPKGUI: 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.log_title_label = tk.Label(log_title_frame, text=self.t('log_title'), + font=('Microsoft YaHei', 10, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.log_title_label.pack(side=tk.LEFT, padx=10) - self.btn_clear = tk.Button(log_title_frame, text="🗑 清空日志", + self.btn_clear = tk.Button(log_title_frame, text=self.t('btn_clear_log'), command=self.clear_log, font=('Microsoft YaHei', 8), fg=self.colors['text_secondary'], @@ -489,14 +637,14 @@ class ADKAPKGUI: bottom_status.pack(fill=tk.X, pady=(5, 0)) bottom_status.pack_propagate(False) - self.status_text = tk.Label(bottom_status, text="就绪", + self.status_text = tk.Label(bottom_status, text=self.t('status_ready'), font=('Microsoft YaHei', 8), fg=self.colors['text_secondary'], bg=self.colors['bg_light']) self.status_text.pack(side=tk.LEFT, padx=10) # 主题和语言切换按钮 - self.btn_theme_switch = tk.Button(bottom_status, text="🌙 暗色", + self.btn_theme_switch = tk.Button(bottom_status, text=self.t('theme_light'), command=self.toggle_theme, font=('Microsoft YaHei', 8), fg=self.colors['accent'], @@ -520,30 +668,31 @@ class ADKAPKGUI: hotspot_card = tk.Frame(right_frame, bg=self.colors['bg_dark'], relief=tk.RAISED, bd=1) hotspot_card.pack(fill=tk.X, padx=5, pady=(10, 5)) - tk.Label(hotspot_card, text="📶 电脑热点", - font=('Microsoft YaHei', 11, 'bold'), - fg=self.colors['accent'], - bg=self.colors['bg_dark']).pack(pady=(8, 5)) + self.hotspot_title_label = tk.Label(hotspot_card, text=self.t('hotspot_title'), + font=('Microsoft YaHei', 11, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.hotspot_title_label.pack(pady=(8, 5)) - self.hotspot_ssid_label = tk.Label(hotspot_card, text="名称: 检测中...", + self.hotspot_ssid_label = tk.Label(hotspot_card, text=self.t('hotspot_name_detecting'), font=('Microsoft YaHei', 9), fg=self.colors['text'], bg=self.colors['bg_dark']) self.hotspot_ssid_label.pack(anchor='w', padx=10, pady=2) - self.hotspot_pwd_label = tk.Label(hotspot_card, text="密码: changan2024", + self.hotspot_pwd_label = tk.Label(hotspot_card, text=self.t('hotspot_pwd_default'), font=('Microsoft YaHei', 9), fg=self.colors['text'], bg=self.colors['bg_dark']) self.hotspot_pwd_label.pack(anchor='w', padx=10, pady=2) - self.hotspot_status_label = tk.Label(hotspot_card, text="状态: 未启动", + self.hotspot_status_label = tk.Label(hotspot_card, text=self.t('hotspot_status_off'), font=('Microsoft YaHei', 9), fg=self.colors['warning'], bg=self.colors['bg_dark']) self.hotspot_status_label.pack(anchor='w', padx=10, pady=2) - self.btn_hotspot = tk.Button(hotspot_card, text="🔧 打开热点设置", + self.btn_hotspot = tk.Button(hotspot_card, text=self.t('hotspot_start'), command=self.start_hotspot_action, font=('Microsoft YaHei', 8), fg='white', @@ -556,36 +705,24 @@ class ADKAPKGUI: tk.Frame(right_frame, bg=self.colors['border'], height=1).pack(fill=tk.X, padx=8, pady=5) # 使用提示卡片 - hint_card = tk.Frame(right_frame, bg=self.colors['bg_dark'], relief=tk.RAISED, bd=1) - hint_card.pack(fill=tk.X, padx=5, pady=5) + self.hint_card = tk.Frame(right_frame, bg=self.colors['bg_dark'], relief=tk.RAISED, bd=1) + self.hint_card.pack(fill=tk.X, padx=5, pady=5) - tk.Label(hint_card, text="💡 使用提示", - font=('Microsoft YaHei', 11, 'bold'), - fg=self.colors['warning'], - bg=self.colors['bg_dark']).pack(pady=(8, 5)) + self.hint_title_label = tk.Label(self.hint_card, text=self.t('hint_title'), + font=('Microsoft YaHei', 11, 'bold'), + fg=self.colors['warning'], + bg=self.colors['bg_dark']) + self.hint_title_label.pack(pady=(8, 5)) - hint_lines = [ - "1. 确保电脑已开启热点", - "2. 拨号进入工厂模式,点击调试工具", - "3. 需要云端认证时,点击车机状态栏", - "Wi-Fi图标,连接上方显示的热点", - "4. 连接后点击车机“云端认证”按钮", - "5. 打开ADB后即可正常刷入语言包", - ] - for line in hint_lines: - tk.Label(hint_card, text=line, - font=('Microsoft YaHei', 8), - fg=self.colors['text_secondary'], - bg=self.colors['bg_dark'], - justify=tk.LEFT, - anchor='w').pack(anchor='w', padx=10) + self.hint_line_labels = [] + self._render_hint_lines() # 绑定悬停效果 self.bind_hover_effects() def bind_hover_effects(self): """绑定按钮悬停效果""" - buttons = [self.btn_push, self.btn_install_all, + buttons = [self.btn_push, self.btn_unlock_install, self.btn_install_all, self.btn_language, self.btn_timezone, self.btn_settings, self.btn_reboot, self.btn_clear, self.btn_exit, self.btn_query_pwd, self.btn_hotspot] @@ -634,11 +771,37 @@ class ADKAPKGUI: def t(self, key): return self.T.get(self.lang, self.T['zh']).get(key, key) + def tf(self, key, **kwargs): + return str(self.t(key)).format(**kwargs) + + def is_placeholder_vin(self, text): + return text in (self.T['zh']['vin_placeholder'], self.T['en']['vin_placeholder']) + def toggle_lang(self): self.lang = 'en' if self.lang == 'zh' else 'zh' self.btn_lang_switch.config(text=self.t('lang_en') if self.lang == 'zh' else self.t('lang_zh')) self._refresh_ui_texts() - self.log(f"语言已切换为 {'English' if self.lang == 'en' else '中文'}", "INFO") + self.log(self.t('log_lang_changed'), "INFO") + + def _render_hint_lines(self): + if not getattr(self, 'hint_card', None): + return + for label in getattr(self, 'hint_line_labels', []): + label.destroy() + self.hint_line_labels = [] + for line in self.t('hint_lines'): + label = tk.Label( + self.hint_card, + text=line, + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark'], + justify=tk.LEFT, + anchor='w', + wraplength=205 + ) + label.pack(anchor='w', fill=tk.X, padx=10, pady=1) + self.hint_line_labels.append(label) def toggle_theme(self): if self.theme == 'dark': @@ -672,9 +835,10 @@ class ADKAPKGUI: def _refresh_ui_texts(self): t = self.t + self.root.title(t('window_title')) widgets = [ (getattr(self, 'title_label', None), 'title', None), - (getattr(self, 'subtitle_label', None), 'about_company', None), + (getattr(self, 'btn_unlock_install', None), 'btn_unlock_install', None), (getattr(self, 'btn_push', None), 'btn_push', None), (getattr(self, 'btn_install_all', None), 'btn_install', None), (getattr(self, 'btn_language', None), 'btn_language', None), @@ -691,22 +855,41 @@ class ADKAPKGUI: (getattr(self, 'auth_label_title', None), 'auth_label', None), (getattr(self, 'btn_refresh', None), 'btn_refresh', None), (getattr(self, 'hint_label', None), 'hint_factory', None), + (getattr(self, 'pwd_query_label', None), 'pwd_query_label', None), + (getattr(self, 'hotspot_title_label', None), 'hotspot_title', None), + (getattr(self, 'hotspot_ssid_label', None), 'hotspot_name_detecting', None), + (getattr(self, 'hotspot_pwd_label', None), 'hotspot_pwd_default', None), + (getattr(self, 'hotspot_status_label', None), 'hotspot_status_off', None), + (getattr(self, 'btn_hotspot', None), 'hotspot_start', None), + (getattr(self, 'hint_title_label', None), 'hint_title', None), ] for w, key, _ in widgets: - if w: w.config(text=t(key)) + if not w: + continue + text = t(key) + if key == 'title': + text = "🚀 " + text + w.config(text=text) self.btn_theme_switch.config(text=t('theme_light') if self.theme == 'dark' else t('theme_dark')) self.btn_lang_switch.config(text=t('lang_en') if self.lang == 'zh' else t('lang_zh')) + if self.is_placeholder_vin(self.vin_input.get()): + self.vin_input.delete(0, tk.END) + self.vin_input.insert(0, t('vin_placeholder')) + self._render_hint_lines() if self.vin: self._update_device_status_impl(self.device_connected, self.vin, getattr(self, '_last_authorized', False)) def _log_impl(self, message, level="INFO"): """日志写入的实际实现(必须在主线程调用)""" + if not self.debug_mode and level in ("INFO", "CMD"): + return timestamp = datetime.now().strftime("%H:%M:%S") log_entry = f"[{timestamp}] [{level}] {message}\n" self.log_text.insert(tk.END, log_entry, level) self.log_text.see(tk.END) + def log(self, message, level="INFO"): """添加日志(线程安全)""" self.run_on_ui_thread(self._log_impl, message, level) @@ -789,10 +972,35 @@ class ADKAPKGUI: if self.debug_mode: return True if not self.device_connected: - messagebox.showwarning("设备未连接", "请先连接设备并点击「检查」按钮刷新状态!") + messagebox.showwarning(self.t('msg_device_not_connected_title'), self.t('msg_device_not_connected')) return False return True + def unlock_install_permission(self): + """解锁安装权限。""" + if not self.check_device_connection(): + return + + def worker(): + ok, output = self.run_adb_shell('setprop vecentek.model 1') + if ok: + self.log(self.t('log_unlock_success'), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('msg_success_title'), + self.t('msg_unlock_success') + ) + else: + msg = output or self.t('msg_error_title') + self.log(self.tf('log_unlock_failed', output=msg), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_error_title'), + self.tf('msg_unlock_failed', output=msg) + ) + + threading.Thread(target=worker, daemon=True).start() + def start_device_monitor(self): """启动设备状态监控(每5秒检查一次)""" def monitor(): @@ -1189,7 +1397,7 @@ class ADKAPKGUI: if not ok: return False, f"push失败: {err}" - ok, err = self.run_adb_shell(f'pm install -r -d -f {temp_apk_path}') + ok, err = self.run_adb_shell(f'pm install -d -f -r {temp_apk_path}') self.run_adb_shell(f'rm -f {temp_apk_path}') if not ok: return False, f"install失败: {err}" @@ -1201,6 +1409,42 @@ class ADKAPKGUI: ok, _ = self.push_single_apk(apk_path, apk_name) return ok + def cleanup_preinstalled_apps_for_language(self): + """Disable and uninstall built-in apps before flashing language packages.""" + packages = [ + "com.wtcl.electronicdirections", + "com.tinnove.netease.music", + "com.changan.appmarket", + ] + failed = [] + + for package in packages: + disable_ok, disable_output = self.run_adb_shell(f'pm disable-user {package}') + uninstall_ok, uninstall_output = self.run_adb_shell(f'pm uninstall -k --user 0 {package}') + + if self.debug_mode: + if disable_ok: + self.log(f"禁用完成: {package}", "CMD") + else: + self.log(f"禁用失败: {package} {disable_output}", "CMD") + if uninstall_ok: + self.log(f"卸载完成: {package}", "CMD") + else: + self.log(f"卸载失败: {package} {uninstall_output}", "CMD") + + if not disable_ok or not uninstall_ok: + failed.append(package) + + if failed: + if self.debug_mode: + self.log("预置应用清理部分失败: " + ", ".join(failed), "WARNING") + else: + self.log("预置应用清理部分失败,继续刷入语言包", "WARNING") + return False + + self.log("预置应用清理完成", "SUCCESS") + return True + def push_all_apks(self): """推送APK并安装 —— 逸动版仅处理 app 目录,使用 pm install""" # 检查设备连接(仅 UI 层检查在主线程,其余工作进后台线程) @@ -1208,19 +1452,23 @@ class ADKAPKGUI: return if not self.vin: - messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码") + messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_need_vin')) return def do_push_all(): # 验证授权 if not self.check_authorization(self.vin): - self.run_on_ui_thread(lambda: messagebox.showerror("授权失败", "设备未授权")) + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized')) + ) return # 获取解压密码 if not self.extract_password: if not self.fetch_package_password(): - self.run_on_ui_thread(lambda: messagebox.showerror("错误", "数据准备失败!")) + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_data_prepare_failed')) + ) return # 解压 @@ -1229,12 +1477,16 @@ class ADKAPKGUI: self.show_progress(True, is_push=False) if not self.extract_package_silent(): self.show_progress(False, is_push=False) - self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源准备失败!")) + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_prepare_failed')) + ) return self.show_progress(False, is_push=False) if not self.apps_dir or not self.apps_dir.exists(): - self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源目录未找到")) + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_dir_missing')) + ) return # 开始刷入 @@ -1242,6 +1494,7 @@ class ADKAPKGUI: self.log("开始刷入语言包...", "INFO") self.run_adb_shell('mkdir -p /data/local/tmp') self.run_adb_shell('setprop vecentek.model 1') + self.cleanup_preinstalled_apps_for_language() success_count = 0 try: @@ -1255,11 +1508,15 @@ class ADKAPKGUI: apk_name = apk_path.stem ok, _ = self.push_single_apk(apk_path, apk_name) if ok: - self.log(f"安装成功: {apk_name}.apk", "SUCCESS") + if self.debug_mode: + 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) + if self.debug_mode: + self.log(f"安装失败: {apk_name}.apk", "ERROR") + else: + self.log(f"语言包刷入失败: {i}/{total}", "ERROR") + self.update_progress(i, total, "正在刷入", is_push=True) self.update_progress(total, total, "刷入完成", is_push=True) @@ -1268,7 +1525,6 @@ class ADKAPKGUI: else: self.log("语言包刷入失败", "ERROR") finally: - self.run_adb_shell('setprop vecentek.model 0') self.show_progress(False, is_push=True) threading.Thread(target=do_push_all, daemon=True).start() @@ -1339,7 +1595,6 @@ class ADKAPKGUI: self.log(f"安装过程异常: {str(e)}", "ERROR") self.run_on_ui_thread(messagebox.showerror, "安装失败", f"安装过程异常:{str(e)}") finally: - self.run_adb_shell('setprop vecentek.model 0') self.show_progress(False, is_push=True) threading.Thread(target=install, daemon=True).start() @@ -1379,7 +1634,6 @@ class ADKAPKGUI: except Exception as e: self.log(f"安装过程异常: {str(e)}", "ERROR") finally: - self.run_adb_shell('setprop vecentek.model 0') self.show_progress(False, is_push=True) threading.Thread(target=install, daemon=True).start() @@ -1398,7 +1652,7 @@ class ADKAPKGUI: # 创建弹窗 popup = tk.Toplevel(self.root) - popup.title("快捷语言设置") + popup.title(self.t('quick_lang_title')) popup.geometry("520x320") popup.configure(bg=self.colors['bg_dark']) popup.resizable(False, False) @@ -1412,29 +1666,21 @@ class ADKAPKGUI: popup.grab_set() # 标题 - header = tk.Label(popup, text="选择目标语言", + header = tk.Label(popup, text=self.t('quick_lang_header'), font=('Microsoft YaHei', 13, 'bold'), fg=self.colors['accent'], bg=self.colors['bg_dark']) header.pack(pady=(15, 10)) - hint = tk.Label(popup, text="点击按钮即可将系统语言切换为对应语言,重启后生效", + hint = tk.Label(popup, text=self.t('quick_lang_hint'), font=('Microsoft YaHei', 9), fg=self.colors['text_secondary'], bg=self.colors['bg_dark']) hint.pack(pady=(0, 12)) # 语言列表:(显示名, locale_code) - 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"), - ] + locale_codes = ["zh-CN", "en-US", "ru-RU", "fr-FR", "es-ES", "pt-BR", "it-IT", "ar-SA"] + languages = list(zip(self.t('quick_lang_names'), locale_codes)) # 创建按钮容器 btn_frame = tk.Frame(popup, bg=self.colors['bg_dark']) @@ -1468,7 +1714,7 @@ class ADKAPKGUI: 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="⚙️ 打开系统语言设置(手动选择)", + sys_btn = tk.Button(popup, text=self.t('quick_lang_system'), command=lambda: self._open_sys_and_close(popup), font=('Microsoft YaHei', 9), fg=self.colors['text_secondary'], @@ -1482,21 +1728,24 @@ class ADKAPKGUI: 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") + self.log(self.tf('log_quick_lang_success', language=language_name), "SUCCESS") self.run_on_ui_thread( messagebox.showinfo, - "设置成功", - f"系统语言已设置为 {language_name}\n\n⚠️ 请重启设备使其生效。" + self.t('quick_lang_success_title'), + self.tf('quick_lang_success', language=language_name) ) else: - self.log(f"✗ 语言设置失败: {output}", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "设置失败", f"语言设置失败!\n\n{output}") + self.log(self.tf('log_quick_lang_failed', output=output), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('quick_lang_failed_title'), + self.tf('quick_lang_failed', output=output) + ) threading.Thread(target=do_set, daemon=True).start() @@ -1570,21 +1819,21 @@ class ADKAPKGUI: def _on_vin_input_focus_in(self, event): """输入框获得焦点时清除占位符""" - if self.vin_input.get() == "请输入VIN": + if self.is_placeholder_vin(self.vin_input.get()): self.vin_input.delete(0, tk.END) self.vin_input.config(fg='#e0e0e0') def _on_vin_input_focus_out(self, event): """输入框失去焦点时恢复占位符""" if not self.vin_input.get(): - self.vin_input.insert(0, "请输入VIN") + self.vin_input.insert(0, self.t('vin_placeholder')) self.vin_input.config(fg='#636e72') def query_password_by_vin(self): """通过VIN查询密码""" vin = self.vin_input.get().strip() - if not vin: - messagebox.showwarning("提示", "请输入VIN码") + if not vin or self.is_placeholder_vin(vin): + messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_input_vin')) return def do_query(): @@ -1696,7 +1945,6 @@ class ADKAPKGUI: self.log(f"安装过程异常: {str(e)}", "ERROR") self.run_on_ui_thread(messagebox.showerror, "安装失败", f"安装过程异常:{str(e)}") finally: - self.run_adb_shell('setprop vecentek.model 0') self.show_progress(False, is_push=True) threading.Thread(target=install, daemon=True).start() @@ -1708,20 +1956,48 @@ class ADKAPKGUI: def modify_hosts(self): """修改hosts文件,添加云端认证DNS映射""" hosts_path = r"C:\Windows\System32\drivers\etc\hosts" - entry = "103.236.55.140 spm.auto-pai.com" + host_ip = "103.236.55.140" + host_name = "spm.auto-pai.com" + entry = f"{host_ip} {host_name}" try: - with open(hosts_path, 'r', encoding='utf-8') as f: - content = f.read() + try: + with open(hosts_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + except UnicodeDecodeError: + with open(hosts_path, 'r', encoding='gbk', errors='replace') as f: + lines = f.readlines() - if entry in content: - # self.log("hosts条目已存在,无需修改", "INFO") + new_lines = [] + changed = False + has_exact = False + for line in lines: + stripped = line.strip() + if not stripped or stripped.startswith('#'): + new_lines.append(line) + continue + + body, _, _ = line.partition('#') + parts = body.split() + if len(parts) >= 2 and host_name.lower() in [p.lower() for p in parts[1:]]: + if parts[0] == host_ip and len(parts) == 2: + has_exact = True + new_lines.append(line) + else: + changed = True + continue + + new_lines.append(line) + + if has_exact and not changed: return True - with open(hosts_path, 'a', encoding='utf-8') as f: - f.write(f"\n{entry}\n") + if not new_lines or (new_lines[-1] and not new_lines[-1].endswith(('\n', '\r'))): + new_lines.append('\n') + new_lines.append(f"{entry}\n") - # self.log(f"已添加hosts条目: {entry}", "SUCCESS") + with open(hosts_path, 'w', encoding='utf-8', newline='') as f: + f.writelines(new_lines) return True except PermissionError: self.log("需要管理员权限,请以管理员身份运行", "WARNING") @@ -1873,15 +2149,15 @@ class ADKAPKGUI: def _refresh_hotspot_display_impl(self, ssid, password, status): """刷新热点显示的UI实现""" if ssid: - self.hotspot_ssid_label.config(text=f"名称: {ssid}") + self.hotspot_ssid_label.config(text=self.tf('hotspot_name_value', ssid=ssid)) else: - self.hotspot_ssid_label.config(text="名称: 未配置") + self.hotspot_ssid_label.config(text=self.t('hotspot_name_empty')) if password: - self.hotspot_pwd_label.config(text=f"密码: {password}") + self.hotspot_pwd_label.config(text=self.tf('hotspot_pwd_value', password=password)) else: - self.hotspot_pwd_label.config(text="密码: changan2024") + self.hotspot_pwd_label.config(text=self.t('hotspot_pwd_default')) self.hotspot_status_label.config( - text=f"状态: {status}", + text=self.tf('hotspot_status_value', status=status), fg=self.colors['success'] if '已启动' in status else self.colors['warning'] ) diff --git a/CS55-Q05/cs55-q05.ico b/CS55-Q05/cs55-q05.ico new file mode 100644 index 0000000..ad7d089 Binary files /dev/null and b/CS55-Q05/cs55-q05.ico differ diff --git a/CS55-Q05/engmode.key b/CS55-Q05/engmode.key new file mode 100644 index 0000000..fba6924 --- /dev/null +++ b/CS55-Q05/engmode.key @@ -0,0 +1 @@ +Q1DLEsRw96XfzeCXtWG5PlTP10PpL8HbHQvr4YUqDzhUCzSboW924hT5xhjKu78S5ZZGGhxqjyPDh3GHf9L4cqCwpeg2tfKlsl/b7bmUWVVY5KfpwhRQuJlhQn7VJkycYe+TNb89n56mLYIV7DhNLhgGENXdMjHWPdUXQhNanpata8n920J2GGe71dONPRh/Ljeab2sv8SJprCaUYaSwxtkXXz5OS/9L5CRGJ13YDuu+aEIQp5R4i25mqqL4KD+zSkjkyHfQzzemIolfueVya27xZYULSBnQCYL/39GjdTM5btZ7OL9FCjzXROfQyyZ21HZIZgKo/p1wuRH7j4Ne0g== \ No newline at end of file diff --git a/CS55-Q05/pack_cs55_q05.bat b/CS55-Q05/pack_cs55_q05.bat new file mode 100644 index 0000000..77d5a30 --- /dev/null +++ b/CS55-Q05/pack_cs55_q05.bat @@ -0,0 +1,133 @@ +@echo off +chcp 65001 >nul +cd /d "%~dp0" +set "ROOT=%~dp0.." +set "TOOLS=%ROOT%\tools" +set "NAME=CS55-Q05-Installer" +set "SRC=CS55-Q05_Installer.py" +set "ICON=cs55-q05.ico" +title %NAME% - Cython Build + +echo ============================================================ +echo %NAME% - Cython Build +echo ============================================================ +echo. + +where python >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Python not found + pause + exit /b 1 +) +for /f "delims=" %%i in ('where python') do set "PY=%%i" +echo Python: %PY% + +if not exist "%SRC%" ( + echo [ERROR] Source not found: %SRC% + pause + exit /b 1 +) +if not exist "%ICON%" ( + echo [ERROR] Icon not found: %ICON% + pause + exit /b 1 +) +if not exist "%TOOLS%\adb.exe" ( + echo [ERROR] adb.exe not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\7za.exe" ( + echo [ERROR] 7za.exe not found in %TOOLS% + pause + exit /b 1 +) + +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 +) +if errorlevel 1 ( + echo [ERROR] Dependency install failed + pause + exit /b 1 +) + +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 +if exist "%NAME%.spec" del /q "%NAME%.spec" 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',encoding='utf-8').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 [ERROR] Cython failed. Build stopped. + pause + exit /b 1 +) + +set "PYD=" +for %%f in (_core*.pyd) do set "PYD=%%f" +if "%PYD%"=="" ( + cd .. + echo [ERROR] No Cython PYD generated. Build stopped. + pause + exit /b 1 +) +echo PYD: %PYD% +copy "%PYD%" _core.pyd >nul +if errorlevel 1 ( + cd .. + echo [ERROR] Failed to copy Cython PYD + pause + exit /b 1 +) + +%PY% -c "open('launcher.py','w',encoding='utf-8').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')" + +echo [4/6] Copy resources... +copy "%TOOLS%\adb.exe" . >nul +copy "%TOOLS%\AdbWinApi.dll" . >nul +copy "%TOOLS%\AdbWinUsbApi.dll" . >nul +copy "%TOOLS%\7za.exe" . >nul +copy "..\%ICON%" . >nul +if errorlevel 1 ( + cd .. + echo [ERROR] Copy resources failed + pause + exit /b 1 +) + +echo [5/6] PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ICON%" --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --add-data "%ICON%;." --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 1 +) + +echo [6/6] Cleanup... +del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py "%ICON%" 2>nul +rmdir /s /q build 2>nul +cd .. + +echo. +echo Done. +if exist "dist_cy\dist\%NAME%.exe" ( + echo Output: dist_cy\dist\%NAME%.exe +) else ( + echo [ERROR] Output exe was not generated + pause + exit /b 1 +) +pause diff --git a/Mazda-EZ60/Mazda-EZ60.py b/CS75Pro/CS75Pro_Installer.py similarity index 64% rename from Mazda-EZ60/Mazda-EZ60.py rename to CS75Pro/CS75Pro_Installer.py index f64d044..5ec13e8 100644 --- a/Mazda-EZ60/Mazda-EZ60.py +++ b/CS75Pro/CS75Pro_Installer.py @@ -5,13 +5,14 @@ import os import sys import subprocess import json -import threading 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: @@ -58,11 +59,30 @@ def find_tool(file_name, fallback=None): path = find_resource(file_name) if path.exists(): return str(path) - return fallback or str(path) + return fallback or str(path) + + +def set_windows_app_user_model_id(): + if sys.platform != 'win32': + return + try: + import ctypes + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + "yibin.keyi.cs75pro.language.installer" + ) + except Exception: + pass + + class ADKAPKGUI: + # CS75Pro package-key vehicleName is intentionally hardcoded. + # Do not replace this with auth-check data.vehicleName in future edits. + PACKAGE_KEY_VEHICLE_NAME = "CS75Pro" + def __init__(self): + set_windows_app_user_model_id() self.root = tk.Tk() - self.root.title("Mazda-EZ60_OS-1.2适用") + self.root.title("长安语言刷入工具") self.root.geometry("900x620") self.root.resizable(True, True) @@ -100,7 +120,9 @@ class ADKAPKGUI: self.lang = 'zh' self.T = { 'zh': { - 'title': 'Mazda-EZ60_OS-1.2适用', + 'window_title': 'CS75Pro 语言刷入工具', + 'title': 'CS75Pro', + 'btn_unlock_install': '🔓 解锁安装权限', 'btn_push': '📦 刷入语言包', 'btn_install': '📱 安装App', 'btn_language': '🌐 语言设置', @@ -110,6 +132,8 @@ class ADKAPKGUI: 'btn_disable_upgrade': '❌ 禁用升级', 'btn_clear_log': '🗑 清空日志', 'btn_query_pwd': '查询密码', + 'pwd_query_label': '工程密码查询:', + 'vin_placeholder': '请输入VIN', 'device_label': '设备:', 'vin_label': 'VIN码:', 'auth_label': '授权:', @@ -126,15 +150,60 @@ class ADKAPKGUI: 'hint_factory': '🔧 关闭车辆WI-FI和4G网络,拨号获取的密码进入工程模式', 'hotspot_title': '📶 电脑热点', 'hotspot_start': '🔧 打开热点设置', + 'hotspot_name_detecting': '名称: 检测中...', + 'hotspot_name_value': '名称: {ssid}', + 'hotspot_name_empty': '名称: 未配置', + 'hotspot_pwd_default': '密码: changan2024', + 'hotspot_pwd_value': '密码: {password}', + 'hotspot_status_value': '状态: {status}', + 'hotspot_status_off': '状态: 未启动', 'hint_title': '💡 使用提示', + 'hint_lines': [ + '1. 确保电脑已开启热点', + '2. 拨号进入工厂模式,点击调试工具', + '3. 需要云端认证时,点击车机状态栏', + ' Wi-Fi图标,连接上方显示的热点', + '4. 连接后点击车机“云端认证”按钮', + '5. 打开ADB后即可正常刷入语言包', + ], 'theme_dark': '🌙 暗色', 'theme_light': '☀️ 亮色', 'lang_zh': '中', 'lang_en': 'EN', - 'about_company': '宜宾科宜科技有限公司 - 出口改装一站式服务', + 'log_lang_changed': '语言已切换为中文', + 'log_cleared': '日志已清空', + 'log_unlock_success': '安装权限已解锁', + 'log_unlock_failed': '安装权限解锁失败: {output}', + 'msg_success_title': '成功', + 'msg_error_title': '错误', + 'msg_warn_title': '警告', + 'msg_device_not_connected_title': '设备未连接', + 'msg_device_not_connected': '请先连接设备并点击「检查」按钮刷新状态!', + 'msg_unlock_success': '安装权限已解锁,可以继续安装或刷入语言包。', + 'msg_unlock_failed': '安装权限解锁失败:{output}', + 'msg_input_vin': '请输入VIN码', + 'msg_need_vin': '请先刷新设备状态并获取VIN码', + 'msg_auth_failed_title': '授权失败', + 'msg_device_unauthorized': '设备未授权', + 'msg_data_prepare_failed': '数据准备失败!', + 'msg_resource_prepare_failed': '资源准备失败!', + 'msg_resource_dir_missing': '资源目录未找到', + 'quick_lang_title': '快捷语言设置', + 'quick_lang_header': '选择目标语言', + 'quick_lang_hint': '点击按钮即可将系统语言切换为对应语言,重启后生效', + 'quick_lang_system': '⚙️ 打开系统语言设置(手动选择)', + 'quick_lang_success_title': '设置成功', + 'quick_lang_success': '系统语言已设置为 {language}\n\n⚠️ 请重启设备使其生效。', + 'quick_lang_failed_title': '设置失败', + 'quick_lang_failed': '语言设置失败!\n\n{output}', + 'quick_lang_names': ['🇨🇳 中文', '英 English', '俄 Русский', '法 Français', '西 Español', '葡 Português', '意 Italiano', '阿 العربية'], + 'log_quick_lang_success': '语言已设置为 {language}', + 'log_quick_lang_failed': '语言设置失败: {output}', }, 'en': { - 'title': 'Mazda-EZ60_OS-1.2适用', + 'window_title': 'CS75Pro Language Installer', + 'title': 'CS75Pro', + 'btn_unlock_install': '🔓 Unlock Install', 'btn_push': '📦 Flash Lang Pkg', 'btn_install': '📱 Install App', 'btn_language': '🌐 Language', @@ -144,6 +213,8 @@ class ADKAPKGUI: 'btn_disable_upgrade': '❌ Disable OTA', 'btn_clear_log': '🗑 Clear Log', 'btn_query_pwd': 'Query Pwd', + 'pwd_query_label': 'Factory password:', + 'vin_placeholder': 'Enter VIN', 'device_label': 'Device:', 'vin_label': 'VIN:', 'auth_label': 'Auth:', @@ -160,12 +231,55 @@ class ADKAPKGUI: 'hint_factory': '🔧 Turn off WiFi & 4G, enter factory mode with dial code', 'hotspot_title': '📶 Hotspot', 'hotspot_start': '🔧 Open Hotspot Settings', + 'hotspot_name_detecting': 'Name: detecting...', + 'hotspot_name_value': 'Name: {ssid}', + 'hotspot_name_empty': 'Name: not configured', + 'hotspot_pwd_default': 'Password: changan2024', + 'hotspot_pwd_value': 'Password: {password}', + 'hotspot_status_value': 'Status: {status}', + 'hotspot_status_off': 'Status: off', 'hint_title': '💡 Tips', + 'hint_lines': [ + '1. Make sure the PC hotspot is enabled', + '2. Enter factory mode from the dialer', + '3. When cloud auth is needed, tap the', + ' Wi-Fi icon and connect to the hotspot', + '4. Tap Cloud Auth on the vehicle screen', + '5. Enable ADB, then flash the language pack', + ], 'theme_dark': '🌙 Dark', 'theme_light': '☀️ Light', 'lang_zh': '中', 'lang_en': 'EN', - 'about_company': 'Yibin Keyi Technology - Export Modification Service', + 'log_lang_changed': 'Language switched to English', + 'log_cleared': 'Log cleared', + 'log_unlock_success': 'Install permission unlocked', + 'log_unlock_failed': 'Install permission unlock failed: {output}', + 'msg_success_title': 'Success', + 'msg_error_title': 'Error', + 'msg_warn_title': 'Warning', + 'msg_device_not_connected_title': 'Device Not Connected', + 'msg_device_not_connected': 'Connect the device and click Check first.', + 'msg_unlock_success': 'Install permission is unlocked. You can continue installing or flashing.', + 'msg_unlock_failed': 'Install permission unlock failed: {output}', + 'msg_input_vin': 'Enter VIN', + 'msg_need_vin': 'Refresh device status and get VIN first.', + 'msg_auth_failed_title': 'Authorization Failed', + 'msg_device_unauthorized': 'Device is not authorized', + 'msg_data_prepare_failed': 'Data preparation failed.', + 'msg_resource_prepare_failed': 'Resource preparation failed.', + 'msg_resource_dir_missing': 'Resource directory not found', + 'quick_lang_title': 'Quick Language', + 'quick_lang_header': 'Choose target language', + 'quick_lang_hint': 'Tap a button to switch system language. Reboot to apply.', + 'quick_lang_system': '⚙️ Open system language settings', + 'quick_lang_success_title': 'Language Set', + 'quick_lang_success': 'System language was set to {language}.\n\n⚠️ Reboot the device to apply it.', + 'quick_lang_failed_title': 'Language Failed', + 'quick_lang_failed': 'Language setting failed.\n\n{output}', + 'quick_lang_names': ['Chinese', 'English', 'Russian', 'French', 'Spanish', 'Portuguese', 'Italian', 'Arabic'], + 'log_quick_lang_success': 'Language set to {language}', + 'log_quick_lang_failed': 'Language setting failed: {output}', } } @@ -181,27 +295,12 @@ class ADKAPKGUI: self.device_connected = False self._refreshing = False # 防止并发刷新 self.debug_mode = False # 调试模式 - self.mazda_overlay_packages = [ - "com.tinnove.launcher.overlay", - "com.tinnove.scenemode.overlay", - "com.incall.dvr.overlay", - ] - self.mazda_disable_packages = [ - "com.carinno.p1", - "com.wtcl.electronicdirections", - "com.ximalaya.ting.android.car", - "com.tinnove.netease.music", - "com.migu.miguplay.car", - "cn.cmvideo.car.play", - "com.tinnove.carshow", - "com.tinnove.changba", - "com.qiyi.video.iv", - "com.changan.appmarket" - ] + self.root.title(self.t('window_title')) # 设置样式 self.setup_styles() self.setup_ui() + self.root.after(200, self.set_window_icon) self.center_window() # 检查环境 @@ -210,6 +309,38 @@ class ADKAPKGUI: # 启动设备状态监控 self.start_device_monitor() + def set_window_icon(self): + """Set the Tk window/taskbar icon at runtime; PyInstaller --icon only sets the exe file icon.""" + try: + icon_path = find_resource("cs75pro.ico") + if icon_path.exists(): + self.root.iconbitmap(str(icon_path)) + self._set_windows_hwnd_icon(icon_path) + except Exception: + pass + + def _set_windows_hwnd_icon(self, icon_path): + if sys.platform != 'win32': + return + try: + import ctypes + user32 = ctypes.windll.user32 + hwnd = self.root.winfo_id() + image_icon = 1 + lr_loadfromfile = 0x00000010 + wm_seticon = 0x0080 + icon_small = 0 + icon_big = 1 + path = str(icon_path) + small = user32.LoadImageW(None, path, image_icon, 16, 16, lr_loadfromfile) + big = user32.LoadImageW(None, path, image_icon, 32, 32, lr_loadfromfile) + if small: + user32.SendMessageW(hwnd, wm_seticon, icon_small, small) + if big: + user32.SendMessageW(hwnd, wm_seticon, icon_big, big) + except Exception: + pass + def setup_styles(self): """设置自定义样式""" style = ttk.Style() @@ -251,28 +382,22 @@ class ADKAPKGUI: title_frame.pack_propagate(False) # 标题 - title_label = tk.Label(title_frame, - text="🚀 Mazda-EZ60_OS-1.2适用", - 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() + self.title_label = tk.Label(title_frame, + text="🚀 " + self.t('title'), + font=('Microsoft YaHei', 18, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.title_label.pack() # 工程密码查询区域 pwd_query_frame = tk.Frame(left_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) pwd_query_frame.pack(fill=tk.X, pady=(0, 5), padx=5) - 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.pwd_query_label = tk.Label(pwd_query_frame, text=self.t('pwd_query_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.pwd_query_label.pack(side=tk.LEFT, padx=(10, 5), pady=5) self.vin_input = tk.Entry(pwd_query_frame, font=('Consolas', 9), @@ -281,12 +406,12 @@ class ADKAPKGUI: insertbackground='white', relief=tk.FLAT, width=20) - self.vin_input.insert(0, "请输入VIN") + self.vin_input.insert(0, self.t('vin_placeholder')) 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="查询密码", + self.btn_query_pwd = tk.Button(pwd_query_frame, text=self.t('btn_query_pwd'), command=self.query_password_by_vin, font=('Microsoft YaHei', 8), fg='white', @@ -305,10 +430,11 @@ class ADKAPKGUI: # 工厂模式提示 factory_hint_frame = tk.Frame(left_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) + self.hint_label = tk.Label(factory_hint_frame, text=self.t('hint_factory'), + font=('Microsoft YaHei', 8), + fg=self.colors['warning'], + bg=self.colors['bg_dark']) + self.hint_label.pack(side=tk.LEFT, padx=2) # 按钮区域(两排,每排5个) button_frame = tk.Frame(left_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) @@ -321,26 +447,32 @@ class ADKAPKGUI: 'relief': tk.FLAT, 'cursor': 'hand2', 'height': 1, - 'width': 14 + 'width': 13 } # 第一排按钮 row1_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) row1_frame.pack(pady=(8, 4)) - self.btn_push = tk.Button(row1_frame, text="📦 刷入语言包", + self.btn_push = tk.Button(row1_frame, text=self.t('btn_push'), command=self.push_all_apks, bg=self.colors['accent'], **btn_params) self.btn_push.pack(side=tk.LEFT, padx=4) - self.btn_install_all = tk.Button(row1_frame, text="📱 安装App", + self.btn_unlock_install = tk.Button(row1_frame, text=self.t('btn_unlock_install'), + command=self.unlock_install_permission, + bg=self.colors['warning'], + **btn_params) + self.btn_unlock_install.pack(side=tk.LEFT, padx=4) + + self.btn_install_all = tk.Button(row1_frame, text=self.t('btn_install'), command=self.install_apps, bg=self.colors['accent'], **btn_params) self.btn_install_all.pack(side=tk.LEFT, padx=4) - self.btn_language = tk.Button(row1_frame, text="🌐 语言设置", + self.btn_language = tk.Button(row1_frame, text=self.t('btn_language'), command=self.open_language_quick_set, bg=self.colors['accent'], **btn_params) @@ -350,25 +482,25 @@ class ADKAPKGUI: row2_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) row2_frame.pack(pady=(4, 8)) - self.btn_timezone = tk.Button(row2_frame, text="⏰ 时区设置", + self.btn_timezone = tk.Button(row2_frame, text=self.t('btn_timezone'), command=self.open_timezone_settings, bg=self.colors['accent'], **btn_params) self.btn_timezone.pack(side=tk.LEFT, padx=4) - self.btn_settings = tk.Button(row2_frame, text="⚙️ 安卓设置", + self.btn_settings = tk.Button(row2_frame, text=self.t('btn_settings'), command=self.open_android_settings, bg=self.colors['accent'], **btn_params) self.btn_settings.pack(side=tk.LEFT, padx=4) - self.btn_reboot = tk.Button(row2_frame, text="🔄 重启设备", + self.btn_reboot = tk.Button(row2_frame, text=self.t('btn_reboot'), command=self.reboot_device, bg=self.colors['warning'], **btn_params) self.btn_reboot.pack(side=tk.LEFT, padx=4) - self.btn_exit = tk.Button(row2_frame, text="❌ 禁用升级", + self.btn_exit = tk.Button(row2_frame, text=self.t('btn_disable_upgrade'), command=self.on_disable_upgrade, bg=self.colors['error'], **btn_params) @@ -387,12 +519,13 @@ class ADKAPKGUI: 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_label = tk.Label(status_indicator_frame, text=self.t('device_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.device_label.pack(side=tk.LEFT, padx=(5, 3)) - self.device_status_label = tk.Label(status_indicator_frame, text="未检测", + self.device_status_label = tk.Label(status_indicator_frame, text=self.t('status_detecting'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light']) @@ -401,11 +534,12 @@ class ADKAPKGUI: # 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="未获取", + self.vin_label_title = tk.Label(vin_frame, text=self.t('vin_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.vin_label_title.pack(side=tk.LEFT) + self.vin_label = tk.Label(vin_frame, text=self.t('vin_none'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light']) @@ -414,25 +548,26 @@ class ADKAPKGUI: # 授权状态 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="未验证", + self.auth_label_title = tk.Label(auth_frame, text=self.t('auth_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.auth_label_title.pack(side=tk.LEFT) + self.auth_label = tk.Label(auth_frame, text=self.t('auth_none'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light']) self.auth_label.pack(side=tk.LEFT, padx=(5, 0)) # 刷新按钮 - 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) + self.btn_refresh = tk.Button(status_bar_frame, text=self.t('btn_refresh'), + command=lambda: self.refresh_device_status(force=True), + font=('Microsoft YaHei', 8), + fg=self.colors['accent'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + self.btn_refresh.pack(side=tk.RIGHT, padx=10, pady=5) # 解压进度条框架 progress_frame = tk.Frame(left_frame, bg=self.colors['bg_dark']) @@ -464,12 +599,13 @@ class ADKAPKGUI: 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.log_title_label = tk.Label(log_title_frame, text=self.t('log_title'), + font=('Microsoft YaHei', 10, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.log_title_label.pack(side=tk.LEFT, padx=10) - self.btn_clear = tk.Button(log_title_frame, text="🗑 清空日志", + self.btn_clear = tk.Button(log_title_frame, text=self.t('btn_clear_log'), command=self.clear_log, font=('Microsoft YaHei', 8), fg=self.colors['text_secondary'], @@ -505,14 +641,14 @@ class ADKAPKGUI: bottom_status.pack(fill=tk.X, pady=(5, 0)) bottom_status.pack_propagate(False) - self.status_text = tk.Label(bottom_status, text="就绪", + self.status_text = tk.Label(bottom_status, text=self.t('status_ready'), font=('Microsoft YaHei', 8), fg=self.colors['text_secondary'], bg=self.colors['bg_light']) self.status_text.pack(side=tk.LEFT, padx=10) # 主题和语言切换按钮 - self.btn_theme_switch = tk.Button(bottom_status, text="🌙 暗色", + self.btn_theme_switch = tk.Button(bottom_status, text=self.t('theme_light'), command=self.toggle_theme, font=('Microsoft YaHei', 8), fg=self.colors['accent'], @@ -529,36 +665,38 @@ class ADKAPKGUI: # 调试模式快捷键 self.root.bind('', self._toggle_debug) + self.root.bind('', self._debug_test_extract) # ========== 右侧提示面板 ========== # 热点信息卡片 hotspot_card = tk.Frame(right_frame, bg=self.colors['bg_dark'], relief=tk.RAISED, bd=1) hotspot_card.pack(fill=tk.X, padx=5, pady=(10, 5)) - tk.Label(hotspot_card, text="📶 电脑热点", - font=('Microsoft YaHei', 11, 'bold'), - fg=self.colors['accent'], - bg=self.colors['bg_dark']).pack(pady=(8, 5)) + self.hotspot_title_label = tk.Label(hotspot_card, text=self.t('hotspot_title'), + font=('Microsoft YaHei', 11, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.hotspot_title_label.pack(pady=(8, 5)) - self.hotspot_ssid_label = tk.Label(hotspot_card, text="名称: 检测中...", + self.hotspot_ssid_label = tk.Label(hotspot_card, text=self.t('hotspot_name_detecting'), font=('Microsoft YaHei', 9), fg=self.colors['text'], bg=self.colors['bg_dark']) self.hotspot_ssid_label.pack(anchor='w', padx=10, pady=2) - self.hotspot_pwd_label = tk.Label(hotspot_card, text="密码: changan2024", + self.hotspot_pwd_label = tk.Label(hotspot_card, text=self.t('hotspot_pwd_default'), font=('Microsoft YaHei', 9), fg=self.colors['text'], bg=self.colors['bg_dark']) self.hotspot_pwd_label.pack(anchor='w', padx=10, pady=2) - self.hotspot_status_label = tk.Label(hotspot_card, text="状态: 未启动", + self.hotspot_status_label = tk.Label(hotspot_card, text=self.t('hotspot_status_off'), font=('Microsoft YaHei', 9), fg=self.colors['warning'], bg=self.colors['bg_dark']) self.hotspot_status_label.pack(anchor='w', padx=10, pady=2) - self.btn_hotspot = tk.Button(hotspot_card, text="🔧 打开热点设置", + self.btn_hotspot = tk.Button(hotspot_card, text=self.t('hotspot_start'), command=self.start_hotspot_action, font=('Microsoft YaHei', 8), fg='white', @@ -571,36 +709,24 @@ class ADKAPKGUI: tk.Frame(right_frame, bg=self.colors['border'], height=1).pack(fill=tk.X, padx=8, pady=5) # 使用提示卡片 - hint_card = tk.Frame(right_frame, bg=self.colors['bg_dark'], relief=tk.RAISED, bd=1) - hint_card.pack(fill=tk.X, padx=5, pady=5) + self.hint_card = tk.Frame(right_frame, bg=self.colors['bg_dark'], relief=tk.RAISED, bd=1) + self.hint_card.pack(fill=tk.X, padx=5, pady=5) - tk.Label(hint_card, text="💡 使用提示", - font=('Microsoft YaHei', 11, 'bold'), - fg=self.colors['warning'], - bg=self.colors['bg_dark']).pack(pady=(8, 5)) + self.hint_title_label = tk.Label(self.hint_card, text=self.t('hint_title'), + font=('Microsoft YaHei', 11, 'bold'), + fg=self.colors['warning'], + bg=self.colors['bg_dark']) + self.hint_title_label.pack(pady=(8, 5)) - hint_lines = [ - "1. 确保电脑已开启热点", - "2. 拨号进入工厂模式,点击调试工具", - "3. 需要云端认证时,点击车机状态栏", - "Wi-Fi图标,连接上方显示的热点", - "4. 连接后点击车机“云端认证”按钮", - "5. 打开ADB后即可正常刷入语言包", - ] - for line in hint_lines: - tk.Label(hint_card, text=line, - font=('Microsoft YaHei', 8), - fg=self.colors['text_secondary'], - bg=self.colors['bg_dark'], - justify=tk.LEFT, - anchor='w').pack(anchor='w', padx=10) + self.hint_line_labels = [] + self._render_hint_lines() # 绑定悬停效果 self.bind_hover_effects() def bind_hover_effects(self): """绑定按钮悬停效果""" - buttons = [self.btn_push, self.btn_install_all, + buttons = [self.btn_push, self.btn_unlock_install, self.btn_install_all, self.btn_language, self.btn_timezone, self.btn_settings, self.btn_reboot, self.btn_clear, self.btn_exit, self.btn_query_pwd, self.btn_hotspot] @@ -643,14 +769,43 @@ class ADKAPKGUI: """将函数调度到主线程执行,确保线程安全""" self.root.after(0, lambda: func(*args, **kwargs)) + def _adb_cmd(self): + return subprocess.list2cmdline([self.adb]) + def t(self, key): return self.T.get(self.lang, self.T['zh']).get(key, key) + def tf(self, key, **kwargs): + return str(self.t(key)).format(**kwargs) + + def is_placeholder_vin(self, text): + return text in (self.T['zh']['vin_placeholder'], self.T['en']['vin_placeholder']) + def toggle_lang(self): self.lang = 'en' if self.lang == 'zh' else 'zh' self.btn_lang_switch.config(text=self.t('lang_en') if self.lang == 'zh' else self.t('lang_zh')) self._refresh_ui_texts() - self.log(f"语言已切换为 {'English' if self.lang == 'en' else '中文'}", "INFO") + self.log(self.t('log_lang_changed'), "INFO") + + def _render_hint_lines(self): + if not getattr(self, 'hint_card', None): + return + for label in getattr(self, 'hint_line_labels', []): + label.destroy() + self.hint_line_labels = [] + for line in self.t('hint_lines'): + label = tk.Label( + self.hint_card, + text=line, + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark'], + justify=tk.LEFT, + anchor='w', + wraplength=205 + ) + label.pack(anchor='w', fill=tk.X, padx=10, pady=1) + self.hint_line_labels.append(label) def toggle_theme(self): if self.theme == 'dark': @@ -684,9 +839,10 @@ class ADKAPKGUI: def _refresh_ui_texts(self): t = self.t + self.root.title(t('window_title')) widgets = [ (getattr(self, 'title_label', None), 'title', None), - (getattr(self, 'subtitle_label', None), 'about_company', None), + (getattr(self, 'btn_unlock_install', None), 'btn_unlock_install', None), (getattr(self, 'btn_push', None), 'btn_push', None), (getattr(self, 'btn_install_all', None), 'btn_install', None), (getattr(self, 'btn_language', None), 'btn_language', None), @@ -703,22 +859,41 @@ class ADKAPKGUI: (getattr(self, 'auth_label_title', None), 'auth_label', None), (getattr(self, 'btn_refresh', None), 'btn_refresh', None), (getattr(self, 'hint_label', None), 'hint_factory', None), + (getattr(self, 'pwd_query_label', None), 'pwd_query_label', None), + (getattr(self, 'hotspot_title_label', None), 'hotspot_title', None), + (getattr(self, 'hotspot_ssid_label', None), 'hotspot_name_detecting', None), + (getattr(self, 'hotspot_pwd_label', None), 'hotspot_pwd_default', None), + (getattr(self, 'hotspot_status_label', None), 'hotspot_status_off', None), + (getattr(self, 'btn_hotspot', None), 'hotspot_start', None), + (getattr(self, 'hint_title_label', None), 'hint_title', None), ] for w, key, _ in widgets: - if w: w.config(text=t(key)) + if not w: + continue + text = t(key) + if key == 'title': + text = "🚀 " + text + w.config(text=text) self.btn_theme_switch.config(text=t('theme_light') if self.theme == 'dark' else t('theme_dark')) self.btn_lang_switch.config(text=t('lang_en') if self.lang == 'zh' else t('lang_zh')) + if self.is_placeholder_vin(self.vin_input.get()): + self.vin_input.delete(0, tk.END) + self.vin_input.insert(0, t('vin_placeholder')) + self._render_hint_lines() if self.vin: self._update_device_status_impl(self.device_connected, self.vin, getattr(self, '_last_authorized', False)) def _log_impl(self, message, level="INFO"): """日志写入的实际实现(必须在主线程调用)""" + if not self.debug_mode and level in ("INFO", "CMD"): + return timestamp = datetime.now().strftime("%H:%M:%S") log_entry = f"[{timestamp}] [{level}] {message}\n" self.log_text.insert(tk.END, log_entry, level) self.log_text.see(tk.END) + def log(self, message, level="INFO"): """添加日志(线程安全)""" self.run_on_ui_thread(self._log_impl, message, level) @@ -801,16 +976,41 @@ class ADKAPKGUI: if self.debug_mode: return True if not self.device_connected: - messagebox.showwarning("设备未连接", "请先连接设备并点击「检查」按钮刷新状态!") + messagebox.showwarning(self.t('msg_device_not_connected_title'), self.t('msg_device_not_connected')) return False return True + def unlock_install_permission(self): + """解锁安装权限。""" + if not self.check_device_connection(): + return + + def worker(): + ok, output = self.run_adb_shell('setprop vecentek.model 1') + if ok: + self.log(self.t('log_unlock_success'), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('msg_success_title'), + self.t('msg_unlock_success') + ) + else: + msg = output or self.t('msg_error_title') + self.log(self.tf('log_unlock_failed', output=msg), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_error_title'), + self.tf('msg_unlock_failed', output=msg) + ) + + threading.Thread(target=worker, daemon=True).start() + def start_device_monitor(self): """启动设备状态监控(每5秒检查一次)""" def monitor(): while True: try: - result = subprocess.run(f'{self.adb} -d devices', shell=True, capture_output=True, text=True) + 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] @@ -839,7 +1039,7 @@ class ADKAPKGUI: self.log(f"CMD: adb shell {shell_command}", "CMD") try: proc = subprocess.Popen( - f'{self.adb} -d shell {shell_command}', + f'{self._adb_cmd()} -d shell {shell_command}', shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, @@ -882,7 +1082,7 @@ class ADKAPKGUI: def run_adb_command(self, command): """执行原始 adb 命令(adb push / adb install 等,无需 shell 密码)。 静默执行,不显示 adb 原始输出,仅返回结果。""" - command = command.replace('adb', self.adb, 1) + command = command.replace('adb', self._adb_cmd(), 1) if self.debug_mode: self.log(f"CMD: {command}", "CMD") try: @@ -948,44 +1148,50 @@ class ADKAPKGUI: return f"解压失败: {err_msg.strip()[:300]}" return f"解压失败 (返回码 {return_code}),请检查密码是否正确" - def _decode_7z_output(self, *outputs): - """解码 7za 输出,兼容中文 Windows 控制台编码。""" - parts = [] - for output in outputs: - if not output: + def _decode_7z_output(self, output): + for enc in ('gbk', 'utf-8'): + try: + return output.decode(enc) + except UnicodeDecodeError: continue - for enc in ('gbk', 'utf-8'): - try: - parts.append(output.decode(enc, errors='replace')) - break - except Exception: - continue - return ''.join(parts).strip() + return output.decode('utf-8', errors='replace') - def _extract_7za_with_progress(self): - """流式运行 7za 并解析百分比输出。""" - cmd = [self.sz, 'x', str(self.package_file), f'-p{self.extract_password}', f'-o{self.temp_dir}', '-y'] - supports_progress = getattr(self, '_seven_zip_supports_progress_stream', lambda: False)() - if supports_progress: + def _seven_zip_supports_progress_stream(self): + try: + result = subprocess.run( + [self.sz], + capture_output=True, + text=True, + errors='ignore', + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + return '-bs{o|e|p}' in (result.stdout + result.stderr) + except Exception: + return False + + def _extract_with_7za_progress(self): + self.update_progress(0, 100, "Loading resources...") + cmd = [ + self.sz, 'x', str(self.package_file), + f'-p{self.extract_password}', + f'-o{self.temp_dir}', '-y' + ] + if self._seven_zip_supports_progress_stream(): cmd.extend(['-bsp1', '-bso0', '-bse1']) - if getattr(self, 'debug_mode', False): - self.log(f"7ZA CMD: {subprocess.list2cmdline(cmd)}", "CMD") - proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, - creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0, + bufsize=0 ) output = bytearray() - progress_window = bytearray() last_percent = -1 - while True: - chunk = proc.stdout.read(1) if proc.stdout else b"" + chunk = proc.stdout.read(1) if proc.stdout else b'' if not chunk: if proc.poll() is not None: break @@ -993,91 +1199,64 @@ class ADKAPKGUI: continue output.extend(chunk) - progress_window.extend(chunk) - if len(progress_window) > 1024: - del progress_window[:-1024] - if not self.debug_mode and len(output) > 60000: + if len(output) > 60000: del output[:-60000] - matches = re.findall(rb"(\d{1,3})%", bytes(progress_window[-512:])) + 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, "资源加载中") + self.update_progress(percent, 100, "Loading resources...") return_code = proc.wait() decoded_output = self._decode_7z_output(bytes(output)) - - if getattr(self, 'debug_mode', False): - self.log(f"7ZA RET: {return_code}", "CMD" if return_code == 0 else "ERROR") - if decoded_output.strip(): - self.log(f"7ZA OUTPUT:\n{decoded_output.strip()}", "CMD" if return_code == 0 else "ERROR") - - return return_code, decoded_output - - def _seven_zip_supports_progress_stream(self): - """检测当前 7za 是否支持进度流参数。""" - try: - result = subprocess.run( - [self.sz], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 - ) - output = self._decode_7z_output(result.stdout, result.stderr) - return '-bs{o|e|p}' in output - except Exception: - return False + if return_code == 0: + self.update_progress(100, 100, "Resources loaded") + return True, decoded_output + return False, decoded_output def extract_package_silent(self): - """静默解压语言包(带进度)—— 逸动版仅处理 app 目录""" + """Extract package.bin silently with progress; app directory only.""" if not self.package_file.exists(): - self.log(f"错误:未找到资源包 ({self.package_file})", "ERROR") + self.log(f"Error: package not found ({self.package_file})", "ERROR") return False if not self.extract_password: - self.log("错误:解压密码未设置", "ERROR") + self.log("Error: extract password is not set", "ERROR") return False if not os.path.exists(self.sz): - self.log(f"错误:未找到 7za.exe ({self.sz})", "ERROR") + self.log(f"Error: 7za.exe not found ({self.sz})", "ERROR") return False try: - # 使用用户目录,无需管理员权限 local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) hidden_path = Path(local_appdata) / ".cache" / "system" / ".android" hidden_path.mkdir(parents=True, exist_ok=True) - self.temp_dir = hidden_path / "apps_cache_Mazda_EZ60" + self.temp_dir = hidden_path / "apps_cache_CS75Pro" - # 如果已存在,先清理 if self.temp_dir.exists(): shutil.rmtree(self.temp_dir, ignore_errors=True) time.sleep(0.5) self.temp_dir.mkdir(parents=True, exist_ok=True) - # 设置隐藏属性(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") + self.log("正在准备资源包", "INFO") - self.update_progress(0, 100, "资源加载中") - return_code, err_msg = self._extract_7za_with_progress() - if return_code != 0: - self.log(self._format_extract_error(err_msg, return_code), "ERROR") + ok, err_msg = self._extract_with_7za_progress() + if not ok: + self.log(self._format_extract_error(err_msg, 1), "ERROR") self._clear_extracted_cache() return False - self.update_progress(100, 100, "资源加载完成") - # 查找 app 目录(逸动无 priv-app) self.apps_dir = None - - app_candidates = list(self.temp_dir.rglob("apps")) + app_candidates = list(self.temp_dir.rglob("apps")) or list(self.temp_dir.rglob("app")) if app_candidates: self.apps_dir = app_candidates[0] @@ -1097,7 +1276,7 @@ class ADKAPKGUI: except Exception as e: if getattr(self, 'debug_mode', False): - self.log(f"资源准备失败: {str(e)}", "ERROR") + self.log(f"Package preparation failed: {str(e)}", "ERROR") import traceback self.log(traceback.format_exc(), "ERROR") else: @@ -1112,7 +1291,7 @@ class ADKAPKGUI: # 刷新热点显示 self.refresh_hotspot_display() try: - result = subprocess.run(f'{self.adb} version', shell=True, capture_output=True, text=True) + 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(): @@ -1123,81 +1302,93 @@ class ADKAPKGUI: self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") def refresh_device_status(self, force=False): - """刷新设备状态 —— 逸动版使用 ca.car.vin 获取 VIN""" - # 防止并发刷新(手动点击「检查」时强制忽略锁) + """Refresh device status; Yidong uses ca.car.vin.""" if self._refreshing and not force: return self._refreshing = True def refresh(): - was_connected = self.device_connected + try: + 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] + result = subprocess.run(f'{self._adb_cmd()} -d devices', shell=True, capture_output=True, text=True) + lines = result.stdout.strip().splitlines() + devices = [line for line in lines[1:] if line.strip() and 'device' in line and 'offline' not in line] - if devices: - if not was_connected: - self.log("设备已连接", "SUCCESS") + if devices: + if not was_connected: + self.log("Device connected", "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 '' + success, vin_output = self.run_adb_shell('settings get system ca.car.vin') + vin = vin_output.strip() if success else '' + if vin == 'null': + vin = '' - if vin: - self.log(f"VIN: {vin}", "INFO") - authorized = self.check_authorization(vin) - self.update_device_status(True, vin, authorized) + if vin: + self.log(f"VIN: {vin}", "INFO") + authorized = self.check_authorization(vin) + self.update_device_status(True, vin, authorized) + else: + self.log("Unable to read VIN, please confirm factory mode", "WARNING") + self.update_device_status(True, None, False) else: - 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 + if was_connected: + self.log("Device disconnected", "WARNING") + self.update_device_status(False) + except Exception as e: + self.log(f"Refresh device status failed: {str(e)}", "ERROR") + finally: + self._refreshing = False threading.Thread(target=refresh, daemon=True).start() def check_authorization(self, vin): - """检查授权""" - if self.debug_mode: - self.log("调试模式: 跳过授权验证", "WARNING") + """Check authorization.""" + if getattr(self, 'debug_mode', False): + self.log("Debug mode: skip authorization", "WARNING") return True - self.log("正在验证授权状态...", "INFO") - + self.log("Checking authorization...", "INFO") try: - url = f"{self.api_url}?vin={vin}" - req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + authorized, vehicle_name, data = self.query_authorization_info(vin) - 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") + if authorized: + self.log("Authorization passed", "SUCCESS") + if vehicle_name: + self.log(f"Vehicle name: {vehicle_name}", "INFO") return True else: - self.log(f"❌ 授权验证失败", "ERROR") + self.log("Authorization failed", "ERROR") return False - except Exception as e: - self.log(f"❌ 授权验证失败", "ERROR") + except Exception: + self.log("Authorization failed", "ERROR") return False + def query_authorization_info(self, vin): + url = f"{self.api_url}?{urlencode({'vin': vin})}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + payload = data.get('data', {}) if isinstance(data, dict) else {} + vehicle_name = payload.get('vehicleName') or payload.get('vehicle_name') or "" + vehicle_name = str(vehicle_name).strip() + return data.get('authorized') is True, vehicle_name, data + def fetch_package_password(self): - """从服务端获取资源包解压密码""" + """Fetch package password from server.""" if not self.vin: - self.log("请先连接adb!", "ERROR") + self.log("Please connect adb first", "ERROR") return False try: + # CS75Pro package-key vehicleName is fixed by requirement. + # It must not be sourced from auth-check, even if auth-check returns data.vehicleName. + vehicle_name = self.PACKAGE_KEY_VEHICLE_NAME + pwd_api_url = "https://api.changan.softwindy.cn/api/authorizations/package-key" - url = f"{pwd_api_url}?vin={self.vin}" + url = f"{pwd_api_url}?{urlencode({'vin': self.vin, 'vehicleName': vehicle_name})}" req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) with urlopen(req, timeout=10) as response: @@ -1207,11 +1398,11 @@ class ADKAPKGUI: self.extract_password = data['data']['password'] return True else: - self.log(f"数据准备失败: {data.get('message', '未知错误')}", "ERROR") + self.log(f"Data preparation failed: {data.get('message', 'unknown error')}", "ERROR") return False except Exception as e: - self.log(f"数据准备失败: {str(e)}", "ERROR") + self.log(f"Data preparation failed: {str(e)}", "ERROR") return False def push_single_apk(self, apk_path, apk_name): @@ -1222,7 +1413,7 @@ class ADKAPKGUI: if not ok: return False, f"push失败: {err}" - ok, err = self.run_adb_shell(f'pm install -r -d {temp_apk_path}') + ok, err = self.run_adb_shell(f'pm install -d -f -r {temp_apk_path}') self.run_adb_shell(f'rm -f {temp_apk_path}') if not ok: return False, f"install失败: {err}" @@ -1234,34 +1425,42 @@ class ADKAPKGUI: ok, _ = self.push_single_apk(apk_path, apk_name) return ok - def run_mazda_post_install_tasks(self): - """语言包安装完成后启用 Mazda overlay 并禁用指定应用。""" - self.log("正在执行 Mazda-EZ60 安装后配置...", "INFO") + def cleanup_preinstalled_apps_for_language(self): + """Disable and uninstall built-in apps before flashing language packages.""" + packages = [ + "com.wtcl.electronicdirections", + "com.tinnove.netease.music", + "com.incall.apps.softmanager", + "com.tencent.qqlive.audiobox", + ] + failed = [] - overlay_ok = True - for package_name in self.mazda_overlay_packages: - ok, output = self.run_adb_shell(f'cmd overlay enable {package_name}') - if ok: - self.log(f"已启用 overlay: {package_name}", "SUCCESS") + for package in packages: + disable_ok, disable_output = self.run_adb_shell(f'pm disable-user {package}') + uninstall_ok, uninstall_output = self.run_adb_shell(f'pm uninstall -k --user 0 {package}') + + if self.debug_mode: + if disable_ok: + self.log(f"禁用完成: {package}", "CMD") + else: + self.log(f"禁用失败: {package} {disable_output}", "CMD") + if uninstall_ok: + self.log(f"卸载完成: {package}", "CMD") + else: + self.log(f"卸载失败: {package} {uninstall_output}", "CMD") + + if not disable_ok or not uninstall_ok: + failed.append(package) + + if failed: + if self.debug_mode: + self.log("预置应用清理部分失败: " + ", ".join(failed), "WARNING") else: - overlay_ok = False - self.log(f"启用 overlay 失败 {package_name}: {output}", "ERROR") + self.log("预置应用清理部分失败,继续刷入语言包", "WARNING") + return False - disabled_count = 0 - for package_name in self.mazda_disable_packages: - ok, output = self.run_adb_shell(f'pm disable-user {package_name}') - if ok: - disabled_count += 1 - self.log(f"已禁用: {package_name}", "SUCCESS") - else: - self.log(f"禁用失败 {package_name}: {output}", "ERROR") - - self.log( - f"Mazda-EZ60 安装后配置完成:启用 {len(self.mazda_overlay_packages)} 个 overlay," - f"禁用 {disabled_count}/{len(self.mazda_disable_packages)} 个应用", - "INFO", - ) - return overlay_ok and disabled_count == len(self.mazda_disable_packages) + self.log("预置应用清理完成", "SUCCESS") + return True def push_all_apks(self): """推送APK并安装 —— 逸动版仅处理 app 目录,使用 pm install""" @@ -1270,19 +1469,23 @@ class ADKAPKGUI: return if not self.vin: - messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码") + messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_need_vin')) return def do_push_all(): # 验证授权 if not self.check_authorization(self.vin): - self.run_on_ui_thread(lambda: messagebox.showerror("授权失败", "设备未授权")) + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized')) + ) return # 获取解压密码 if not self.extract_password: if not self.fetch_package_password(): - self.run_on_ui_thread(lambda: messagebox.showerror("错误", "数据准备失败!")) + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_data_prepare_failed')) + ) return # 解压 @@ -1291,12 +1494,16 @@ class ADKAPKGUI: self.show_progress(True, is_push=False) if not self.extract_package_silent(): self.show_progress(False, is_push=False) - self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源准备失败!")) + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_prepare_failed')) + ) return self.show_progress(False, is_push=False) if not self.apps_dir or not self.apps_dir.exists(): - self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源目录未找到")) + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_dir_missing')) + ) return # 开始刷入 @@ -1304,41 +1511,38 @@ class ADKAPKGUI: self.log("开始刷入语言包...", "INFO") self.run_adb_shell('mkdir -p /data/local/tmp') self.run_adb_shell('setprop vecentek.model 1') + self.cleanup_preinstalled_apps_for_language() - 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 + try: + all_apks = list(self.apps_dir.glob("*.apk")) + if not all_apks: + self.log("未找到语言包文件", "WARNING") + return + + total = len(all_apks) + for i, apk_path in enumerate(all_apks, 1): + apk_name = apk_path.stem + ok, _ = self.push_single_apk(apk_path, apk_name) + if ok: + if self.debug_mode: + self.log(f"安装成功: {apk_name}.apk", "SUCCESS") + success_count += 1 + else: + if self.debug_mode: + self.log(f"安装失败: {apk_name}.apk", "ERROR") + else: + self.log(f"语言包刷入失败: {i}/{total}", "ERROR") + self.update_progress(i, total, "正在刷入", is_push=True) + + self.update_progress(total, total, "刷入完成", is_push=True) + + if success_count > 0: + self.log("语言包刷入完成,重启设备后生效", "SUCCESS") else: - self.log(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 == total: - self.log("语言包刷入完成,开始执行 Mazda-EZ60 安装后配置", "SUCCESS") - elif success_count > 0: - self.log("语言包部分刷入成功,仍继续执行 Mazda-EZ60 安装后配置", "WARNING") - else: - self.log("语言包刷入失败,仍继续执行 Mazda-EZ60 安装后配置", "WARNING") - - if self.run_mazda_post_install_tasks(): - self.log("Mazda-EZ60 安装后配置全部完成,重启设备后生效", "SUCCESS") - else: - self.log("Mazda-EZ60 安装后配置部分失败,请查看日志", "WARNING") - - self.show_progress(False, is_push=True) + self.log("语言包刷入失败", "ERROR") + finally: + self.show_progress(False, is_push=True) threading.Thread(target=do_push_all, daemon=True).start() @@ -1382,30 +1586,33 @@ class ADKAPKGUI: 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 + try: + self.run_adb_shell('setprop vecentek.model 1') + + for i, apk_path in enumerate(apk_files, 1): + apk_name = apk_path.stem + self.update_progress(i, total, "安装中...", is_push=True) + if self._push_and_install(apk_path, apk_name): + self.log(f"安装成功: {apk_name}.apk", "SUCCESS") + success_count += 1 + else: + self.log(f"安装失败: {apk_name}.apk", "ERROR") + + self.update_progress(total, total, "安装完成", is_push=True) + + if success_count == total: + self.run_on_ui_thread(messagebox.showinfo, "安装完成", f"成功安装 {total} 个APK!") + elif success_count > 0: + self.run_on_ui_thread(messagebox.showwarning, "部分成功", f"成功: {success_count}\n失败: {total - success_count}") else: - self.log(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安装失败!") + self.log("安装失败", "ERROR") + self.run_on_ui_thread(messagebox.showerror, "安装失败", "所有APK安装失败!") + except Exception as e: + self.log(f"安装过程异常: {str(e)}", "ERROR") + self.run_on_ui_thread(messagebox.showerror, "安装失败", f"安装过程异常:{str(e)}") + finally: + self.show_progress(False, is_push=True) threading.Thread(target=install, daemon=True).start() @@ -1433,16 +1640,18 @@ class ADKAPKGUI: 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) + try: + self.run_adb_shell('setprop vecentek.model 1') + success = self._push_and_install(file_path, apk_name) + self.update_progress(100, 100, "完成", is_push=True) + if success: + self.log("安装成功", "SUCCESS") + else: + self.log("安装失败", "ERROR") + except Exception as e: + self.log(f"安装过程异常: {str(e)}", "ERROR") + finally: + self.show_progress(False, is_push=True) threading.Thread(target=install, daemon=True).start() @@ -1460,7 +1669,7 @@ class ADKAPKGUI: # 创建弹窗 popup = tk.Toplevel(self.root) - popup.title("快捷语言设置") + popup.title(self.t('quick_lang_title')) popup.geometry("520x320") popup.configure(bg=self.colors['bg_dark']) popup.resizable(False, False) @@ -1474,29 +1683,21 @@ class ADKAPKGUI: popup.grab_set() # 标题 - header = tk.Label(popup, text="选择目标语言", + header = tk.Label(popup, text=self.t('quick_lang_header'), font=('Microsoft YaHei', 13, 'bold'), fg=self.colors['accent'], bg=self.colors['bg_dark']) header.pack(pady=(15, 10)) - hint = tk.Label(popup, text="点击按钮即可将系统语言切换为对应语言,重启后生效", + hint = tk.Label(popup, text=self.t('quick_lang_hint'), font=('Microsoft YaHei', 9), fg=self.colors['text_secondary'], bg=self.colors['bg_dark']) hint.pack(pady=(0, 12)) # 语言列表:(显示名, locale_code) - 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"), - ] + locale_codes = ["zh-CN", "en-US", "ru-RU", "fr-FR", "es-ES", "pt-BR", "it-IT", "ar-SA"] + languages = list(zip(self.t('quick_lang_names'), locale_codes)) # 创建按钮容器 btn_frame = tk.Frame(popup, bg=self.colors['bg_dark']) @@ -1530,7 +1731,7 @@ class ADKAPKGUI: 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="⚙️ 打开系统语言设置(手动选择)", + sys_btn = tk.Button(popup, text=self.t('quick_lang_system'), command=lambda: self._open_sys_and_close(popup), font=('Microsoft YaHei', 9), fg=self.colors['text_secondary'], @@ -1544,20 +1745,24 @@ class ADKAPKGUI: 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⚠️ 请重启设备使其生效。" + self.log(self.tf('log_quick_lang_success', language=language_name), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('quick_lang_success_title'), + self.tf('quick_lang_success', language=language_name) ) else: - self.log(f"✗ 语言设置失败: {output}", "ERROR") - messagebox.showerror("设置失败", f"语言设置失败!\n\n{output}") + self.log(self.tf('log_quick_lang_failed', output=output), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('quick_lang_failed_title'), + self.tf('quick_lang_failed', output=output) + ) threading.Thread(target=do_set, daemon=True).start() @@ -1583,7 +1788,14 @@ class ADKAPKGUI: if not self.check_device_connection(): return if messagebox.askyesno("确认重启", "确定要重启设备吗?"): - self.run_adb_shell('reboot') + proc = subprocess.Popen(f'{self._adb_cmd()} -d shell reboot', shell=True, + stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + try: + proc.stdin.write(b'adb36987\n') + proc.stdin.flush() + proc.stdin.close() + except: + pass self.log("设备正在重启...", "INFO") self.update_device_status(False) @@ -1613,10 +1825,10 @@ class ADKAPKGUI: if success: self.log("系统升级已禁用", "SUCCESS") - messagebox.showinfo("成功", "系统升级已成功禁用!") + self.run_on_ui_thread(messagebox.showinfo, "成功", "系统升级已成功禁用!") else: self.log("禁用系统升级失败", "ERROR") - messagebox.showerror("错误", f"禁用失败:{output}") + self.run_on_ui_thread(messagebox.showerror, "错误", f"禁用失败:{output}") self.show_progress(False, is_push=False) @@ -1624,27 +1836,27 @@ class ADKAPKGUI: def _on_vin_input_focus_in(self, event): """输入框获得焦点时清除占位符""" - if self.vin_input.get() == "请输入VIN": + if self.is_placeholder_vin(self.vin_input.get()): self.vin_input.delete(0, tk.END) self.vin_input.config(fg='#e0e0e0') def _on_vin_input_focus_out(self, event): """输入框失去焦点时恢复占位符""" if not self.vin_input.get(): - self.vin_input.insert(0, "请输入VIN") + self.vin_input.insert(0, self.t('vin_placeholder')) self.vin_input.config(fg='#636e72') def query_password_by_vin(self): """通过VIN查询密码""" vin = self.vin_input.get().strip() - if not vin: - messagebox.showwarning("提示", "请输入VIN码") + if not vin or self.is_placeholder_vin(vin): + messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_input_vin')) return def do_query(): try: api_url = "https://api.changan.softwindy.cn/api/authorizations/generate-password-by-vin" - url = f"{api_url}?vin={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: @@ -1722,31 +1934,35 @@ class ADKAPKGUI: 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 + try: + self.run_adb_shell('setprop vecentek.model 1') + + for i, file_path in enumerate(file_paths, 1): + apk_name = Path(file_path).stem + self.update_progress(i, count, f"安装中 ({apk_name})", is_push=True) + if self._push_and_install(file_path, apk_name): + self.log(f"✓ {apk_name}.apk", "SUCCESS") + success_count += 1 + else: + self.log(f"✗ {apk_name}.apk", "ERROR") + + self.update_progress(count, count, "安装完成", is_push=True) + + if success_count == count: + self.log(f"安装完成:全部 {count} 个成功", "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, "安装完成", f"成功安装 {count} 个APK!") + elif success_count > 0: + self.log(f"安装完成:{success_count}/{count} 成功", "WARNING") + self.run_on_ui_thread(messagebox.showwarning, "部分成功", f"成功: {success_count}\n失败: {count - success_count}") else: - self.log(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安装失败!") + self.log("安装失败", "ERROR") + self.run_on_ui_thread(messagebox.showerror, "安装失败", "所有APK安装失败!") + except Exception as e: + self.log(f"安装过程异常: {str(e)}", "ERROR") + self.run_on_ui_thread(messagebox.showerror, "安装失败", f"安装过程异常:{str(e)}") + finally: + self.show_progress(False, is_push=True) threading.Thread(target=install, daemon=True).start() @@ -1757,20 +1973,48 @@ class ADKAPKGUI: def modify_hosts(self): """修改hosts文件,添加云端认证DNS映射""" hosts_path = r"C:\Windows\System32\drivers\etc\hosts" - entry = "103.236.55.140 spm.auto-pai.com" + host_ip = "103.236.55.140" + host_name = "spm.auto-pai.com" + entry = f"{host_ip} {host_name}" try: - with open(hosts_path, 'r', encoding='utf-8') as f: - content = f.read() + try: + with open(hosts_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + except UnicodeDecodeError: + with open(hosts_path, 'r', encoding='gbk', errors='replace') as f: + lines = f.readlines() - if entry in content: - # self.log("hosts条目已存在,无需修改", "INFO") + new_lines = [] + changed = False + has_exact = False + for line in lines: + stripped = line.strip() + if not stripped or stripped.startswith('#'): + new_lines.append(line) + continue + + body, _, _ = line.partition('#') + parts = body.split() + if len(parts) >= 2 and host_name.lower() in [p.lower() for p in parts[1:]]: + if parts[0] == host_ip and len(parts) == 2: + has_exact = True + new_lines.append(line) + else: + changed = True + continue + + new_lines.append(line) + + if has_exact and not changed: return True - with open(hosts_path, 'a', encoding='utf-8') as f: - f.write(f"\n{entry}\n") + if not new_lines or (new_lines[-1] and not new_lines[-1].endswith(('\n', '\r'))): + new_lines.append('\n') + new_lines.append(f"{entry}\n") - # self.log(f"已添加hosts条目: {entry}", "SUCCESS") + with open(hosts_path, 'w', encoding='utf-8', newline='') as f: + f.writelines(new_lines) return True except PermissionError: self.log("需要管理员权限,请以管理员身份运行", "WARNING") @@ -1922,18 +2166,49 @@ class ADKAPKGUI: def _refresh_hotspot_display_impl(self, ssid, password, status): """刷新热点显示的UI实现""" if ssid: - self.hotspot_ssid_label.config(text=f"名称: {ssid}") + self.hotspot_ssid_label.config(text=self.tf('hotspot_name_value', ssid=ssid)) else: - self.hotspot_ssid_label.config(text="名称: 未配置") + self.hotspot_ssid_label.config(text=self.t('hotspot_name_empty')) if password: - self.hotspot_pwd_label.config(text=f"密码: {password}") + self.hotspot_pwd_label.config(text=self.tf('hotspot_pwd_value', password=password)) else: - self.hotspot_pwd_label.config(text="密码: changan2024") + self.hotspot_pwd_label.config(text=self.t('hotspot_pwd_default')) self.hotspot_status_label.config( - text=f"状态: {status}", + text=self.tf('hotspot_status_value', status=status), fg=self.colors['success'] if '已启动' in status else self.colors['warning'] ) + def _debug_test_extract(self, event=None): + """Debug-only package extraction test.""" + if not getattr(self, 'debug_mode', False): + messagebox.showwarning("Debug mode", "Press Ctrl+Shift+D to enable debug mode first") + return + + pwd = simpledialog.askstring("Test extraction", "Enter package.bin password:", show='*', parent=self.root) + if not pwd: + return + + def do_extract(): + old_password = self.extract_password + self.extract_password = pwd + try: + self.show_progress(True, is_push=False) + if self.extract_package_silent(): + self.log("Test extraction succeeded", "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + "Test extraction succeeded", + f"Resources extracted to:\n{self.temp_dir}" + ) + else: + self.log("Test extraction failed", "ERROR") + self.run_on_ui_thread(messagebox.showerror, "Test extraction failed", "Check the 7za output in logs") + finally: + self.extract_password = old_password + self.show_progress(False, is_push=False) + + threading.Thread(target=do_extract, daemon=True).start() + def run(self): """运行程序""" self.root.mainloop() diff --git a/CS75Pro/cs75pro.ico b/CS75Pro/cs75pro.ico new file mode 100644 index 0000000..ad7d089 Binary files /dev/null and b/CS75Pro/cs75pro.ico differ diff --git a/CS75Pro/pack_cs75pro.bat b/CS75Pro/pack_cs75pro.bat new file mode 100644 index 0000000..8b1f258 --- /dev/null +++ b/CS75Pro/pack_cs75pro.bat @@ -0,0 +1,133 @@ +@echo off +chcp 65001 >nul +cd /d "%~dp0" +set "ROOT=%~dp0.." +set "TOOLS=%ROOT%\tools" +set "NAME=CS75Pro-Installer" +set "SRC=CS75Pro_Installer.py" +set "ICON=cs75pro.ico" +title %NAME% - Cython Build + +echo ============================================================ +echo %NAME% - Cython Build +echo ============================================================ +echo. + +where python >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Python not found + pause + exit /b 1 +) +for /f "delims=" %%i in ('where python') do set "PY=%%i" +echo Python: %PY% + +if not exist "%SRC%" ( + echo [ERROR] Source not found: %SRC% + pause + exit /b 1 +) +if not exist "%ICON%" ( + echo [ERROR] Icon not found: %ICON% + pause + exit /b 1 +) +if not exist "%TOOLS%\adb.exe" ( + echo [ERROR] adb.exe not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\7za.exe" ( + echo [ERROR] 7za.exe not found in %TOOLS% + pause + exit /b 1 +) + +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 +) +if errorlevel 1 ( + echo [ERROR] Dependency install failed + pause + exit /b 1 +) + +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 +if exist "%NAME%.spec" del /q "%NAME%.spec" 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',encoding='utf-8').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 [ERROR] Cython failed. Build stopped. + pause + exit /b 1 +) + +set "PYD=" +for %%f in (_core*.pyd) do set "PYD=%%f" +if "%PYD%"=="" ( + cd .. + echo [ERROR] No Cython PYD generated. Build stopped. + pause + exit /b 1 +) +echo PYD: %PYD% +copy "%PYD%" _core.pyd >nul +if errorlevel 1 ( + cd .. + echo [ERROR] Failed to copy Cython PYD + pause + exit /b 1 +) + +%PY% -c "open('launcher.py','w',encoding='utf-8').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')" + +echo [4/6] Copy resources... +copy "%TOOLS%\adb.exe" . >nul +copy "%TOOLS%\AdbWinApi.dll" . >nul +copy "%TOOLS%\AdbWinUsbApi.dll" . >nul +copy "%TOOLS%\7za.exe" . >nul +copy "..\%ICON%" . >nul +if errorlevel 1 ( + cd .. + echo [ERROR] Copy resources failed + pause + exit /b 1 +) + +echo [5/6] PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ICON%" --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --add-data "%ICON%;." --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 1 +) + +echo [6/6] Cleanup... +del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py "%ICON%" 2>nul +rmdir /s /q build 2>nul +cd .. + +echo. +echo Done. +if exist "dist_cy\dist\%NAME%.exe" ( + echo Output: dist_cy\dist\%NAME%.exe +) else ( + echo [ERROR] Output exe was not generated + pause + exit /b 1 +) +pause diff --git a/Mazda-EZ6/Mazda-EZ6_Installer.py b/Mazda-EZ6/Mazda-EZ6_Installer.py new file mode 100644 index 0000000..ca7b3a7 --- /dev/null +++ b/Mazda-EZ6/Mazda-EZ6_Installer.py @@ -0,0 +1,1398 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import subprocess +import sys +import threading +import time +import tkinter as tk +from tkinter import filedialog, messagebox, scrolledtext, ttk +from datetime import datetime +from pathlib import Path +from urllib.error import HTTPError, URLError +from urllib.parse import urlencode +from urllib.request import Request, urlopen +import json + + +def set_windows_app_id(): + """Set taskbar grouping id on Windows.""" + if sys.platform != "win32": + return + try: + import ctypes + app_id = "YibinKeyi.MazdaEZ6.Installer.1.0" + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id) + except Exception: + pass + + +def get_app_dir(): + if getattr(sys, "frozen", False): + return Path(sys.executable).resolve().parent + return Path(__file__).resolve().parent + + +def resource_candidates(file_name): + base_dir = get_app_dir() + candidates = [] + if getattr(sys, "frozen", False): + candidates.append(Path(getattr(sys, "_MEIPASS", base_dir)) / file_name) + candidates.extend([ + base_dir / file_name, + base_dir / "tools" / file_name, + base_dir.parent / "tools" / file_name, + base_dir.parent / file_name, + ]) + unique = [] + for candidate in candidates: + if candidate not in unique: + unique.append(candidate) + return unique + + +def find_resource(file_name): + candidates = resource_candidates(file_name) + for candidate in candidates: + if candidate.exists(): + return candidate + return candidates[0] + + +def find_adb(): + candidates = [ + Path(r"D:\UserData\adb-fastboot\adb.exe"), + ] + candidates.extend(resource_candidates("adb.exe")) + for candidate in candidates: + if candidate.exists(): + return str(candidate) + return "adb" + + +class ADKAPKGUI: + def __init__(self): + set_windows_app_id() + self.lang = "zh" + self.T = { + "zh": { + "title": "Mazda-EZ6 安装工具", + "lang_switch": "EN", + "status_ready": "就绪", + "device_checking": "设备状态: 检测中", + "device_connected": "设备状态: 设备已连接", + "device_disconnected": "设备状态: 未连接设备", + "vin_placeholder": "输入VIN或VIN后六位查询密码", + "password_unknown": "未查询", + "password_querying": "查询中...", + "password_failed": "查询失败", + "btn_query_password": "获取密码", + "password_status": "密码状态:", + "tips_title": "操作提示", + "tips_text": ( + "1. 连续点击右上角时间,输入查询到的密码进入工厂模式。\n" + "2. 点击“USB模式切换”,点击“CURRENT USB MODE: HOST”,将设备切换为“DEVICES”。\n" + "3. 使用USB双公线,连接车机和电脑。\n" + "4. 点击“设置语言”,选择需要设置的目标语言。\n" + "5. 点击获取安装权限,重启后再次打开“DEVICES”模式,程序连接上设备以后安装App。" + ), + "function_area": "功能区", + "btn_permission": "获取安装权限", + "btn_install_app": "安装App", + "btn_set_language": "设置语言", + "btn_timezone": "时区选择", + "btn_settings": "安卓设置", + "btn_reboot": "重启设备", + "log_title": "运行日志", + "btn_clear_log": "清空日志", + "log_started": "Mazda-EZ6 安装工具界面已启动。", + "log_cleared": "日志已清空。", + "warn_title": "提示", + "error_title": "错误", + "success_title": "成功", + "input_vin": "请输入VIN或VIN后六位。", + "status_password_querying": "正在查询密码", + "log_password_querying": "正在查询工程密码...", + "status_password_success": "密码查询成功", + "log_password_success": "密码查询成功: {password}", + "password_query_failed": "密码查询失败", + "password_http_failed": "密码查询失败: HTTP {code}", + "password_network_failed": "密码查询失败,请检查网络连接后重试。", + "password_retry_failed": "密码查询失败,请稍后重试。", + "progress_get_permission": "正在获取安装权限...", + "status_get_permission": "正在获取安装权限", + "log_get_permission": "正在获取安装权限,请保持设备连接。", + "progress_detect_device": "正在检测设备...", + "progress_enter_device": "正在进入设备环境...", + "progress_config_env": "正在配置安装环境...", + "progress_confirm_permission": "正在确认权限状态...", + "permission_ready": "安装权限已获取", + "permission_success_log": "获取安装权限成功。", + "permission_failed": "安装权限获取失败", + "permission_failed_log": "获取安装权限失败,请确认设备连接后重试。", + "status_check_permission": "正在检查安装权限", + "progress_check_permission": "正在检查安装权限...", + "need_permission_status": "请先获取安装权限", + "need_permission_log": "当前设备尚未获取安装权限。", + "need_permission_msg": "请先获取安装权限。", + "file_select_apk_title": "选择APK文件", + "filetype_apk": "APK文件", + "filetype_all": "所有文件", + "install_cancelled_status": "已取消安装", + "install_cancelled_log": "已取消安装App。", + "install_prepare": "准备安装 {count} 个APK...", + "status_installing_app": "正在安装App", + "log_install_start": "开始安装 {count} 个APK。", + "installing_item": "正在安装 {name} ({index}/{total})", + "installing_status": "正在安装 {name}", + "apk_install_success": "{name} 安装成功。", + "apk_install_failed": "{name} 安装失败。", + "install_done": "安装完成", + "install_done_log": "安装完成:全部 {count} 个成功。", + "install_done_msg": "成功安装 {count} 个APK。", + "install_partial": "部分安装完成", + "install_partial_log": "安装完成:成功 {success} 个,失败 {failed} 个。", + "install_partial_title": "部分成功", + "install_partial_msg": "成功: {success}\n失败: {failed}", + "install_failed": "安装失败", + "install_failed_log": "安装失败:所有APK均未安装成功。", + "install_failed_msg": "所有APK安装失败。", + "language_title": "设置语言", + "language_header": "选择目标语言", + "language_hint": "设置完成后请重启设备使语言生效", + "language_names": ["中文", "English", "Русский", "Français", "Español", "ไทย", "Italiano", "Português"], + "progress_setting_language": "正在设置语言: {language}", + "status_setting_language": "正在设置语言", + "log_setting_language": "正在设置系统语言为: {language}", + "language_done": "语言设置完成", + "language_success_title": "设置成功", + "language_success_log": "系统语言已设置为 {language},请重启设备使其生效。", + "language_success_msg": "系统语言已设置为 {language}\n\n请重启设备使其生效。", + "language_failed": "语言设置失败", + "language_failed_msg": "语言设置失败,请确认设备连接后重试。", + "reboot_confirm_title": "确认重启", + "reboot_confirm_msg": "确定要重启设备吗?", + "reboot_cancelled": "已取消重启设备。", + "status_rebooting": "正在重启设备", + "log_rebooting": "正在重启设备...", + "status_rebooting_now": "设备正在重启", + "reboot_success_log": "设备正在重启。", + "reboot_failed": "重启失败", + "reboot_failed_log": "重启失败,请确认设备连接。", + "timezone_label": "时区设置", + "settings_label": "安卓设置", + "status_opening": "正在打开{label}", + "log_opening": "正在打开设备{label}...", + "status_opened": "已打开{label}", + "log_opened": "{label}已打开。", + "timezone_fallback_log": "已打开安卓设置,请手动进入日期和时间/时区设置。", + "open_failed": "{label}打开失败,请确认设备连接。", + "log_lang_switched": "界面语言已切换为中文。", + }, + "en": { + "title": "Mazda-EZ6 Installer", + "lang_switch": "中", + "status_ready": "Ready", + "device_checking": "Device: checking", + "device_connected": "Device: connected", + "device_disconnected": "Device: disconnected", + "vin_placeholder": "Enter VIN or last 6 digits to query password", + "password_unknown": "Not queried", + "password_querying": "Querying...", + "password_failed": "Failed", + "btn_query_password": "Get Password", + "password_status": "Password:", + "tips_title": "Tips", + "tips_text": ( + "1. Tap the top-right clock repeatedly, then enter the queried password to enter factory mode.\n" + "2. Tap \"USB mode switch\", then tap \"CURRENT USB MODE: HOST\" and switch the device to \"DEVICES\".\n" + "3. Connect the head unit and PC with a USB-A to USB-A cable.\n" + "4. Tap \"Set Language\" and choose the target language.\n" + "5. Tap \"Get Install Permission\". After reboot, open \"DEVICES\" mode again; once connected, install apps." + ), + "function_area": "Actions", + "btn_permission": "Get Install Permission", + "btn_install_app": "Install App", + "btn_set_language": "Set Language", + "btn_timezone": "Timezone", + "btn_settings": "Android Settings", + "btn_reboot": "Reboot Device", + "log_title": "Log", + "btn_clear_log": "Clear Log", + "log_started": "Mazda-EZ6 Installer started.", + "log_cleared": "Log cleared.", + "warn_title": "Hint", + "error_title": "Error", + "success_title": "Success", + "input_vin": "Enter the VIN or the last 6 digits.", + "status_password_querying": "Querying password", + "log_password_querying": "Querying factory password...", + "status_password_success": "Password query succeeded", + "log_password_success": "Password query succeeded: {password}", + "password_query_failed": "Password query failed", + "password_http_failed": "Password query failed: HTTP {code}", + "password_network_failed": "Password query failed. Check the network and try again.", + "password_retry_failed": "Password query failed. Try again later.", + "progress_get_permission": "Getting install permission...", + "status_get_permission": "Getting install permission", + "log_get_permission": "Getting install permission. Keep the device connected.", + "progress_detect_device": "Checking device...", + "progress_enter_device": "Entering device environment...", + "progress_config_env": "Configuring install environment...", + "progress_confirm_permission": "Confirming permission status...", + "permission_ready": "Install permission acquired", + "permission_success_log": "Install permission acquired.", + "permission_failed": "Install permission failed", + "permission_failed_log": "Failed to get install permission. Confirm device connection and try again.", + "status_check_permission": "Checking install permission", + "progress_check_permission": "Checking install permission...", + "need_permission_status": "Get install permission first", + "need_permission_log": "The device does not have install permission yet.", + "need_permission_msg": "Please get install permission first.", + "file_select_apk_title": "Select APK files", + "filetype_apk": "APK files", + "filetype_all": "All files", + "install_cancelled_status": "Install cancelled", + "install_cancelled_log": "App installation cancelled.", + "install_prepare": "Preparing to install {count} APK(s)...", + "status_installing_app": "Installing apps", + "log_install_start": "Starting installation for {count} APK(s).", + "installing_item": "Installing {name} ({index}/{total})", + "installing_status": "Installing {name}", + "apk_install_success": "{name} installed successfully.", + "apk_install_failed": "{name} install failed.", + "install_done": "Install complete", + "install_done_log": "Install complete: all {count} APK(s) succeeded.", + "install_done_msg": "Successfully installed {count} APK(s).", + "install_partial": "Partially complete", + "install_partial_log": "Install complete: {success} succeeded, {failed} failed.", + "install_partial_title": "Partially successful", + "install_partial_msg": "Succeeded: {success}\nFailed: {failed}", + "install_failed": "Install failed", + "install_failed_log": "Install failed: no APK was installed successfully.", + "install_failed_msg": "All APK installations failed.", + "language_title": "Set Language", + "language_header": "Choose Target Language", + "language_hint": "Reboot the device after setting the language", + "language_names": ["Chinese", "English", "Russian", "French", "Spanish", "Thai", "Italian", "Portuguese"], + "progress_setting_language": "Setting language: {language}", + "status_setting_language": "Setting language", + "log_setting_language": "Setting system language to: {language}", + "language_done": "Language set", + "language_success_title": "Set Successfully", + "language_success_log": "System language has been set to {language}. Reboot the device to apply it.", + "language_success_msg": "System language has been set to {language}.\n\nReboot the device to apply it.", + "language_failed": "Language setting failed", + "language_failed_msg": "Language setting failed. Confirm device connection and try again.", + "reboot_confirm_title": "Confirm Reboot", + "reboot_confirm_msg": "Reboot the device now?", + "reboot_cancelled": "Device reboot cancelled.", + "status_rebooting": "Rebooting device", + "log_rebooting": "Rebooting device...", + "status_rebooting_now": "Device is rebooting", + "reboot_success_log": "Device is rebooting.", + "reboot_failed": "Reboot failed", + "reboot_failed_log": "Reboot failed. Confirm device connection.", + "timezone_label": "timezone settings", + "settings_label": "Android settings", + "status_opening": "Opening {label}", + "log_opening": "Opening device {label}...", + "status_opened": "Opened {label}", + "log_opened": "{label} opened.", + "timezone_fallback_log": "Android settings opened. Open Date & time / Timezone manually.", + "open_failed": "Failed to open {label}. Confirm device connection.", + "log_lang_switched": "Interface language switched to English.", + }, + } + + self.root = tk.Tk() + self.root.title(self.t("title")) + self.set_window_icon() + self.root.geometry("860x820") + self.root.minsize(760, 760) + + self.colors = { + "bg": "#1f2430", + "panel": "#2b3242", + "panel_alt": "#343c4f", + "accent": "#2f80ed", + "accent_hover": "#2568c7", + "success": "#27ae60", + "warning": "#f2c94c", + "danger": "#eb5757", + "text": "#f2f2f2", + "text_muted": "#b8c0cc", + "border": "#465066", + } + + self.vin_var = tk.StringVar() + self.password_var = tk.StringVar(value=self.t("password_unknown")) + self.status_var = tk.StringVar(value=self.t("status_ready")) + self.device_status_var = tk.StringVar(value=self.t("device_checking")) + self.vin_placeholder = self.t("vin_placeholder") + self.password_api_url = "https://api.changan.softwindy.cn/api/authorizations/get-deepal-pwd" + self.adb = find_adb() + self.install_permission_ready = False + self.device_connected = False + self._device_monitor_running = False + + self.setup_styles() + self.setup_ui() + self.center_window() + self.start_device_monitor() + + self.log(self.t("log_started")) + + def t(self, key): + return self.T.get(self.lang, self.T["zh"]).get(key, self.T["zh"].get(key, key)) + + def tf(self, key, **kwargs): + return str(self.t(key)).format(**kwargs) + + def setup_styles(self): + style = ttk.Style() + style.theme_use("clam") + style.configure( + "Horizontal.TProgressbar", + troughcolor=self.colors["panel_alt"], + background=self.colors["accent"], + bordercolor=self.colors["panel_alt"], + lightcolor=self.colors["accent"], + darkcolor=self.colors["accent"], + ) + + def set_window_icon(self): + """Set the Tk runtime icon; PyInstaller --icon only sets the exe file icon.""" + try: + icon_path = find_resource("app.ico") + if not icon_path.exists(): + return + self.root.iconbitmap(str(icon_path)) + if sys.platform == "win32": + import ctypes + hwnd = self.root.winfo_id() + image = ctypes.windll.user32.LoadImageW( + None, str(icon_path), 1, 0, 0, 0x00000010 + ) + if image: + ctypes.windll.user32.SendMessageW(hwnd, 0x0080, 0, image) + ctypes.windll.user32.SendMessageW(hwnd, 0x0080, 1, image) + except Exception: + pass + + def setup_ui(self): + self.root.configure(bg=self.colors["bg"]) + + main_frame = tk.Frame(self.root, bg=self.colors["bg"]) + main_frame.pack(fill=tk.BOTH, expand=True, padx=14, pady=14) + + top_frame = tk.Frame(main_frame, bg=self.colors["bg"]) + top_frame.pack(fill=tk.X) + + header_frame = tk.Frame(top_frame, bg=self.colors["bg"]) + header_frame.pack(fill=tk.X, pady=(0, 8)) + + header_frame.columnconfigure(0, weight=1, uniform="header_side") + header_frame.columnconfigure(1, weight=0) + header_frame.columnconfigure(2, weight=1, uniform="header_side") + + self.status_label = tk.Label( + header_frame, + textvariable=self.status_var, + font=("Microsoft YaHei", 10), + fg=self.colors["warning"], + bg=self.colors["bg"], + anchor="w", + ) + self.status_label.grid(row=0, column=0, sticky="w") + + self.title_label = tk.Label( + header_frame, + text=self.t("title"), + font=("Microsoft YaHei", 20, "bold"), + fg=self.colors["text"], + bg=self.colors["bg"], + ) + self.title_label.grid(row=0, column=1, sticky="n") + + self.btn_lang_switch = self.create_button( + header_frame, + self.t("lang_switch"), + self.toggle_lang, + bg=self.colors["panel_alt"], + width=4, + height=1, + ) + self.btn_lang_switch.grid(row=0, column=2, sticky="e") + + device_status_frame = tk.Frame(top_frame, bg=self.colors["panel"], highlightthickness=1, + highlightbackground=self.colors["border"]) + device_status_frame.pack(fill=tk.X, pady=(0, 8)) + + self.device_status_dot = tk.Canvas( + device_status_frame, + width=12, + height=12, + bg=self.colors["panel"], + highlightthickness=0, + ) + self.device_status_dot.pack(side=tk.LEFT, padx=(12, 6), pady=8) + self.device_status_oval = self.device_status_dot.create_oval( + 2, + 2, + 10, + 10, + fill=self.colors["text_muted"], + outline="", + ) + + self.device_status_label = tk.Label( + device_status_frame, + textvariable=self.device_status_var, + font=("Microsoft YaHei", 10, "bold"), + fg=self.colors["text_muted"], + bg=self.colors["panel"], + anchor="w", + ) + self.device_status_label.pack(side=tk.LEFT, fill=tk.X, expand=True, pady=8) + + vin_panel = tk.Frame(top_frame, bg=self.colors["panel"], highlightthickness=1, + highlightbackground=self.colors["border"]) + vin_panel.pack(fill=tk.X, pady=(0, 8)) + + vin_row = tk.Frame(vin_panel, bg=self.colors["panel"]) + vin_row.pack(fill=tk.X, padx=12, pady=(10, 8)) + + tk.Label( + vin_row, + text="VIN:", + font=("Microsoft YaHei", 11, "bold"), + fg=self.colors["text"], + bg=self.colors["panel"], + ).pack(side=tk.LEFT, padx=(0, 8)) + + self.vin_entry = tk.Entry( + vin_row, + textvariable=self.vin_var, + font=("Consolas", 12), + fg=self.colors["text_muted"], + bg=self.colors["panel_alt"], + insertbackground=self.colors["text"], + relief=tk.FLAT, + ) + self.vin_var.set(self.vin_placeholder) + self.vin_entry.bind("", self._on_vin_focus_in) + self.vin_entry.bind("", self._on_vin_focus_out) + self.vin_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=7) + + self.btn_query_password = self.create_button( + vin_row, + self.t("btn_query_password"), + self.query_password, + bg=self.colors["accent"], + width=12, + ) + self.btn_query_password.pack(side=tk.LEFT, padx=(10, 0)) + + password_row = tk.Frame(vin_panel, bg=self.colors["panel"]) + password_row.pack(fill=tk.X, padx=12, pady=(0, 8)) + + self.password_status_label = tk.Label( + password_row, + text=self.t("password_status"), + font=("Microsoft YaHei", 10), + fg=self.colors["text_muted"], + bg=self.colors["panel"], + ) + self.password_status_label.pack(side=tk.LEFT) + + tk.Label( + password_row, + textvariable=self.password_var, + font=("Microsoft YaHei", 10, "bold"), + fg=self.colors["success"], + bg=self.colors["panel"], + ).pack(side=tk.LEFT, padx=(8, 0)) + + tips_panel = tk.Frame(top_frame, bg=self.colors["panel"], highlightthickness=1, + highlightbackground=self.colors["border"]) + tips_panel.pack(fill=tk.X, pady=(0, 8)) + + self.tips_title_label = tk.Label( + tips_panel, + text=self.t("tips_title"), + font=("Microsoft YaHei", 11, "bold"), + fg=self.colors["warning"], + bg=self.colors["panel"], + anchor="w", + ) + self.tips_title_label.pack(fill=tk.X, padx=12, pady=(8, 2)) + + self.tips_text_label = tk.Label( + tips_panel, + text=self.t("tips_text"), + font=("Microsoft YaHei", 9), + fg=self.colors["text"], + bg=self.colors["panel"], + justify=tk.LEFT, + anchor="w", + wraplength=800, + ) + self.tips_text_label.pack(fill=tk.X, padx=12, pady=(0, 8)) + + self.function_panel = tk.LabelFrame( + top_frame, + text=f" {self.t('function_area')} ", + font=("Microsoft YaHei", 11, "bold"), + fg=self.colors["text"], + bg=self.colors["panel"], + bd=1, + relief=tk.SOLID, + labelanchor="nw", + ) + self.function_panel.pack(fill=tk.X, pady=(0, 8)) + + row1_frame = tk.Frame(self.function_panel, bg=self.colors["panel"]) + row1_frame.pack(fill=tk.X, padx=12, pady=(10, 4)) + + row2_frame = tk.Frame(self.function_panel, bg=self.colors["panel"]) + row2_frame.pack(fill=tk.X, padx=12, pady=(4, 10)) + + self.btn_permission = self.create_button( + row1_frame, + self.t("btn_permission"), + self.get_install_permission, + bg=self.colors["accent"], + ) + self.btn_permission.pack(side=tk.LEFT, padx=(0, 8), fill=tk.X, expand=True) + + self.btn_install_app = self.create_button( + row1_frame, + self.t("btn_install_app"), + self.install_app, + bg=self.colors["accent"], + ) + self.btn_install_app.pack(side=tk.LEFT, padx=8, fill=tk.X, expand=True) + + self.btn_set_language = self.create_button( + row1_frame, + self.t("btn_set_language"), + self.set_language, + bg=self.colors["accent"], + ) + self.btn_set_language.pack(side=tk.LEFT, padx=(8, 0), fill=tk.X, expand=True) + + self.btn_timezone = self.create_button( + row2_frame, + self.t("btn_timezone"), + self.open_timezone_settings, + bg=self.colors["accent"], + ) + self.btn_timezone.pack(side=tk.LEFT, padx=(0, 8), fill=tk.X, expand=True) + + self.btn_settings = self.create_button( + row2_frame, + self.t("btn_settings"), + self.open_android_settings, + bg=self.colors["accent"], + ) + self.btn_settings.pack(side=tk.LEFT, padx=8, fill=tk.X, expand=True) + + self.btn_reboot = self.create_button( + row2_frame, + self.t("btn_reboot"), + self.reboot_device, + bg=self.colors["warning"], + fg="#202020", + ) + self.btn_reboot.pack(side=tk.LEFT, padx=(8, 0), fill=tk.X, expand=True) + + progress_frame = tk.Frame(top_frame, bg=self.colors["bg"]) + progress_frame.pack(fill=tk.X, pady=(0, 6)) + + self.progress = ttk.Progressbar( + progress_frame, + mode="determinate", + maximum=100, + style="Horizontal.TProgressbar", + ) + self.progress.pack(fill=tk.X) + + self.progress_label = tk.Label( + top_frame, + text="", + font=("Microsoft YaHei", 9), + fg=self.colors["text_muted"], + bg=self.colors["bg"], + anchor="w", + ) + self.progress_label.pack(fill=tk.X, pady=(2, 8)) + + log_frame = tk.Frame(main_frame, bg=self.colors["bg"]) + log_frame.pack(fill=tk.BOTH, expand=True) + + log_header = tk.Frame(log_frame, bg=self.colors["bg"]) + log_header.pack(fill=tk.X, pady=(0, 6)) + + self.log_title_label = tk.Label( + log_header, + text=self.t("log_title"), + font=("Microsoft YaHei", 11, "bold"), + fg=self.colors["text"], + bg=self.colors["bg"], + ) + self.log_title_label.pack(side=tk.LEFT) + + self.btn_clear_log = self.create_button( + log_header, + self.t("btn_clear_log"), + self.clear_log, + bg=self.colors["panel_alt"], + width=10, + height=1, + ) + self.btn_clear_log.pack(side=tk.RIGHT) + + self.log_text = scrolledtext.ScrolledText( + log_frame, + wrap=tk.WORD, + font=("Consolas", 10), + bg="#151922", + fg=self.colors["text"], + insertbackground=self.colors["text"], + relief=tk.FLAT, + height=12, + ) + self.log_text.pack(fill=tk.BOTH, expand=True) + self.log_text.tag_config("INFO", foreground=self.colors["text"]) + self.log_text.tag_config("SUCCESS", foreground=self.colors["success"]) + self.log_text.tag_config("WARNING", foreground=self.colors["warning"]) + self.log_text.tag_config("ERROR", foreground=self.colors["danger"]) + + def create_button(self, parent, text, command, bg, fg="white", width=14, height=1): + button = tk.Button( + parent, + text=text, + command=command, + font=("Microsoft YaHei", 10, "bold"), + fg=fg, + bg=bg, + activeforeground=fg, + activebackground=self.colors["accent_hover"], + relief=tk.FLAT, + cursor="hand2", + width=width, + height=height, + ) + return button + + def run_on_ui_thread(self, func, *args, **kwargs): + self.root.after(0, lambda: func(*args, **kwargs)) + + def run_worker(self, target): + threading.Thread(target=target, daemon=True).start() + + def log(self, message, level="INFO"): + timestamp = datetime.now().strftime("%H:%M:%S") + self.log_text.insert(tk.END, f"[{timestamp}] {message}\n", level) + self.log_text.see(tk.END) + + def set_status(self, text): + self.status_var.set(text) + + def set_progress(self, value, text=""): + self.progress["value"] = value + self.progress_label.config(text=text) + + def is_placeholder_vin(self, text): + return text in (self.T["zh"]["vin_placeholder"], self.T["en"]["vin_placeholder"]) + + def _current_key_for_value(self, value, keys): + for key in keys: + for lang_data in self.T.values(): + if lang_data.get(key) == value: + return key + return None + + def toggle_lang(self): + old_placeholder = self.vin_placeholder + self.lang = "en" if self.lang == "zh" else "zh" + self.apply_language(old_placeholder) + self.log(self.t("log_lang_switched"), "INFO") + + def apply_language(self, old_placeholder=None): + old_placeholder = old_placeholder or self.vin_placeholder + password_key = self._current_key_for_value( + self.password_var.get(), + ["password_unknown", "password_querying", "password_failed"], + ) + status_key = self._current_key_for_value( + self.status_var.get(), + [ + "status_ready", + "status_password_querying", + "status_password_success", + "password_query_failed", + "status_get_permission", + "permission_ready", + "permission_failed", + "status_check_permission", + "need_permission_status", + "install_cancelled_status", + "status_installing_app", + "install_done", + "install_partial", + "install_failed", + "status_setting_language", + "language_done", + "language_failed", + "status_rebooting", + "status_rebooting_now", + "reboot_failed", + ], + ) + progress_key = self._current_key_for_value( + self.progress_label.cget("text"), + [ + "progress_get_permission", + "progress_detect_device", + "progress_enter_device", + "progress_config_env", + "progress_confirm_permission", + "permission_ready", + "permission_failed", + "progress_check_permission", + "install_done", + "install_partial", + "install_failed", + "language_done", + "language_failed", + ], + ) + + self.vin_placeholder = self.t("vin_placeholder") + self.root.title(self.t("title")) + self.title_label.config(text=self.t("title")) + self.btn_lang_switch.config(text=self.t("lang_switch")) + self.btn_query_password.config(text=self.t("btn_query_password")) + self.password_status_label.config(text=self.t("password_status")) + self.tips_title_label.config(text=self.t("tips_title")) + self.tips_text_label.config(text=self.t("tips_text")) + self.function_panel.config(text=f" {self.t('function_area')} ") + self.btn_permission.config(text=self.t("btn_permission")) + self.btn_install_app.config(text=self.t("btn_install_app")) + self.btn_set_language.config(text=self.t("btn_set_language")) + self.btn_timezone.config(text=self.t("btn_timezone")) + self.btn_settings.config(text=self.t("btn_settings")) + self.btn_reboot.config(text=self.t("btn_reboot")) + self.log_title_label.config(text=self.t("log_title")) + self.btn_clear_log.config(text=self.t("btn_clear_log")) + + if self.vin_var.get() in ("", old_placeholder) or self.is_placeholder_vin(self.vin_var.get()): + self.vin_var.set(self.vin_placeholder) + self.vin_entry.config(fg=self.colors["text_muted"]) + if password_key: + self.password_var.set(self.t(password_key)) + if status_key: + self.status_var.set(self.t(status_key)) + if progress_key: + self.progress_label.config(text=self.t(progress_key)) + self._update_device_status(self.device_connected) + + def set_busy(self, busy): + state = tk.DISABLED if busy else tk.NORMAL + buttons = [ + self.btn_lang_switch, + self.btn_query_password, + self.btn_permission, + self.btn_install_app, + self.btn_set_language, + self.btn_timezone, + self.btn_settings, + self.btn_reboot, + self.btn_clear_log, + ] + for button in buttons: + button.config(state=state) + + def clear_log(self): + self.log_text.delete("1.0", tk.END) + self.log(self.t("log_cleared")) + + def get_vin(self): + vin = self.vin_var.get().strip() + if self.is_placeholder_vin(vin): + return "" + return vin + + def _on_vin_focus_in(self, _event): + if self.vin_var.get() == self.vin_placeholder: + self.vin_var.set("") + self.vin_entry.config(fg=self.colors["text"]) + + def _on_vin_focus_out(self, _event): + if not self.vin_var.get().strip(): + self.vin_var.set(self.vin_placeholder) + self.vin_entry.config(fg=self.colors["text_muted"]) + + def query_password(self): + vin = self.get_vin() + if not vin: + messagebox.showwarning(self.t("warn_title"), self.t("input_vin")) + return + self.set_busy(True) + self.password_var.set(self.t("password_querying")) + self.set_status(self.t("status_password_querying")) + self.log(self.t("log_password_querying")) + self.run_worker(lambda: self._query_password_worker(vin)) + + def _query_password_worker(self, vin): + try: + query_date = datetime.now().strftime("%Y-%m-%d") + params = urlencode({ + "carModel": "EZ6", + "vin": vin, + "date": query_date, + }) + request = Request( + f"{self.password_api_url}?{params}", + headers={"User-Agent": "Mazda-EZ6-Installer/1.0"}, + ) + with urlopen(request, timeout=15) as response: + payload = response.read() + data = json.loads(payload.decode("utf-8")) + + result = data.get("data") if isinstance(data, dict) else None + password = result.get("password") if isinstance(result, dict) else None + error = result.get("error") if isinstance(result, dict) else None + + if data.get("success") and password: + self.run_on_ui_thread(self.password_var.set, password) + self.run_on_ui_thread(self.set_status, self.t("status_password_success")) + self.run_on_ui_thread(self.log, self.tf("log_password_success", password=password), "SUCCESS") + return + + message = error or data.get("message") or self.t("password_query_failed") + self.run_on_ui_thread(self.password_var.set, self.t("password_failed")) + self.run_on_ui_thread(self.set_status, self.t("password_query_failed")) + self.run_on_ui_thread(self.log, message, "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t("password_query_failed"), message) + except HTTPError as exc: + message = self.tf("password_http_failed", code=exc.code) + self.run_on_ui_thread(self.password_var.set, self.t("password_failed")) + self.run_on_ui_thread(self.set_status, self.t("password_query_failed")) + self.run_on_ui_thread(self.log, message, "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t("password_query_failed"), message) + except (URLError, TimeoutError): + message = self.t("password_network_failed") + self.run_on_ui_thread(self.password_var.set, self.t("password_failed")) + self.run_on_ui_thread(self.set_status, self.t("password_query_failed")) + self.run_on_ui_thread(self.log, message, "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t("password_query_failed"), message) + except Exception: + message = self.t("password_retry_failed") + self.run_on_ui_thread(self.password_var.set, self.t("password_failed")) + self.run_on_ui_thread(self.set_status, self.t("password_query_failed")) + self.run_on_ui_thread(self.log, message, "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t("password_query_failed"), message) + finally: + self.run_on_ui_thread(self.set_busy, False) + + def get_install_permission(self): + self.set_busy(True) + self.set_progress(10, self.t("progress_get_permission")) + self.set_status(self.t("status_get_permission")) + self.log(self.t("log_get_permission")) + self.run_worker(self._get_install_permission_worker) + + def install_app(self): + self.set_busy(True) + self.set_progress(0, self.t("progress_check_permission")) + self.set_status(self.t("status_check_permission")) + self.run_worker(self._check_permission_before_file_select) + + def set_language(self): + popup = tk.Toplevel(self.root) + popup.title(self.t("language_title")) + popup.geometry("420x320") + popup.configure(bg=self.colors["bg"]) + popup.transient(self.root) + popup.grab_set() + + tk.Label( + popup, + text=self.t("language_header"), + font=("Microsoft YaHei", 15, "bold"), + fg=self.colors["text"], + bg=self.colors["bg"], + ).pack(pady=(18, 6)) + + tk.Label( + popup, + text=self.t("language_hint"), + font=("Microsoft YaHei", 9), + fg=self.colors["text_muted"], + bg=self.colors["bg"], + ).pack(pady=(0, 12)) + + lang_frame = tk.Frame(popup, bg=self.colors["bg"]) + lang_frame.pack(fill=tk.BOTH, expand=True, padx=18, pady=(0, 14)) + + locale_codes = ["zh-CN", "en-US", "ru-RU", "fr-FR", "es-ES", "th-TH", "it-IT", "pt-BR"] + languages = list(zip(self.t("language_names"), locale_codes)) + + for index, (label, locale_code) in enumerate(languages): + row = index // 2 + col = index % 2 + button = tk.Button( + lang_frame, + text=label, + command=lambda loc=locale_code, name=label: self._set_language_from_popup(loc, name, popup), + font=("Microsoft YaHei", 10, "bold"), + fg="white", + bg=self.colors["accent"], + activeforeground="white", + activebackground=self.colors["accent_hover"], + relief=tk.FLAT, + cursor="hand2", + height=2, + ) + button.grid(row=row, column=col, padx=6, pady=6, sticky="nsew") + + for col in range(2): + lang_frame.columnconfigure(col, weight=1) + for row in range(4): + lang_frame.rowconfigure(row, weight=1) + + popup.update_idletasks() + x = self.root.winfo_x() + (self.root.winfo_width() - popup.winfo_width()) // 2 + y = self.root.winfo_y() + (self.root.winfo_height() - popup.winfo_height()) // 2 + popup.geometry(f"+{x}+{y}") + + def reboot_device(self): + if not messagebox.askyesno(self.t("reboot_confirm_title"), self.t("reboot_confirm_msg")): + self.log(self.t("reboot_cancelled"), "WARNING") + return + self.set_status(self.t("status_rebooting")) + self.log(self.t("log_rebooting")) + self.run_worker(self._reboot_device_worker) + + def _reboot_device_worker(self): + try: + code, _, _ = self.run_adb_command(["shell", "reboot"], timeout=20) + if code == 0: + self.run_on_ui_thread(self.set_status, self.t("status_rebooting_now")) + self.run_on_ui_thread(self.log, self.t("reboot_success_log"), "SUCCESS") + else: + self.run_on_ui_thread(self.set_status, self.t("reboot_failed")) + self.run_on_ui_thread(self.log, self.t("reboot_failed_log"), "ERROR") + except Exception: + self.run_on_ui_thread(self.set_status, self.t("reboot_failed")) + self.run_on_ui_thread(self.log, self.t("reboot_failed_log"), "ERROR") + + def open_timezone_settings(self): + label_key = "timezone_label" + label = self.t(label_key) + self.set_status(self.tf("status_opening", label=label)) + self.log(self.tf("log_opening", label=label)) + commands = [ + ["shell", "am", "start", "-a", "android.settings.DATE_SETTINGS"], + ["shell", "am", "start", "-n", "com.android.settings/.Settings$DateTimeSettingsActivity"], + ["shell", "am", "start", "-n", "com.android.settings/.DateTimeSettings"], + ["shell", "am", "start", "-a", "android.settings.SETTINGS"], + ] + self.run_worker(lambda: self._open_settings_action(commands, label_key)) + + def open_android_settings(self): + label_key = "settings_label" + label = self.t(label_key) + self.set_status(self.tf("status_opening", label=label)) + self.log(self.tf("log_opening", label=label)) + commands = [ + ["shell", "am", "start", "-a", "android.settings.SETTINGS"], + ["shell", "am", "start", "-n", "com.android.settings/.Settings"], + ] + self.run_worker(lambda: self._open_settings_action(commands, label_key)) + + def _open_settings_action(self, commands, label_key): + try: + for index, command in enumerate(commands): + code, stdout, stderr = self.run_adb_command(command, timeout=20) + if self._am_start_succeeded(code, stdout, stderr): + label = self.t(label_key) + self.run_on_ui_thread(self.set_status, self.tf("status_opened", label=label)) + if label_key == "timezone_label" and index == len(commands) - 1: + self.run_on_ui_thread(self.log, self.t("timezone_fallback_log"), "WARNING") + else: + self.run_on_ui_thread(self.log, self.tf("log_opened", label=label), "SUCCESS") + return + label = self.t(label_key) + self.run_on_ui_thread(self.set_status, self.tf("open_failed", label=label)) + self.run_on_ui_thread(self.log, self.tf("open_failed", label=label), "ERROR") + except Exception: + label = self.t(label_key) + self.run_on_ui_thread(self.set_status, self.tf("open_failed", label=label)) + self.run_on_ui_thread(self.log, self.tf("open_failed", label=label), "ERROR") + + def _am_start_succeeded(self, code, stdout, stderr): + output = f"{stdout}\n{stderr}".lower() + failed_markers = [ + "error:", + "exception", + "unable to resolve", + "not found", + "does not exist", + "permission denied", + "failed", + "please enter password", + ] + return code == 0 and not any(marker in output for marker in failed_markers) + + def _set_language_from_popup(self, locale_code, language_name, popup): + try: + popup.destroy() + except Exception: + pass + self.set_busy(True) + self.set_progress(0, self.tf("progress_setting_language", language=language_name)) + self.set_status(self.t("status_setting_language")) + self.log(self.tf("log_setting_language", language=language_name)) + self.run_worker(lambda: self._set_language_worker(locale_code, language_name)) + + def _set_language_worker(self, locale_code, language_name): + try: + code, _, _ = self.run_adb_command( + ["shell", "settings", "put", "system", "system_locales", locale_code], + timeout=20, + ) + if code == 0: + self.run_on_ui_thread(self.set_progress, 100, self.t("language_done")) + self.run_on_ui_thread(self.set_status, self.t("language_done")) + self.run_on_ui_thread(self.log, self.tf("language_success_log", language=language_name), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t("language_success_title"), + self.tf("language_success_msg", language=language_name), + ) + else: + self.run_on_ui_thread(self.set_progress, 0, self.t("language_failed")) + self.run_on_ui_thread(self.set_status, self.t("language_failed")) + self.run_on_ui_thread(self.log, self.t("language_failed_msg"), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t("language_failed"), self.t("language_failed_msg")) + except Exception: + self.run_on_ui_thread(self.set_progress, 0, self.t("language_failed")) + self.run_on_ui_thread(self.set_status, self.t("language_failed")) + self.run_on_ui_thread(self.log, self.t("language_failed_msg"), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t("language_failed"), self.t("language_failed_msg")) + self.run_on_ui_thread(self.set_busy, False) + + def _startupinfo(self): + if sys.platform != "win32": + return None + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + return startupinfo + + def _decode_output(self, data): + if not data: + return "" + for encoding in ("utf-8", "gbk", "latin-1"): + try: + return data.decode(encoding, errors="replace") + except Exception: + continue + return data.decode(errors="replace") + + def _run_process(self, args, timeout=30, input_text=None): + kwargs = { + "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, + "startupinfo": self._startupinfo(), + } + if sys.platform == "win32": + kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW + if input_text is not None: + kwargs["input"] = input_text.encode("utf-8") + completed = subprocess.run(args, timeout=timeout, **kwargs) + stdout = self._decode_output(completed.stdout) + stderr = self._decode_output(completed.stderr) + return completed.returncode, stdout, stderr + + def run_adb_command(self, adb_args, timeout=30, input_text=None): + command_args = list(adb_args) + result = self._run_process([self.adb, "-d"] + command_args, timeout=timeout, input_text=input_text) + if self._adb_needs_shell_password(command_args, result): + self._run_process( + [self.adb, "-d", "shell", "password", "*2@#Shell"], + timeout=10, + ) + result = self._run_process( + [self.adb, "-d"] + command_args, + timeout=timeout, + input_text=input_text, + ) + return result + + def _adb_needs_shell_password(self, adb_args, result): + if not adb_args or adb_args[0] != "shell": + return False + if len(adb_args) >= 2 and adb_args[1] == "password": + return False + _, stdout, stderr = result + output = f"{stdout}\n{stderr}".lower() + return "please enter password" in output and "adb shell password" in output + + def run_adb_host_command(self, adb_args, timeout=10): + return self._run_process([self.adb] + list(adb_args), timeout=timeout) + + def start_device_monitor(self): + if self._device_monitor_running: + return + self._device_monitor_running = True + self._schedule_device_check(0) + + def _schedule_device_check(self, delay_ms=3000): + self.root.after(delay_ms, self._check_device_status_async) + + def _check_device_status_async(self): + self.run_worker(self._check_device_status_worker) + + def _check_device_status_worker(self): + connected = False + try: + code, stdout, _ = self.run_adb_host_command(["devices"], timeout=8) + connected = code == 0 and self._has_connected_device(stdout) + except Exception: + connected = False + self.run_on_ui_thread(self._update_device_status, connected) + self.run_on_ui_thread(self._schedule_device_check, 3000) + + def _has_connected_device(self, adb_devices_output): + for line in adb_devices_output.splitlines(): + stripped = line.strip() + if not stripped or stripped.startswith("List of devices"): + continue + parts = stripped.split() + if len(parts) >= 2 and parts[1] == "device": + return True + return False + + def is_usb_device_connected(self): + code, stdout, _ = self.run_adb_host_command(["devices"], timeout=8) + return code == 0 and self._has_connected_device(stdout) + + def _update_device_status(self, connected): + self.device_connected = connected + if connected: + text = self.t("device_connected") + color = self.colors["success"] + else: + text = self.t("device_disconnected") + color = self.colors["danger"] + self.device_status_var.set(text) + self.device_status_label.config(fg=color) + self.device_status_dot.itemconfig(self.device_status_oval, fill=color) + + def _permission_script(self): + payload = """LClass1;->method1( +10 +--runtime-args +--setuid=1000 +--setgid=1000 +--runtime-flags=2049 +--mount-external-full +--setgroups=3003 +--nice-name=runnetcat +--seinfo=platform:targetSdkVersion=30:complete +--invoke-with +toybox nc -s 127.0.0.1 -p 4321 -L /system/bin/sh -l; +""" + return f"""settings put global hidden_api_blacklist_exemptions "{payload}" +settings delete global hidden_api_blacklist_exemptions +sleep 2 +settings get global hidden_api_blacklist_exemptions +toybox nc localhost 4321 <<'NC_CMDS' +setprop sys.config.app_install_disabled false +getprop sys.config.app_install_disabled +settings delete global hidden_api_blacklist_exemptions +settings get global hidden_api_blacklist_exemptions +setprop ctl.restart zygote +exit +NC_CMDS +""" + + def _get_install_permission_worker(self): + success = False + try: + self.run_on_ui_thread(self.set_progress, 20, self.t("progress_detect_device")) + if not self.is_usb_device_connected(): + raise RuntimeError("device not connected") + self.run_on_ui_thread(self._update_device_status, True) + + self.run_on_ui_thread(self.set_progress, 30, self.t("progress_enter_device")) + self.run_adb_command( + ["shell", "password", "*2@#Shell"], + timeout=10, + ) + + self.run_on_ui_thread(self.set_progress, 45, self.t("progress_config_env")) + try: + self.run_adb_command( + ["shell"], + timeout=45, + input_text=self._permission_script(), + ) + except subprocess.TimeoutExpired: + pass + + self.run_on_ui_thread(self.set_progress, 80, self.t("progress_confirm_permission")) + for _ in range(8): + time.sleep(1) + if self._is_install_permission_ready(): + success = True + break + except Exception: + success = False + + self.install_permission_ready = success + if success: + self.run_on_ui_thread(self.set_progress, 100, self.t("permission_ready")) + self.run_on_ui_thread(self.set_status, self.t("permission_ready")) + self.run_on_ui_thread(self.log, self.t("permission_success_log"), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t("success_title"), self.t("permission_success_log")) + else: + self.run_on_ui_thread(self.set_progress, 0, self.t("permission_failed")) + self.run_on_ui_thread(self.set_status, self.t("permission_failed")) + self.run_on_ui_thread(self.log, self.t("permission_failed_log"), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t("permission_failed"), self.t("permission_failed_log")) + self.run_on_ui_thread(self.set_busy, False) + + def _is_install_permission_ready(self): + prop_code, prop_out, _ = self.run_adb_command( + ["shell", "getprop", "sys.config.app_install_disabled"], + timeout=15, + ) + setting_code, setting_out, _ = self.run_adb_command( + ["shell", "settings", "get", "global", "hidden_api_blacklist_exemptions"], + timeout=15, + ) + prop_value = prop_out.strip().lower() + setting_value = setting_out.strip().lower() + return prop_code == 0 and setting_code == 0 and prop_value == "false" and setting_value == "null" + + def _check_permission_before_file_select(self): + try: + ready = self._is_install_permission_ready() + except Exception: + ready = False + + if not ready: + self.install_permission_ready = False + self.run_on_ui_thread(self.set_progress, 0, "") + self.run_on_ui_thread(self.set_status, self.t("need_permission_status")) + self.run_on_ui_thread(self.log, self.t("need_permission_log"), "WARNING") + self.run_on_ui_thread(messagebox.showwarning, self.t("warn_title"), self.t("need_permission_msg")) + self.run_on_ui_thread(self.set_busy, False) + return + + self.install_permission_ready = True + self.run_on_ui_thread(self._select_apk_files) + + def _select_apk_files(self): + files = filedialog.askopenfilenames( + title=self.t("file_select_apk_title"), + filetypes=[(self.t("filetype_apk"), "*.apk"), (self.t("filetype_all"), "*.*")], + ) + if not files: + self.set_progress(0, "") + self.set_status(self.t("install_cancelled_status")) + self.log(self.t("install_cancelled_log"), "WARNING") + self.set_busy(False) + return + + self.set_progress(0, self.tf("install_prepare", count=len(files))) + self.set_status(self.t("status_installing_app")) + self.log(self.tf("log_install_start", count=len(files))) + self.run_worker(lambda: self._install_apk_files(list(files))) + + def _install_apk_files(self, files): + total = len(files) + success_count = 0 + failed_count = 0 + + for index, file_path in enumerate(files, start=1): + apk_name = Path(file_path).name + percent = int(((index - 1) / total) * 100) + self.run_on_ui_thread( + self.set_progress, + percent, + self.tf("installing_item", name=apk_name, index=index, total=total), + ) + self.run_on_ui_thread(self.set_status, self.tf("installing_status", name=apk_name)) + + try: + code, _, _ = self.run_adb_command( + ["install", "-r", "-d", file_path], + timeout=180, + ) + if code == 0: + success_count += 1 + self.run_on_ui_thread(self.log, self.tf("apk_install_success", name=apk_name), "SUCCESS") + else: + failed_count += 1 + self.run_on_ui_thread(self.log, self.tf("apk_install_failed", name=apk_name), "ERROR") + except Exception: + failed_count += 1 + self.run_on_ui_thread(self.log, self.tf("apk_install_failed", name=apk_name), "ERROR") + + if failed_count == 0: + self.run_on_ui_thread(self.set_progress, 100, self.t("install_done")) + self.run_on_ui_thread(self.set_status, self.t("install_done")) + self.run_on_ui_thread(self.log, self.tf("install_done_log", count=success_count), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t("install_done"), + self.tf("install_done_msg", count=success_count), + ) + elif success_count > 0: + self.run_on_ui_thread(self.set_progress, 100, self.t("install_partial")) + self.run_on_ui_thread(self.set_status, self.t("install_partial")) + self.run_on_ui_thread( + self.log, + self.tf("install_partial_log", success=success_count, failed=failed_count), + "WARNING", + ) + self.run_on_ui_thread( + messagebox.showwarning, + self.t("install_partial_title"), + self.tf("install_partial_msg", success=success_count, failed=failed_count), + ) + else: + self.run_on_ui_thread(self.set_progress, 0, self.t("install_failed")) + self.run_on_ui_thread(self.set_status, self.t("install_failed")) + self.run_on_ui_thread(self.log, self.t("install_failed_log"), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t("install_failed"), self.t("install_failed_msg")) + + self.run_on_ui_thread(self.set_busy, False) + + def center_window(self): + self.root.update_idletasks() + width = self.root.winfo_width() + height = self.root.winfo_height() + screen_width = self.root.winfo_screenwidth() + screen_height = self.root.winfo_screenheight() + x = (screen_width - width) // 2 + y = (screen_height - height) // 2 + self.root.geometry(f"{width}x{height}+{x}+{y}") + + def run(self): + self.root.mainloop() + + +def main(): + app = ADKAPKGUI() + app.run() + + +if __name__ == "__main__": + main() diff --git a/Mazda-EZ6/app.ico b/Mazda-EZ6/app.ico new file mode 100644 index 0000000..4f8e684 Binary files /dev/null and b/Mazda-EZ6/app.ico differ diff --git a/Mazda-EZ6/pack_mazda_ez6.bat b/Mazda-EZ6/pack_mazda_ez6.bat new file mode 100644 index 0000000..b8b1f6a --- /dev/null +++ b/Mazda-EZ6/pack_mazda_ez6.bat @@ -0,0 +1,153 @@ +@echo off +chcp 65001 >nul +cd /d "%~dp0" +set "ROOT=%~dp0.." +set "TOOLS=%ROOT%\tools" +set "NAME=Mazda-EZ6-Installer" +set "SRC=Mazda-EZ6_Installer.py" +set "ICON=%~dp0app.ico" +title %NAME% - Cython Build + +echo ============================================================ +echo %NAME% - Cython Build +echo ============================================================ +echo. + +where python >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Python not found + pause + exit /b 1 +) +for /f "delims=" %%i in ('where python') do set "PY=%%i" +echo Python: %PY% + +if not exist "%SRC%" ( + echo [ERROR] Source not found: %SRC% + pause + exit /b 1 +) +if not exist "%ICON%" ( + echo [ERROR] Icon not found: %ICON% + pause + exit /b 1 +) +if not exist "%TOOLS%\adb.exe" ( + echo [ERROR] adb.exe not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\AdbWinApi.dll" ( + echo [ERROR] AdbWinApi.dll not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\AdbWinUsbApi.dll" ( + echo [ERROR] AdbWinUsbApi.dll not found in %TOOLS% + pause + exit /b 1 +) + +echo [1/6] Installing deps... +"%PY%" -m pip install pyinstaller cython -q +if errorlevel 1 ( + "%PY%" -m pip install pyinstaller cython -q -i https://pypi.tuna.tsinghua.edu.cn/simple +) +if errorlevel 1 ( + echo [ERROR] Dependency install failed + pause + exit /b 1 +) + +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 +if exist "%NAME%.spec" del /q "%NAME%.spec" 2>nul + +echo [3/6] Cython compile... +mkdir dist_cy 2>nul +copy "%SRC%" "dist_cy\_core.py" >nul +if errorlevel 1 ( + echo [ERROR] Copy source failed + pause + exit /b 1 +) + +"%PY%" -c "open('dist_cy/setup_cython.py','w',encoding='utf-8').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')" +if errorlevel 1 ( + echo [ERROR] Failed to create Cython setup script + pause + exit /b 1 +) + +cd dist_cy +"%PY%" setup_cython.py build_ext --inplace +if errorlevel 1 ( + cd .. + echo [ERROR] Cython failed. Build stopped. + pause + exit /b 1 +) + +set "PYD=" +for %%f in (_core*.pyd) do set "PYD=%%f" +if "%PYD%"=="" ( + cd .. + echo [ERROR] No Cython PYD generated. Build stopped. + pause + exit /b 1 +) +echo PYD: %PYD% +copy "%PYD%" "_core.pyd" >nul +if errorlevel 1 ( + cd .. + echo [ERROR] Failed to copy Cython PYD + pause + exit /b 1 +) + +"%PY%" -c "open('launcher.py','w',encoding='utf-8').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')" +if errorlevel 1 ( + cd .. + echo [ERROR] Failed to create launcher.py + pause + exit /b 1 +) + +echo [4/6] Copy resources... +copy "%TOOLS%\adb.exe" . >nul +copy "%TOOLS%\AdbWinApi.dll" . >nul +copy "%TOOLS%\AdbWinUsbApi.dll" . >nul +copy "%ICON%" . >nul +if errorlevel 1 ( + cd .. + echo [ERROR] Copy resources failed + pause + exit /b 1 +) + +echo [5/6] PyInstaller... +"%PY%" -m PyInstaller --onefile --windowed --name="%NAME%" --icon="app.ico" --add-data "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=tkinter.filedialog --hidden-import=tkinter.messagebox --hidden-import=tkinter.scrolledtext --hidden-import=tkinter.ttk --hidden-import=json --hidden-import=urllib --hidden-import=urllib.error --hidden-import=urllib.parse --hidden-import=urllib.request --collect-all tkinter --uac-admin launcher.py +if errorlevel 1 ( + cd .. + echo [ERROR] PyInstaller failed + pause + exit /b 1 +) + +echo [6/6] Cleanup... +del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py adb.exe AdbWinApi.dll AdbWinUsbApi.dll app.ico 2>nul +rmdir /s /q build 2>nul +cd .. + +echo. +echo Done. +if exist "dist_cy\dist\%NAME%.exe" ( + echo Output: dist_cy\dist\%NAME%.exe +) else ( + echo [ERROR] Output exe was not generated + pause + exit /b 1 +) +pause diff --git a/Mazda-EZ60/Mazda-EZ60_1.2.py b/Mazda-EZ60/Mazda-EZ60_1.2.py new file mode 100644 index 0000000..c752ddb --- /dev/null +++ b/Mazda-EZ60/Mazda-EZ60_1.2.py @@ -0,0 +1,3981 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import subprocess +import json +import threading +import re +import atexit +import base64 +import hashlib +import struct +import tempfile +import zlib +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 +try: + from cryptography.hazmat.primitives.ciphers.aead import AESGCM +except ImportError: + AESGCM = None + +DEFAULT_EXTRACT_PASSWORD = object() + + +def set_windows_app_id(): + if sys.platform != 'win32': + return + try: + import ctypes + app_id = 'YibinKeyi.MazdaEZ60.LanguageInstaller.1.2' + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id) + except Exception: + pass + + +def get_app_dir(): + return Path(sys.executable).resolve().parent if getattr(sys, 'frozen', False) else Path(__file__).resolve().parent + + +def resource_candidates(file_name): + base_dir = get_app_dir() + candidates = [] + if getattr(sys, 'frozen', False): + candidates.append(Path(getattr(sys, '_MEIPASS', base_dir)) / file_name) + candidates.extend([ + base_dir / file_name, + base_dir / 'tools' / file_name, + base_dir / 'shared' / file_name, + base_dir.parent / 'tools' / file_name, + base_dir.parent / 'shared' / file_name, + base_dir.parent / file_name, + ]) + unique = [] + for candidate in candidates: + if candidate not in unique: + unique.append(candidate) + return unique + + +def find_resource(file_name): + candidates = resource_candidates(file_name) + for candidate in candidates: + if candidate.exists(): + return candidate + return candidates[0] + + +def resource_dir_candidates(dir_name): + base_dir = get_app_dir() + candidates = [] + if getattr(sys, 'frozen', False): + candidates.append(Path(getattr(sys, '_MEIPASS', base_dir)) / dir_name) + candidates.extend([ + base_dir / dir_name, + base_dir / 'tools' / dir_name, + base_dir / 'shared' / dir_name, + base_dir.parent / 'tools' / dir_name, + base_dir.parent / 'shared' / dir_name, + base_dir.parent / dir_name, + base_dir.parent / 'Q05-Lidar' / dir_name, + ]) + unique = [] + for candidate in candidates: + if candidate not in unique: + unique.append(candidate) + return unique + + +def find_resource_dir(dir_name): + candidates = resource_dir_candidates(dir_name) + for candidate in candidates: + if candidate.exists() and candidate.is_dir(): + return candidate + return candidates[0] + + +def find_tool(file_name, fallback=None): + path = find_resource(file_name) + if path.exists(): + return str(path) + return fallback or str(path) +class ADKAPKGUI: + CACHE_DIR_NAME = "apps_cache_Mazda_EZ60" + + def __init__(self): + set_windows_app_id() + self.root = tk.Tk() + self.root.title("Mazda-EZ60_OS-1.2适用") + self.root.geometry("900x620") + self.root.resizable(True, True) + self.set_window_icon() + self.root.after(200, self.set_window_icon) + + # 设置颜色主题 + 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': 'Mazda-EZ60_OS-1.2适用', + 'btn_permission': '🔓 获取权限', + 'btn_voice_patch': '🎙 语音助理补丁', + 'btn_push': '📦 刷入语言包', + 'btn_install': '📱 安装App', + 'btn_language': '🌐 语言设置', + 'btn_timezone': '⏰ 时区设置', + 'btn_settings': '⚙️ 安卓设置', + 'btn_reboot': '🔄 重启设备', + 'btn_disable_upgrade': '❌ 禁用升级', + 'btn_clear_log': '🗑 清空日志', + 'btn_query_pwd': '查询密码', + 'btn_install_driver': '🧩 安装驱动', + 'btn_debug_extract': '解压测试', + 'btn_debug_boot': '解压Boot测试', + 'device_label': '设备:', + 'vin_label': 'VIN码:', + 'auth_label': '授权:', + 'log_title': '📋 运行日志', + 'status_ready': '就绪', + 'status_connected': '已连接', + 'status_disconnected': '未连接', + 'status_detecting': '未检测', + 'vin_none': '未获取', + 'auth_none': '未验证', + 'auth_yes': '已授权', + 'auth_no': '未授权', + 'btn_refresh': '🔄 检查', + 'hint_factory': '🔧 关闭车辆WI-FI和4G网络,拨号获取的密码进入工程模式', + 'hotspot_icon': '📶', + 'hotspot_title': '电脑热点', + 'hotspot_start': '🔧 打开热点设置', + 'hint_icon': '💡', + 'hint_title': '使用提示', + 'theme_dark': '🌙 暗色', + 'theme_light': '☀️ 亮色', + 'lang_zh': '中', + 'lang_en': 'EN', + 'pwd_query_label': '工程密码查询:', + 'vin_placeholder': '请输入VIN', + 'vin_query_hint': '💡 请输入VIN或者VIN后八位查询。', + 'pwd_empty': '', + 'pwd_success': '密码: *#{password}#*', + 'pwd_failed': '失败: {message}', + 'pwd_request_failed': '请求失败', + 'hotspot_name_detecting': '名称: 检测中...', + 'hotspot_name_unset': '名称: 未配置', + 'hotspot_name_value': '名称: {ssid}', + 'hotspot_pwd_default': '密码: changan2024', + 'hotspot_pwd_value': '密码: {password}', + 'hotspot_status_off': '状态: 未启动', + 'hotspot_status_value': '状态: {status}', + 'hotspot_started': '已启动', + 'hotspot_stopped': '未启动', + 'hint_lines': [ + '1. 确保电脑已开启热点', + '2. 拨号进入工厂模式,点击调试工具', + '3. 需要云端认证时,连接右侧显示的电脑热点', + '4. 连接后点击车机“云端认证”按钮', + '5. 打开 ADB 后即可刷入语言包', + ], + 'log_lang_changed': '语言已切换为中文', + 'log_cleared': '日志已清空', + 'msg_warn_title': '警告', + 'msg_error_title': '错误', + 'msg_success_title': '成功', + 'msg_hint_title': '提示', + 'msg_device_not_connected_title': '设备未连接', + 'msg_device_not_connected': '请先连接设备并点击「检查」按钮刷新状态!', + 'msg_need_vin': '请先刷新设备状态并获取VIN码', + 'msg_auth_failed_title': '授权失败', + 'msg_device_unauthorized': '设备未授权', + 'msg_device_unauthorized_action': '设备未授权,无法执行此操作', + 'msg_data_prepare_failed': '资源初始化失败!', + 'msg_resource_prepare_failed': '资源准备失败!', + 'msg_resource_dir_missing': '资源目录未找到', + 'msg_input_vin': '请输入VIN码', + 'msg_done_title': '完成', + 'msg_confirm_permission_title': '确认获取权限', + 'msg_confirm_permission': '即将获取系统权限,过程中请勿断开数据连接或关闭程序。\n\n是否继续?', + 'msg_permission_done': '获取成功,设备正在重启。', + 'msg_driver_missing_title': '驱动环境', + 'msg_driver_missing': '驱动缺失,即将自动安装驱动。', + 'msg_driver_install_confirm': '即将安装驱动环境,需要管理员权限。', + 'msg_driver_install_done': '驱动安装完成。如设备仍无法识别,请重新插拔 USB 线缆。', + 'msg_driver_install_failed': '驱动安装失败: {error}', + 'msg_reboot_title': '确认重启', + 'msg_reboot_confirm': '确定要重启设备吗?', + 'msg_disable_ota_title': '确认禁用升级', + 'msg_disable_ota_confirm': '⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n是否确定要禁用系统升级应用?', + 'msg_disable_ota_success': '系统升级已成功禁用!', + 'msg_disable_ota_failed': '禁用失败:{output}', + 'file_select_apk_title': '选择APK文件', + 'filetype_apk': 'APK文件', + 'filetype_all': '所有文件', + 'msg_install_confirm_title': '确认安装', + 'msg_install_confirm_many': '已选择 {count} 个APK文件\n\n是否开始安装?', + 'msg_install_confirm_folder': '找到 {count} 个APK文件\n\n是否开始批量安装?', + 'msg_install_done_title': '安装完成', + 'msg_install_done_all': '成功安装 {count} 个APK!', + 'msg_install_partial_title': '部分成功', + 'msg_install_partial': '成功: {success}\n失败: {failed}', + 'msg_install_failed_title': '安装失败', + 'msg_install_failed_all': '所有APK安装失败!', + 'msg_apks_dir_missing': '未找到apks文件夹!\n请在程序目录下创建apks文件夹并放入APK文件。', + 'msg_apks_empty': 'apks文件夹中没有找到APK文件!', + 'quick_lang_title': '快捷语言设置', + 'quick_lang_header': '选择目标语言', + 'quick_lang_hint': '点击按钮即可将系统语言切换为对应语言,重启后生效', + 'quick_lang_system': '⚙️ 打开系统语言设置(手动选择)', + 'quick_lang_success_title': '设置成功', + 'quick_lang_success': '系统语言已设置为 {language}\n\n⚠️ 请重启设备使其生效。', + 'quick_lang_failed_title': '设置失败', + 'quick_lang_failed': '语言设置失败!', + 'quick_lang_names': ['🇨🇳 中文', '英 English', '俄 Русский', '法 Français', '西 Español', '葡 Português', '意 Italiano', '阿 العربية'], + 'debug_title': '调试模式', + 'debug_prompt': '请输入调试密码:', + 'debug_password_verifying': '正在校验调试模式密码...', + 'debug_verify_failed': '调试模式密码校验失败: {message}', + 'debug_status': '🔧 调试模式', + 'debug_need_enable': '请先按 Ctrl+Shift+D 进入调试模式', + 'debug_need_vin': '请先刷新设备VIN,或在工程密码输入框填入VIN', + 'debug_boot_title': '解压Boot测试', + 'debug_boot_prompt': '未检测到VIN/设备。\n请输入 BOOT_KEY 或 boot-key 返回的 sessionKey:', + 'debug_key_len_error': '密钥长度错误,应为32字节AES密钥', + 'debug_key_format_error': '密钥格式错误: {error}', + 'msg_debug_wrong_password': '密码错误', + 'status_debug': '🔧 调试模式', + 'progress_loading': '资源加载中', + 'progress_loaded': '资源加载完成', + 'progress_preparing': '正在准备资源', + 'progress_prepare_runtime': '正在获取权限中', + 'progress_install_runtime': '正在获取权限中', + 'progress_fetch_boot_key': '正在获取权限中', + 'progress_reboot_fastboot': '正在获取权限中', + 'progress_decrypt_init_boot': '正在获取权限中', + 'progress_flash_init_boot': '正在获取权限中', + 'progress_reboot_device': '正在获取权限中', + 'progress_install_module': '安装补丁', + 'progress_flashing': '正在刷入', + 'progress_flash_done': '刷入完成', + 'progress_installing': '安装中', + 'progress_installing_name': '安装中 ({name})', + 'progress_install_done': '安装完成', + 'progress_done': '完成', + 'log_device_disconnected': '设备已断开连接', + 'log_device_connected': '设备已连接', + 'log_no_package': '资源文件缺失', + 'log_no_adb': '未找到adb命令,请将ADB文件放入本目录', + 'log_no_fastboot': '运行环境缺失', + 'log_vin': 'VIN: {vin}', + 'log_vin_unavailable': '无法获取VIN,请确认设备已进入工厂模式', + 'log_auth_checking': '正在验证授权状态...', + 'log_auth_success': '授权验证通过', + 'log_auth_failed': '授权验证失败', + 'log_debug_skip_auth': '调试模式: 跳过授权验证', + 'log_vehicle_name': '车辆名称: {vehicle}', + 'log_data_prepare_failed': '资源初始化失败', + 'log_boot_challenge_failed': '获取失败', + 'log_boot_key_failed': '获取失败', + 'log_boot_key_len_error': '获取失败', + 'log_boot_key_success': '正在获取权限中', + 'log_crypto_missing': '获取失败', + 'log_driver_found': '已检测到驱动环境', + 'log_driver_missing': '未检测到驱动环境', + 'log_driver_install_start': '正在安装驱动环境...', + 'log_driver_install_success': '驱动环境安装完成', + 'log_driver_install_failed': '驱动环境安装失败: {error}', + 'log_runtime_missing': '获取失败', + 'log_base_apk_invalid': '获取失败', + 'log_runtime_ready': '正在获取权限中', + 'log_runtime_install_failed': '获取失败', + 'log_runtime_install_success': '正在获取权限中', + 'log_permission_resource_missing': '获取失败', + 'log_permission_resource_format_unsupported': '获取失败', + 'log_permission_resource_algorithm_unsupported': '获取失败', + 'log_permission_resource_decrypt_auth_failed': '获取失败', + 'log_permission_resource_decrypt_ready': '正在获取权限中', + 'log_permission_resource_decrypt_failed': '获取失败', + 'log_fastboot_wait': '正在获取权限,请不要关闭程序或断开数据连接!', + 'log_fastboot_missing': '获取失败', + 'log_fastboot_enter_failed': '获取失败', + 'log_init_boot_flash_failed': '获取失败', + 'log_init_boot_flash_success': '正在获取权限中', + 'log_fastboot_reboot_failed': '获取失败', + 'log_permission_success': '获取成功', + 'log_permission_failed': '获取失败', + 'log_temp_img_deleted': '临时文件已清理', + 'log_temp_img_delete_failed': '临时文件清理失败', + 'log_open_magisk': '请在弹窗中点击“允许”授予权限', + 'log_root_checking': '正在检测权限...', + 'log_root_ok': '权限已授予', + 'log_root_failed': '权限获取失败,请手动授予权限', + 'msg_root_failed': '请手动授予权限后重试。', + 'log_magisk_cleanup_done': '权限入口已清理', + 'log_magisk_cleanup_failed': '权限入口清理失败', + 'log_magisk_cleanup_module_done': '权限入口清理已持久化', + 'log_magisk_cleanup_module_failed': '权限入口清理持久化失败', + 'log_voice_patch_start': '开始安装语音助理补丁', + 'log_voice_patch_done': '语音助理补丁安装完成,重启设备后生效', + 'log_voice_patch_failed': '语音助理补丁安装失败', + 'log_module_install_start': '开始安装补丁', + 'log_module_install_done': '补丁安装完成', + 'log_module_install_failed': '补丁安装失败: {error}', + 'log_push_file_failed': '文件推送失败: {file}: {error}', + 'log_root_cmd_failed': '权限操作失败: {error}', + 'err_module_prop_missing': '资源包缺少补丁文件', + 'err_module_id_missing': '补丁配置缺少 ID 字段', + 'err_module_zip_invalid': '补丁文件无效或缺少配置: {file}', + 'err_duplicate_module_id': '资源包存在重复补丁 ID', + 'log_need_adb': '请先连接设备', + 'log_no_vehicle_name': '资源初始化失败', + 'log_package_missing': '资源文件缺失', + 'log_extract_password_missing': '资源初始化失败', + 'log_7za_missing': '运行环境缺失', + 'log_extracting': '资源准备中...', + 'log_apps_missing': '警告:未找到 apps 目录', + 'log_resource_invalid': '资源校验失败', + 'log_resource_ready': '资源准备完成', + 'log_resource_failed': '资源准备失败,请检查网络连接后重试', + 'log_cache_invalid': '资源缓存无效', + 'log_no_language_files': '未找到语言包文件', + 'log_flash_start': '开始刷入语言包,请勿断电或重启电脑和车机。', + 'log_install_success': '安装成功', + 'log_install_failed': '安装失败', + 'log_flash_item_failed': '语言包刷入失败: {current}/{total}', + 'log_flash_done_config': '语言包刷入完成,开始执行 Mazda-EZ60 安装后配置', + 'log_flash_partial_config': '语言包部分刷入成功,仍继续执行 Mazda-EZ60 安装后配置', + 'log_flash_failed_config': '语言包刷入失败,仍继续执行 Mazda-EZ60 安装后配置', + 'log_post_config_start': '正在执行 Mazda-EZ60 安装后配置...', + 'log_overlay_enabled': '配置项已完成', + 'log_overlay_failed': '配置项执行失败', + 'log_disabled_package': '清理项已完成', + 'log_disable_package_failed': '清理项执行失败', + 'log_post_config_done': 'Mazda-EZ60 安装后配置完成', + 'log_post_config_all_done': 'Mazda-EZ60 安装后配置全部完成,重启设备后生效', + 'log_post_config_partial': 'Mazda-EZ60 安装后配置部分失败,请查看日志', + 'log_batch_install_start': '开始批量安装 {count} 个APK...', + 'log_install_many_start': '开始安装 {count} 个APK...', + 'log_install_done_all': '安装完成:全部 {count} 个成功', + 'log_install_done_partial': '安装完成:{success}/{count} 成功', + 'log_install_failed_simple': '安装失败', + 'log_rebooting': '设备正在重启...', + 'log_disable_ota_cancelled': '已取消禁用升级操作', + 'log_disable_ota_success': '系统升级已禁用', + 'log_disable_ota_failed': '禁用系统升级失败', + 'log_pwd_success': '密码查询成功', + 'log_pwd_failed': '密码查询失败', + 'log_pwd_request_failed': '密码查询请求失败', + 'log_debug_off': '调试模式已关闭', + 'log_debug_on': '调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志', + 'log_debug_extract_start': '开始资源包解压测试...', + 'log_debug_extract_success': '资源包解压测试成功: APK数量={apk_count}, 补丁数量={module_count}', + 'log_debug_extract_failed': '资源包解压测试失败', + 'log_debug_boot_start': '开始 Boot 资源解压测试...', + 'log_debug_boot_success': 'Boot 资源解压测试成功: size={size}, sha256={sha}', + 'log_debug_boot_failed': 'Boot 资源解压测试失败', + 'log_env_config_failed_admin': '环境配置失败,请以管理员身份运行', + 'log_env_config_failed': '环境配置失败', + 'log_env_config_failed_detail': '环境配置失败: {error}', + 'log_hotspot_opening': '已打开热点设置,正在检测热点...', + 'log_hotspot_detected': '检测到热点: {ssid} / {password}', + 'log_hotspot_not_detected': '未检测到热点,请确认已开启', + 'err_extract_wrong_password': '资源准备失败', + 'err_extract_data': '资源准备失败', + 'err_extract_headers': '资源准备失败', + 'err_extract_detail': '资源准备失败', + 'err_extract_code': '资源准备失败', + 'err_missing_apps': '资源目录异常', + 'err_empty_apps': '资源目录异常', + 'err_zero_apks': '资源文件异常', + 'err_cmd_timeout': '命令超时', + 'err_fastboot_timeout': '获取失败', + 'err_push_failed': 'push失败: {error}', + 'err_install_failed': 'install失败: {error}', + 'err_decode_failed': '解码失败', + 'msg_start_failed_title': '错误', + 'msg_start_failed': '程序启动失败: {error}', + 'print_python_required': '错误:需要Python 3.6或更高版本', + 'print_start_failed': '启动失败: {error}', + }, + 'en': { + 'title': 'Mazda-EZ60_OS-1.2适用', + 'btn_permission': '🔓 Unlock', + 'btn_voice_patch': '🎙 Voice Patch', + 'btn_push': '📦 Flash Lang Pkg', + 'btn_install': '📱 Install App', + 'btn_language': '🌐 Language', + 'btn_timezone': '⏰ Timezone', + 'btn_settings': '⚙️ Settings', + 'btn_reboot': '🔄 Reboot', + 'btn_disable_upgrade': '❌ Disable OTA', + 'btn_clear_log': '🗑 Clear Log', + 'btn_query_pwd': 'Query Pwd', + 'btn_install_driver': '🧩 Driver', + 'btn_debug_extract': 'Extract Test', + 'btn_debug_boot': 'Boot Extract', + 'device_label': 'Device:', + 'vin_label': 'VIN:', + 'auth_label': 'Auth:', + 'log_title': '📋 Log', + 'status_ready': 'Ready', + 'status_connected': 'Connected', + 'status_disconnected': 'Disconnected', + 'status_detecting': 'Detecting', + 'vin_none': 'None', + 'auth_none': 'Unknown', + 'auth_yes': 'Authorized', + 'auth_no': 'Unauthorized', + 'btn_refresh': '🔄 Check', + 'hint_factory': '🔧 Turn off WiFi & 4G, enter factory mode with dial code', + 'hotspot_icon': '📶', + 'hotspot_title': 'Hotspot', + 'hotspot_start': '🔧 Open Hotspot Settings', + 'hint_icon': '💡', + 'hint_title': 'Tips', + 'theme_dark': '🌙 Dark', + 'theme_light': '☀️ Light', + 'lang_zh': '中', + 'lang_en': 'EN', + 'pwd_query_label': 'Factory password:', + 'vin_placeholder': 'Enter VIN', + 'vin_query_hint': '💡 Enter the VIN or the last 8 digits of the VIN to query.', + 'pwd_empty': '', + 'pwd_success': 'Password: *#{password}#*', + 'pwd_failed': 'Failed: {message}', + 'pwd_request_failed': 'Request failed', + 'hotspot_name_detecting': 'Name: detecting...', + 'hotspot_name_unset': 'Name: not configured', + 'hotspot_name_value': 'Name: {ssid}', + 'hotspot_pwd_default': 'Password: changan2024', + 'hotspot_pwd_value': 'Password: {password}', + 'hotspot_status_off': 'Status: stopped', + 'hotspot_status_value': 'Status: {status}', + 'hotspot_started': 'started', + 'hotspot_stopped': 'stopped', + 'hint_lines': [ + '1. Turn on the PC hotspot', + '2. Enter factory mode with the dial password and open debug tools', + '3. For cloud authentication, connect the head unit to the hotspot shown above', + '4. Tap “Cloud Authentication” on the head unit after connecting', + '5. Enable ADB, then flash the language package', + ], + 'log_lang_changed': 'Language switched to English', + 'log_cleared': 'Log cleared', + 'msg_warn_title': 'Warning', + 'msg_error_title': 'Error', + 'msg_success_title': 'Success', + 'msg_hint_title': 'Hint', + 'msg_device_not_connected_title': 'Device not connected', + 'msg_device_not_connected': 'Connect the device and click "Check" to refresh status first.', + 'msg_need_vin': 'Refresh device status and get VIN first', + 'msg_auth_failed_title': 'Authorization failed', + 'msg_device_unauthorized': 'Device is not authorized', + 'msg_device_unauthorized_action': 'Device is not authorized. This action cannot continue.', + 'msg_data_prepare_failed': 'Resource initialization failed!', + 'msg_resource_prepare_failed': 'Resource preparation failed!', + 'msg_resource_dir_missing': 'Resource directory not found', + 'msg_input_vin': 'Enter VIN', + 'msg_done_title': 'Done', + 'msg_confirm_permission_title': 'Confirm unlock', + 'msg_confirm_permission': 'The tool will get system permission. Do not disconnect the data cable or close the program during the process.\n\nContinue?', + 'msg_permission_done': 'Permission acquired. The device is rebooting.', + 'msg_driver_missing_title': 'Driver Environment', + 'msg_driver_missing': 'Driver environment is missing. Driver installation will start automatically.', + 'msg_driver_install_confirm': 'Driver environment installation requires administrator permission.', + 'msg_driver_install_done': 'Driver installation completed. If the device is still not recognized, reconnect USB cable.', + 'msg_driver_install_failed': 'Driver installation failed: {error}', + 'msg_reboot_title': 'Confirm reboot', + 'msg_reboot_confirm': 'Reboot the device now?', + 'msg_disable_ota_title': 'Confirm Disable OTA', + 'msg_disable_ota_confirm': 'Warning: after disabling OTA, the system will not receive updates.\n\nDisable the OTA app now?', + 'msg_disable_ota_success': 'System OTA has been disabled.', + 'msg_disable_ota_failed': 'Disable failed: {output}', + 'file_select_apk_title': 'Select APK files', + 'filetype_apk': 'APK files', + 'filetype_all': 'All files', + 'msg_install_confirm_title': 'Confirm install', + 'msg_install_confirm_many': 'Selected {count} APK file(s).\n\nStart installing?', + 'msg_install_confirm_folder': 'Found {count} APK file(s).\n\nStart batch install?', + 'msg_install_done_title': 'Install complete', + 'msg_install_done_all': 'Successfully installed {count} APK file(s).', + 'msg_install_partial_title': 'Partially complete', + 'msg_install_partial': 'Succeeded: {success}\nFailed: {failed}', + 'msg_install_failed_title': 'Install failed', + 'msg_install_failed_all': 'All APK installs failed.', + 'msg_apks_dir_missing': 'apks folder not found.\nCreate an apks folder next to the program and place APK files in it.', + 'msg_apks_empty': 'No APK files found in the apks folder.', + 'quick_lang_title': 'Quick Language', + 'quick_lang_header': 'Select Target Language', + 'quick_lang_hint': 'Tap a language to switch the system locale. Reboot to apply.', + 'quick_lang_system': '⚙️ Open system language settings', + 'quick_lang_success_title': 'Set Successfully', + 'quick_lang_success': 'System language has been set to {language}.\n\nReboot the device to apply.', + 'quick_lang_failed_title': 'Set Failed', + 'quick_lang_failed': 'Language setting failed.', + 'quick_lang_names': ['🇨🇳 Chinese', 'English', 'Russian', 'French', 'Spanish', 'Portuguese', 'Italian', 'Arabic'], + 'debug_title': 'Debug Mode', + 'debug_prompt': 'Enter debug password:', + 'debug_password_verifying': 'Verifying debug mode password...', + 'debug_verify_failed': 'Debug mode password verification failed: {message}', + 'debug_status': '🔧 Debug Mode', + 'debug_need_enable': 'Press Ctrl+Shift+D to enable debug mode first', + 'debug_need_vin': 'Refresh device VIN first, or enter VIN in the password query box', + 'debug_boot_title': 'Boot Extract Test', + 'debug_boot_prompt': 'No VIN/device detected.\nEnter BOOT_KEY or sessionKey returned by boot-key:', + 'debug_key_len_error': 'Invalid key length. Expected a 32-byte AES key.', + 'debug_key_format_error': 'Invalid key format: {error}', + 'msg_debug_wrong_password': 'Wrong password', + 'status_debug': '🔧 Debug mode', + 'progress_loading': 'Preparing resources', + 'progress_loaded': 'Resources ready', + 'progress_preparing': 'Preparing resources', + 'progress_prepare_runtime': 'Getting permission', + 'progress_install_runtime': 'Getting permission', + 'progress_fetch_boot_key': 'Getting permission', + 'progress_reboot_fastboot': 'Getting permission', + 'progress_decrypt_init_boot': 'Getting permission', + 'progress_flash_init_boot': 'Getting permission', + 'progress_reboot_device': 'Getting permission', + 'progress_install_module': 'Installing patch', + 'progress_flashing': 'Flashing', + 'progress_flash_done': 'Flash complete', + 'progress_installing': 'Installing', + 'progress_installing_name': 'Installing ({name})', + 'progress_install_done': 'Install complete', + 'progress_done': 'Done', + 'log_device_disconnected': 'Device disconnected', + 'log_device_connected': 'Device connected', + 'log_no_package': 'Resource file is missing', + 'log_no_adb': 'adb not found. Place ADB files in this folder.', + 'log_no_fastboot': 'Runtime environment is incomplete', + 'log_vin': 'VIN: {vin}', + 'log_vin_unavailable': 'Unable to read VIN. Confirm the device is in factory mode.', + 'log_auth_checking': 'Checking authorization...', + 'log_auth_success': 'Authorization passed', + 'log_auth_failed': 'Authorization failed', + 'log_debug_skip_auth': 'Debug mode: skipping authorization', + 'log_vehicle_name': 'Vehicle name: {vehicle}', + 'log_data_prepare_failed': 'Resource initialization failed', + 'log_boot_challenge_failed': 'Permission failed', + 'log_boot_key_failed': 'Permission failed', + 'log_boot_key_len_error': 'Permission failed', + 'log_boot_key_success': 'Getting permission', + 'log_crypto_missing': 'Permission failed', + 'log_driver_found': 'Driver environment detected', + 'log_driver_missing': 'Driver environment not detected', + 'log_driver_install_start': 'Installing driver environment...', + 'log_driver_install_success': 'Driver environment installed', + 'log_driver_install_failed': 'Driver environment installation failed: {error}', + 'log_runtime_missing': 'Permission failed', + 'log_base_apk_invalid': 'Permission failed', + 'log_runtime_ready': 'Getting permission', + 'log_runtime_install_failed': 'Permission failed', + 'log_runtime_install_success': 'Getting permission', + 'log_permission_resource_missing': 'Permission failed', + 'log_permission_resource_format_unsupported': 'Permission failed', + 'log_permission_resource_algorithm_unsupported': 'Permission failed', + 'log_permission_resource_decrypt_auth_failed': 'Permission failed', + 'log_permission_resource_decrypt_ready': 'Getting permission', + 'log_permission_resource_decrypt_failed': 'Permission failed', + 'log_fastboot_wait': 'Getting permission. Do not close the program or disconnect the data cable.', + 'log_fastboot_missing': 'Permission failed', + 'log_fastboot_enter_failed': 'Permission failed', + 'log_init_boot_flash_failed': 'Permission failed', + 'log_init_boot_flash_success': 'Getting permission', + 'log_fastboot_reboot_failed': 'Permission failed', + 'log_permission_success': 'Permission acquired', + 'log_permission_failed': 'Permission failed', + 'log_temp_img_deleted': 'Temporary file cleaned', + 'log_temp_img_delete_failed': 'Failed to clean temporary file', + 'log_open_magisk': 'Tap "允许" to grant permission when prompted.', + 'log_root_checking': 'Checking permission...', + 'log_root_ok': 'Permission granted', + 'log_root_failed': 'Permission grant failed. Grant permission manually.', + 'msg_root_failed': 'Grant permission manually, then retry.', + 'log_magisk_cleanup_done': 'Permission entry cleaned', + 'log_magisk_cleanup_failed': 'Permission entry cleanup failed', + 'log_magisk_cleanup_module_done': 'Permission entry cleanup persisted', + 'log_magisk_cleanup_module_failed': 'Permission entry cleanup persistence failed', + 'log_voice_patch_start': 'Starting voice assistant patch install', + 'log_voice_patch_done': 'Voice assistant patch installed. Reboot the device to apply.', + 'log_voice_patch_failed': 'Voice assistant patch install failed', + 'log_module_install_start': 'Starting patch install', + 'log_module_install_done': 'Patch installed', + 'log_module_install_failed': 'Patch install failed: {error}', + 'log_push_file_failed': 'File push failed: {file}: {error}', + 'log_root_cmd_failed': 'Permission operation failed: {error}', + 'err_module_prop_missing': 'Patch files are missing from the resource package', + 'err_module_id_missing': 'Patch config is missing an ID field', + 'err_module_zip_invalid': 'Patch file is invalid or missing config: {file}', + 'err_duplicate_module_id': 'The resource package contains duplicate patch IDs', + 'log_need_adb': 'Connect the device first.', + 'log_no_vehicle_name': 'Resource initialization failed', + 'log_package_missing': 'Resource file is missing', + 'log_extract_password_missing': 'Resource initialization failed', + 'log_7za_missing': 'Runtime environment is incomplete', + 'log_extracting': 'Preparing resources...', + 'log_apps_missing': 'Warning: apps directory not found', + 'log_resource_invalid': 'Resource validation failed', + 'log_resource_ready': 'Resources ready', + 'log_resource_failed': 'Resource preparation failed. Check the network and try again.', + 'log_cache_invalid': 'Resource cache is invalid', + 'log_no_language_files': 'No language package files found', + 'log_flash_start': 'Starting language package flashing. Do not power off or restart the computer or vehicle head unit.', + 'log_install_success': 'Install succeeded', + 'log_install_failed': 'Install failed', + 'log_flash_item_failed': 'Language package flash failed: {current}/{total}', + 'log_flash_done_config': 'Language packages flashed. Running Mazda-EZ60 post-install configuration.', + 'log_flash_partial_config': 'Some language packages flashed. Continuing Mazda-EZ60 post-install configuration.', + 'log_flash_failed_config': 'Language package flashing failed. Still running Mazda-EZ60 post-install configuration.', + 'log_post_config_start': 'Running Mazda-EZ60 post-install configuration...', + 'log_overlay_enabled': 'Configuration item completed', + 'log_overlay_failed': 'Configuration item failed', + 'log_disabled_package': 'Cleanup item completed', + 'log_disable_package_failed': 'Cleanup item failed', + 'log_post_config_done': 'Mazda-EZ60 post-install configuration complete', + 'log_post_config_all_done': 'Mazda-EZ60 post-install configuration complete. Reboot the device to apply.', + 'log_post_config_partial': 'Mazda-EZ60 post-install configuration partly failed. Check the log.', + 'log_batch_install_start': 'Starting batch install for {count} APK file(s)...', + 'log_install_many_start': 'Starting install for {count} APK file(s)...', + 'log_install_done_all': 'Install complete: all {count} succeeded', + 'log_install_done_partial': 'Install complete: {success}/{count} succeeded', + 'log_install_failed_simple': 'Install failed', + 'log_rebooting': 'Device is rebooting...', + 'log_disable_ota_cancelled': 'Disable OTA operation cancelled', + 'log_disable_ota_success': 'System OTA disabled', + 'log_disable_ota_failed': 'Disable OTA failed', + 'log_pwd_success': 'Password query succeeded', + 'log_pwd_failed': 'Password query failed', + 'log_pwd_request_failed': 'Password query request failed', + 'log_debug_off': 'Debug mode disabled', + 'log_debug_on': 'Debug mode enabled - authorization/device checks skipped, detailed ADB logs shown', + 'log_debug_extract_start': 'Starting package extract test...', + 'log_debug_extract_success': 'Package extract test passed: APK count={apk_count}, patch count={module_count}', + 'log_debug_extract_failed': 'Package extract test failed', + 'log_debug_boot_start': 'Starting boot resource extract test...', + 'log_debug_boot_success': 'Boot resource extract test passed: size={size}, sha256={sha}', + 'log_debug_boot_failed': 'Boot resource extract test failed', + 'log_env_config_failed_admin': 'Environment configuration failed. Run as administrator.', + 'log_env_config_failed': 'Environment configuration failed', + 'log_env_config_failed_detail': 'Environment configuration failed: {error}', + 'log_hotspot_opening': 'Opened hotspot settings. Detecting hotspot...', + 'log_hotspot_detected': 'Hotspot detected: {ssid} / {password}', + 'log_hotspot_not_detected': 'Hotspot not detected. Make sure it is turned on.', + 'err_extract_wrong_password': 'Resource preparation failed', + 'err_extract_data': 'Resource preparation failed', + 'err_extract_headers': 'Resource preparation failed', + 'err_extract_detail': 'Resource preparation failed', + 'err_extract_code': 'Resource preparation failed', + 'err_missing_apps': 'Resource directory is invalid', + 'err_empty_apps': 'Resource directory is invalid', + 'err_zero_apks': 'Resource file is invalid', + 'err_cmd_timeout': 'Command timed out', + 'err_fastboot_timeout': 'Permission failed', + 'err_push_failed': 'push failed: {error}', + 'err_install_failed': 'install failed: {error}', + 'err_decode_failed': 'Decode failed', + 'msg_start_failed_title': 'Error', + 'msg_start_failed': 'Program failed to start: {error}', + 'print_python_required': 'Error: Python 3.6 or later is required', + 'print_start_failed': 'Startup failed: {error}', + } + } + + self.base_dir = get_app_dir() + self.adb = find_tool('adb.exe', 'adb') + self.fastboot = find_tool('fastboot.exe', 'fastboot') + self.sz = find_tool('7za.exe') + self.driver_dir = find_resource_dir("usb_driver") + self.driver_inf = self.driver_dir / "android_winusb.inf" + self.package_file = find_resource("package_voice-assistant.bin") + self.runtime_file = find_resource("runtime.dat") + self.permission_resource_file = find_resource("EZ60_resource.dat") + self.extract_password = None + self.runtime_password = None + self.permission_resource_key = None + self.apps_dir = None + self.voice_module_zips = [] + self.temp_dir = None + self.runtime_cache_dir = None + self.api_url = "https://api.changan.softwindy.cn/api/authorizations/auth-check" + self.boot_challenge_api_url = "https://api.changan.softwindy.cn/api/authorizations/boot-challenge" + self.boot_key_api_url = "https://api.changan.softwindy.cn/api/authorizations/boot-key" + self.debug_password_api_url = "https://api.changan.softwindy.cn/api/authorizations/verify-debug-mode-password" + self.tool_version = "Mazda-EZ60_1.2/1.2.0" + self.vin = None + self.vehicle_name = "" + self.device_connected = False + self._refreshing = False # 防止并发刷新 + self.debug_mode = False # 调试模式 + self.driver_prompted = False + self.voice_patch_module_names = [ + "enable_install.zip", + "MazdaEZ60VoiceEnglish-1.2-Aemeth.zip", + ] + self.mazda_overlay_packages = [ + "com.tinnove.launcher.overlay", + "com.tinnove.scenemode.overlay", + "com.incall.dvr.overlay", + ] + self.mazda_disable_packages = [ + "com.carinno.p1", + "com.wtcl.electronicdirections", + "com.ximalaya.ting.android.car", + "com.tinnove.netease.music", + "com.migu.miguplay.car", + "cn.cmvideo.car.play", + "com.tinnove.carshow", + "com.tinnove.changba", + "com.qiyi.video.iv", + "com.changan.appmarket", + "com.incall.apps.softmanager" + ] + atexit.register(self.cleanup_cache_on_exit) + + # 设置样式 + self.setup_styles() + self.setup_ui() + self.root.protocol("WM_DELETE_WINDOW", self.on_close) + self.center_window() + self._clear_extracted_cache() + + # 检查环境 + 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) + + # 左侧内容区 + left_frame = tk.Frame(main_frame, bg=self.colors['bg_dark']) + left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # 右侧提示面板 + right_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1, width=235) + right_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=(10, 0)) + right_frame.pack_propagate(False) + + # 顶部标题栏 + title_frame = tk.Frame(left_frame, bg=self.colors['bg_dark'], height=65) + title_frame.pack(fill=tk.X, pady=(0, 10)) + title_frame.pack_propagate(False) + + title_content_frame = tk.Frame(title_frame, bg=self.colors['bg_dark']) + title_content_frame.pack(fill=tk.X, expand=True) + + # 标题 + self.title_label = tk.Label(title_content_frame, + text="🚀 " + self.t('title'), + font=('Microsoft YaHei', 18, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.title_label.pack(side=tk.LEFT, expand=True, padx=(0, 10)) + + self.btn_lang_switch = tk.Button(title_content_frame, text=self.t('lang_en'), + command=self.toggle_lang, + font=('Microsoft YaHei', 9, 'bold'), + fg='white', + bg=self.colors['accent'], + activeforeground='white', + activebackground=self.colors['accent_hover'], + relief=tk.FLAT, + cursor='hand2', + width=7, + height=1) + self.btn_lang_switch.pack(side=tk.RIGHT, padx=(8, 4)) + + # 工程密码查询区域 + pwd_query_frame = tk.Frame(left_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + pwd_query_frame.pack(fill=tk.X, pady=(0, 5), padx=5) + + pwd_query_row = tk.Frame(pwd_query_frame, bg=self.colors['bg_light']) + pwd_query_row.pack(fill=tk.X) + + self.pwd_query_label = tk.Label(pwd_query_row, text=self.t('pwd_query_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.pwd_query_label.pack(side=tk.LEFT, padx=(10, 5), pady=5) + + self.vin_input = tk.Entry(pwd_query_row, + font=('Consolas', 9), + bg='#2d2d3d', + fg='#636e72', + insertbackground='white', + relief=tk.FLAT, + width=20) + self.vin_input.insert(0, self.t('vin_placeholder')) + self.vin_input.bind("", 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_row, text=self.t('btn_query_pwd'), + command=self.query_password_by_vin, + font=('Microsoft YaHei', 8), + fg='white', + bg=self.colors['accent'], + relief=tk.FLAT, + cursor='hand2') + self.btn_query_pwd.pack(side=tk.LEFT, padx=5, pady=5) + + self.pwd_result_label = tk.Label(pwd_query_row, 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) + + self.vin_query_hint_label = tk.Label(pwd_query_frame, text=self.t('vin_query_hint'), + font=('Microsoft YaHei', 8, 'bold'), + fg=self.colors['warning'], + bg=self.colors['bg_light'], + anchor='w') + self.vin_query_hint_label.pack(fill=tk.X, padx=(10, 10), pady=(0, 6)) + + + # 工厂模式提示 + factory_hint_frame = tk.Frame(left_frame, bg=self.colors['bg_dark']) + factory_hint_frame.pack(fill=tk.X, pady=(0, 3)) + self.hint_label = tk.Label(factory_hint_frame, text=self.t('hint_factory'), + font=('Microsoft YaHei', 8), + fg=self.colors['warning'], + bg=self.colors['bg_dark']) + self.hint_label.pack(side=tk.LEFT, padx=2) + + # 按钮区域(两排,每排5个) + button_frame = tk.Frame(left_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + button_frame.pack(fill=tk.X, pady=(0, 10), padx=5) + + # 按钮样式参数 + btn_params = { + 'font': ('Microsoft YaHei', 9), + 'fg': 'white', + 'relief': tk.FLAT, + 'cursor': 'hand2', + 'height': 1, + 'width': 12 + } + + # 第一排按钮 + row1_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + row1_frame.pack(pady=(8, 4)) + + self.btn_permission = tk.Button(row1_frame, text=self.t('btn_permission'), + command=self.prepare_ez60_permission, + bg=self.colors['warning'], + **btn_params) + self.btn_permission.pack(side=tk.LEFT, padx=4) + + self.btn_voice_patch = tk.Button(row1_frame, text=self.t('btn_voice_patch'), + command=self.install_voice_assistant_patch, + bg=self.colors['accent'], + **btn_params) + self.btn_voice_patch.pack(side=tk.LEFT, padx=4) + + self.btn_push = tk.Button(row1_frame, text=self.t('btn_push'), + command=self.push_all_apks, + bg=self.colors['accent'], + **btn_params) + self.btn_push.pack(side=tk.LEFT, padx=4) + + self.btn_install_all = tk.Button(row1_frame, text=self.t('btn_install'), + command=self.install_apps, + bg=self.colors['accent'], + **btn_params) + self.btn_install_all.pack(side=tk.LEFT, padx=4) + + self.btn_language = tk.Button(row1_frame, text=self.t('btn_language'), + command=self.open_language_quick_set, + bg=self.colors['accent'], + **btn_params) + self.btn_language.pack(side=tk.LEFT, padx=4) + + # 第二排按钮 + row2_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + row2_frame.pack(pady=(4, 8)) + + self.btn_timezone = tk.Button(row2_frame, text=self.t('btn_timezone'), + command=self.open_timezone_settings, + bg=self.colors['accent'], + **btn_params) + self.btn_timezone.pack(side=tk.LEFT, padx=4) + + self.btn_settings = tk.Button(row2_frame, text=self.t('btn_settings'), + command=self.open_android_settings, + bg=self.colors['accent'], + **btn_params) + self.btn_settings.pack(side=tk.LEFT, padx=4) + + self.btn_reboot = tk.Button(row2_frame, text=self.t('btn_reboot'), + command=self.reboot_device, + bg=self.colors['warning'], + **btn_params) + self.btn_reboot.pack(side=tk.LEFT, padx=4) + + self.btn_exit = tk.Button(row2_frame, text=self.t('btn_disable_upgrade'), + command=self.on_disable_upgrade, + bg=self.colors['error'], + **btn_params) + self.btn_exit.pack(side=tk.LEFT, padx=4) + + self.debug_button_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + + self.btn_debug_extract = tk.Button(self.debug_button_frame, text=self.t('btn_debug_extract'), + command=self.debug_test_package_extract, + bg=self.colors['info'], + **btn_params) + self.btn_debug_extract.pack(side=tk.LEFT, padx=4) + + self.btn_debug_boot = tk.Button(self.debug_button_frame, text=self.t('btn_debug_boot'), + command=self.debug_test_boot_extract, + bg=self.colors['info'], + **btn_params) + self.btn_debug_boot.pack(side=tk.LEFT, padx=4) + + # 设备状态栏(横条) + status_bar_frame = tk.Frame(left_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + status_bar_frame.pack(fill=tk.X, pady=(0, 5)) + + # 状态指示器 + status_indicator_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + status_indicator_frame.pack(side=tk.LEFT, padx=10, pady=5) + + self.status_indicator = tk.Canvas(status_indicator_frame, width=10, height=10, + bg=self.colors['bg_light'], highlightthickness=0) + self.status_indicator.pack(side=tk.LEFT) + self.status_dot = self.status_indicator.create_oval(2, 2, 8, 8, fill='#636e72') + + self.device_label = tk.Label(status_indicator_frame, text=self.t('device_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.device_label.pack(side=tk.LEFT, padx=(5, 3)) + + self.device_status_label = tk.Label(status_indicator_frame, text=self.t('status_detecting'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light']) + self.device_status_label.pack(side=tk.LEFT) + + # VIN信息 + vin_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + vin_frame.pack(side=tk.LEFT, padx=20, pady=5) + self.vin_label_title = tk.Label(vin_frame, text=self.t('vin_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.vin_label_title.pack(side=tk.LEFT) + self.vin_label = tk.Label(vin_frame, text=self.t('vin_none'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light']) + self.vin_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 授权状态 + auth_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + auth_frame.pack(side=tk.LEFT, padx=20, pady=5) + self.auth_label_title = tk.Label(auth_frame, text=self.t('auth_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.auth_label_title.pack(side=tk.LEFT) + self.auth_label = tk.Label(auth_frame, text=self.t('auth_none'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light']) + self.auth_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 操作按钮:检查靠近设备状态,安装驱动在检查右侧 + status_actions_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + status_actions_frame.pack(side=tk.RIGHT, padx=10, pady=5) + + self.btn_refresh = tk.Button(status_actions_frame, text=self.t('btn_refresh'), + command=lambda: self.refresh_device_status(force=True), + font=('Microsoft YaHei', 8), + fg=self.colors['accent'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + self.btn_refresh.pack(side=tk.LEFT, padx=(0, 8)) + + self.btn_install_driver = tk.Button(status_actions_frame, text=self.t('btn_install_driver'), + command=self.install_fastboot_driver, + font=('Microsoft YaHei', 8), + fg=self.colors['warning'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + self.btn_install_driver.pack(side=tk.LEFT) + + # 解压进度条框架 + progress_frame = tk.Frame(left_frame, bg=self.colors['bg_dark']) + progress_frame.pack(fill=tk.X, pady=(5, 5)) + + self.progress_label = tk.Label(progress_frame, text="", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + self.progress_label.pack() + + self.progress = ttk.Progressbar(progress_frame, mode='determinate', style='TProgressbar') + self.progress.pack(fill=tk.X, pady=(2, 0)) + + # 推送进度条 + self.push_progress_label = tk.Label(progress_frame, text="", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + + self.push_progress = ttk.Progressbar(progress_frame, mode='determinate', style='TProgressbar') + + # 日志区域(下方) + log_card = tk.Frame(left_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + log_card.pack(fill=tk.BOTH, expand=True, pady=(5, 0)) + + # 日志标题栏 + log_title_frame = tk.Frame(log_card, bg=self.colors['bg_dark'], height=30) + log_title_frame.pack(fill=tk.X) + log_title_frame.pack_propagate(False) + + self.log_title_label = tk.Label(log_title_frame, text=self.t('log_title'), + font=('Microsoft YaHei', 10, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.log_title_label.pack(side=tk.LEFT, padx=10) + + self.btn_clear = tk.Button(log_title_frame, text=self.t('btn_clear_log'), + command=self.clear_log, + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark'], + relief=tk.FLAT, + cursor='hand2') + self.btn_clear.pack(side=tk.RIGHT, padx=10) + + # 日志文本框 + text_frame = tk.Frame(log_card, bg=self.colors['bg_light']) + text_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + self.log_text = scrolledtext.ScrolledText(text_frame, + height=12, + wrap=tk.WORD, + font=('Consolas', 9), + bg='#2d2d3d', + fg='#e0e0e0', + insertbackground='white', + relief=tk.FLAT, + borderwidth=0) + self.log_text.pack(fill=tk.BOTH, expand=True) + + # 配置日志颜色标签 + self.log_text.tag_config('INFO', foreground='#74b9ff') + self.log_text.tag_config('SUCCESS', foreground='#55efc4') + self.log_text.tag_config('ERROR', foreground='#ff7675') + self.log_text.tag_config('WARNING', foreground='#ffeaa7') + self.log_text.tag_config('CMD', foreground='#a29bfe') + + # 底部状态栏 + bottom_status = tk.Frame(left_frame, bg=self.colors['bg_light'], height=22) + bottom_status.pack(fill=tk.X, pady=(5, 0)) + bottom_status.pack_propagate(False) + + self.status_text = tk.Label(bottom_status, text=self.t('status_ready'), + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light']) + self.status_text.pack(side=tk.LEFT, padx=10) + + # 主题切换按钮 + self.btn_theme_switch = tk.Button(bottom_status, text=self.t('theme_dark'), + 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.root.bind('', self._toggle_debug) + + # ========== 右侧提示面板 ========== + # 热点信息卡片 + hotspot_card = tk.Frame(right_frame, bg=self.colors['bg_dark'], relief=tk.RAISED, bd=1) + hotspot_card.pack(fill=tk.X, padx=5, pady=(10, 5)) + + hotspot_title_row = tk.Frame(hotspot_card, bg=self.colors['bg_dark']) + hotspot_title_row.pack(anchor='w', fill=tk.X, padx=10, pady=(8, 5)) + + self.hotspot_icon_label = tk.Label(hotspot_title_row, text=self.t('hotspot_icon'), + font=('Segoe UI Emoji', 12), + fg=self.colors['accent'], + bg=self.colors['bg_dark'], + width=2, + anchor='center') + self.hotspot_icon_label.pack(side=tk.LEFT, padx=(0, 4)) + + self.hotspot_title_label = tk.Label(hotspot_title_row, text=self.t('hotspot_title'), + font=('Microsoft YaHei', 11, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark'], + anchor='w') + self.hotspot_title_label.pack(side=tk.LEFT, fill=tk.X, expand=True) + + self.hotspot_ssid_label = tk.Label(hotspot_card, text=self.t('hotspot_name_detecting'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_dark']) + self.hotspot_ssid_label.pack(anchor='w', padx=10, pady=2) + + self.hotspot_pwd_label = tk.Label(hotspot_card, text=self.t('hotspot_pwd_default'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_dark']) + self.hotspot_pwd_label.pack(anchor='w', padx=10, pady=2) + + self.hotspot_status_label = tk.Label(hotspot_card, text=self.t('hotspot_status_off'), + font=('Microsoft YaHei', 9), + fg=self.colors['warning'], + bg=self.colors['bg_dark']) + self.hotspot_status_label.pack(anchor='w', padx=10, pady=2) + + self.btn_hotspot = tk.Button(hotspot_card, text=self.t('hotspot_start'), + command=self.start_hotspot_action, + font=('Microsoft YaHei', 8), + fg='white', + bg=self.colors['accent'], + relief=tk.FLAT, + cursor='hand2') + self.btn_hotspot.pack(pady=8, padx=10, fill=tk.X) + + # 分隔线 + tk.Frame(right_frame, bg=self.colors['border'], height=1).pack(fill=tk.X, padx=8, pady=5) + + # 使用提示卡片 + hint_card = tk.Frame(right_frame, bg=self.colors['bg_dark'], relief=tk.RAISED, bd=1) + hint_card.pack(fill=tk.X, padx=5, pady=5) + + hint_title_row = tk.Frame(hint_card, bg=self.colors['bg_dark']) + hint_title_row.pack(anchor='w', fill=tk.X, padx=10, pady=(8, 5)) + + self.hint_icon_label = tk.Label(hint_title_row, text=self.t('hint_icon'), + font=('Segoe UI Emoji', 12), + fg=self.colors['warning'], + bg=self.colors['bg_dark'], + width=2, + anchor='center') + self.hint_icon_label.pack(side=tk.LEFT, padx=(0, 4)) + + self.hint_title_label = tk.Label(hint_title_row, text=self.t('hint_title'), + font=('Microsoft YaHei', 11, 'bold'), + fg=self.colors['warning'], + bg=self.colors['bg_dark'], + anchor='w') + self.hint_title_label.pack(side=tk.LEFT, fill=tk.X, expand=True) + + self.hint_lines_frame = tk.Frame(hint_card, bg=self.colors['bg_dark']) + self.hint_lines_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) + self._render_hint_lines() + + # 绑定悬停效果 + self.bind_hover_effects() + self.set_debug_buttons_visible(False) + + def bind_hover_effects(self): + """绑定按钮悬停效果""" + buttons = [self.btn_permission, self.btn_voice_patch, 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, + self.btn_hotspot, self.btn_debug_extract, self.btn_debug_boot] + + 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 set_window_icon(self): + """Set the Tk window/taskbar icon at runtime; PyInstaller --icon only sets the exe file icon.""" + try: + icon_path = find_resource("app.ico") + if icon_path.exists(): + self.root.iconbitmap(str(icon_path)) + if sys.platform == 'win32': + import ctypes + hwnd = self.root.winfo_id() + image = ctypes.windll.user32.LoadImageW( + None, str(icon_path), 1, 0, 0, 0x00000010 + ) + if image: + ctypes.windll.user32.SendMessageW(hwnd, 0x0080, 0, image) + ctypes.windll.user32.SendMessageW(hwnd, 0x0080, 1, image) + except Exception: + pass + + def center_window(self): + """将窗口居中显示在屏幕上""" + self.root.update_idletasks() + screen_w = self.root.winfo_screenwidth() + screen_h = self.root.winfo_screenheight() + win_w = self.root.winfo_reqwidth() + win_h = self.root.winfo_reqheight() + x = (screen_w - win_w) // 2 + y = (screen_h - win_h) // 2 + self.root.geometry(f"+{x}+{y}") + + def run_on_ui_thread(self, func, *args, **kwargs): + """将函数调度到主线程执行,确保线程安全""" + self.root.after(0, lambda: func(*args, **kwargs)) + + def t(self, key): + return self.T.get(self.lang, self.T['zh']).get(key, key) + + def tf(self, key, **kwargs): + try: + return self.t(key).format(**kwargs) + except Exception: + return self.t(key) + + def is_placeholder_vin(self, value): + return value in ( + self.T['zh'].get('vin_placeholder'), + self.T['en'].get('vin_placeholder'), + ) + + def toggle_lang(self): + self.lang = 'en' if self.lang == 'zh' else 'zh' + self.btn_lang_switch.config(text=self.t('lang_en') if self.lang == 'zh' else self.t('lang_zh')) + self._refresh_ui_texts() + self.log(self.t('log_lang_changed'), "INFO") + + def 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 _render_hint_lines(self): + if not hasattr(self, 'hint_lines_frame'): + return + for child in self.hint_lines_frame.winfo_children(): + child.destroy() + for line in self.t('hint_lines'): + tk.Label(self.hint_lines_frame, + text=line, + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark'], + justify=tk.LEFT, + anchor='w', + wraplength=190).pack(anchor='w', fill=tk.X, pady=1) + + def _refresh_ui_texts(self): + t = self.t + widgets = [ + (getattr(self, 'title_label', None), 'title', None), + (getattr(self, 'pwd_query_label', None), 'pwd_query_label', None), + (getattr(self, 'btn_permission', None), 'btn_permission', None), + (getattr(self, 'btn_voice_patch', None), 'btn_voice_patch', None), + (getattr(self, 'btn_push', None), 'btn_push', None), + (getattr(self, 'btn_install_all', None), 'btn_install', None), + (getattr(self, 'btn_language', None), 'btn_language', None), + (getattr(self, 'btn_timezone', None), 'btn_timezone', None), + (getattr(self, 'btn_settings', None), 'btn_settings', None), + (getattr(self, 'btn_reboot', None), 'btn_reboot', None), + (getattr(self, 'btn_exit', None), 'btn_disable_upgrade', None), + (getattr(self, 'btn_clear', None), 'btn_clear_log', None), + (getattr(self, 'btn_query_pwd', None), 'btn_query_pwd', None), + (getattr(self, 'btn_debug_extract', None), 'btn_debug_extract', None), + (getattr(self, 'btn_debug_boot', None), 'btn_debug_boot', 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, 'btn_install_driver', None), 'btn_install_driver', None), + (getattr(self, 'hint_label', None), 'hint_factory', None), + (getattr(self, 'vin_query_hint_label', None), 'vin_query_hint', None), + (getattr(self, 'hotspot_icon_label', None), 'hotspot_icon', None), + (getattr(self, 'hotspot_title_label', None), 'hotspot_title', None), + (getattr(self, 'btn_hotspot', None), 'hotspot_start', None), + (getattr(self, 'hint_icon_label', None), 'hint_icon', None), + (getattr(self, 'hint_title_label', None), 'hint_title', None), + ] + for w, key, _ in widgets: + if not w: + continue + text = t(key) + if key == 'title': + text = "🚀 " + text + w.config(text=text) + self.btn_theme_switch.config(text=t('theme_light') if self.theme == 'dark' else t('theme_dark')) + self.btn_lang_switch.config(text=t('lang_en') if self.lang == 'zh' else t('lang_zh')) + if self.is_placeholder_vin(self.vin_input.get()): + self.vin_input.delete(0, tk.END) + self.vin_input.insert(0, t('vin_placeholder')) + self._render_hint_lines() + self.refresh_hotspot_display() + if self.vin: + self._update_device_status_impl(self.device_connected, self.vin, + getattr(self, '_last_authorized', False)) + + def _sanitize_user_log_message(self, message): + """Hide low-level commands, paths, package names, and APK names in normal logs. + VIN and vehicle names are operator-facing identifiers and are intentionally kept visible. + """ + text = str(message) + replacements = [ + (r'com\.[\w.\-]+', '相关应用'), + (r'cn\.[\w.\-]+', '相关应用'), + (r'[\w.\-]+\.apk', '文件'), + (r'[\w.\-]+\.img', '文件'), + (r'EZ60_resource\.dat', '资源文件'), + (r'package\.bin', '资源文件'), + (r'7za(?:\.exe)?', '资源工具'), + (r'adb(?:\.exe)?', '设备连接工具'), + (r'fastboot(?:\.exe)?', '设备工具'), + (r'/debug_ramdisk/su(?:\s+-c)?', '权限操作'), + (r'MazdaEZ60VoiceEnglish-1\.2-Aemeth\.zip', '补丁文件'), + (r'enable_install\.zip', '补丁文件'), + (r'[\w.\-]+\.zip', '补丁文件'), + (r'module\.prop', '补丁配置'), + (r'/data/adb/modules/[A-Za-z0-9_.-]+', '补丁目录'), + (r'/data/local/tmp/[A-Za-z0-9_./-]+', '临时目录'), + (r'init_boot', '系统资源'), + (r'pm\s+\S+', '系统操作'), + (r'cmd\s+overlay\s+\S+', '系统配置'), + (r'(?/dev/null 2>&1 + cmd package uninstall -k com.topjohnwu.magisk >/dev/null 2>&1 + for user in 0 $(pm list users 2>/dev/null | sed -n 's/.*{\([0-9][0-9]*\):.*/\1/p'); do + pm uninstall -k --user "$user" com.topjohnwu.magisk >/dev/null 2>&1 + pm uninstall --user "$user" com.topjohnwu.magisk >/dev/null 2>&1 + pm disable-user --user "$user" com.topjohnwu.magisk >/dev/null 2>&1 + done + rm -f /data/adb/magisk.apk /data/adb/stub.apk /data/adb/manager.apk >/dev/null 2>&1 + rm -f /data/adb/magisk/magisk.apk /data/adb/magisk/stub.apk /data/adb/magisk/manager.apk >/dev/null 2>&1 + sleep 10 + done +) & +""" + stage_dir = "/data/local/tmp/ez60_magisk_manager_cleanup" + with tempfile.TemporaryDirectory(prefix="ez60_magisk_cleanup_") as tmp: + local_prop = Path(tmp) / "module.prop" + local_service = Path(tmp) / "service.sh" + local_prop.write_text(module_prop, encoding="utf-8") + local_service.write_text(service_sh, encoding="utf-8", newline="\n") + + commands = [ + f"rm -rf {self.shell_quote(module_dir)} {self.shell_quote(stage_dir)}", + f"mkdir -p {self.shell_quote(module_dir)} {self.shell_quote(stage_dir)}", + f"chmod 777 {self.shell_quote(stage_dir)}", + ] + for command in commands: + ok, output = self.run_root_command(command, timeout=60) + if not ok: + if self.debug_mode: + if output: + self.log(f"MAGISK CLEANUP MODULE: {output}", "CMD") + self.log(self.t('log_magisk_cleanup_module_failed'), "WARNING") + return False + + for local_path, remote_name in ((local_prop, "module.prop"), (local_service, "service.sh")): + ok, output = self.run_adb_command(f'adb -d push "{local_path}" {stage_dir}/{remote_name}') + if not ok: + if self.debug_mode: + if output: + self.log(f"MAGISK CLEANUP MODULE PUSH: {output}", "CMD") + self.log(self.t('log_magisk_cleanup_module_failed'), "WARNING") + return False + + commands = [ + f"cp -f {self.shell_quote(stage_dir + '/module.prop')} {self.shell_quote(module_dir + '/module.prop')}", + f"cp -f {self.shell_quote(stage_dir + '/service.sh')} {self.shell_quote(module_dir + '/service.sh')}", + f"chmod 755 {self.shell_quote(module_dir)}", + f"chmod 644 {self.shell_quote(module_dir + '/module.prop')}", + f"chmod 755 {self.shell_quote(module_dir + '/service.sh')}", + f"rm -rf {self.shell_quote(stage_dir)}", + ] + for command in commands: + ok, output = self.run_root_command(command, timeout=60) + if not ok: + if self.debug_mode: + if output: + self.log(f"MAGISK CLEANUP MODULE: {output}", "CMD") + self.log(self.t('log_magisk_cleanup_module_failed'), "WARNING") + return False + if self.debug_mode: + self.log(self.t('log_magisk_cleanup_module_done'), "INFO") + return True + + def read_module_id_from_zip(self, zip_path): + try: + with zipfile.ZipFile(zip_path, 'r') as zf: + prop_name = self._find_module_prop_in_zip(zf) + if not prop_name: + return "" + raw = zf.read(prop_name) + for line in raw.decode('utf-8', errors='replace').splitlines(): + line = line.strip() + if line.startswith("id="): + return line.split("=", 1)[1].strip() + except Exception: + return "" + return "" + + def _find_module_prop_in_zip(self, zf): + names = zf.namelist() + normalized = {} + for name in names: + clean = name.replace("\\", "/").lstrip("./") + normalized[clean] = name + if "module.prop" in normalized: + return normalized["module.prop"] + + candidates = [] + for clean, original in normalized.items(): + parts = [part for part in clean.split("/") if part] + if len(parts) == 2 and parts[-1] == "module.prop": + candidates.append((len(parts), original)) + if candidates: + candidates.sort() + return candidates[0][1] + return "" + + def install_magisk_module_zip(self, zip_path): + zip_path = Path(zip_path) + mod_id = self.read_module_id_from_zip(zip_path) + if not mod_id: + return False, self.t('err_module_id_missing') + if not zip_path.exists(): + return False, self.tf('err_extract_detail', error=f"{zip_path} not found") + + device_stage_dir = "/data/local/tmp/ez60_voice_modules" + device_zip = f"{device_stage_dir}/{mod_id}.zip" + device_module = f"/data/adb/modules/{mod_id}" + + self.log(self.t('log_module_install_start'), "INFO") + commands = [ + f"rm -rf {self.shell_quote(device_module)}", + f"mkdir -p {self.shell_quote(device_stage_dir)} {self.shell_quote(device_module)}", + f"chmod 777 {self.shell_quote(device_stage_dir)}", + ] + for command in commands: + ok, output = self.run_root_command(command, timeout=60) + if not ok: + return False, self.tf('log_root_cmd_failed', error=output) + + ok, output = self.run_adb_command(f'adb -d push "{zip_path}" {device_zip}') + if not ok: + return False, self.tf('log_push_file_failed', file=zip_path.name, error=output) + + install_commands = [ + f"unzip -oq {self.shell_quote(device_zip)} -x 'META-INF/*' -d {self.shell_quote(device_module)}", + f"find {self.shell_quote(device_module)} -type d -exec chmod 755 {{}} \\;", + f"find {self.shell_quote(device_module)} -type f -exec chmod 644 {{}} \\;", + f"find {self.shell_quote(device_module)} -type f -name '*.sh' -exec chmod 755 {{}} \\;", + f"rm -f {self.shell_quote(device_zip)}", + ] + for command in install_commands: + ok, output = self.run_root_command(command, timeout=300) + if not ok: + return False, self.tf('log_root_cmd_failed', error=output) + + self.log(self.t('log_module_install_done'), "INFO") + return True, mod_id + + def check_package_extracted(self): + """检查语言包是否已解压""" + has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0 + if has_app: + ok, reason = self._validate_extracted_apks() + if not ok: + self.log(self.tf('log_cache_invalid', reason=reason), "ERROR") + self._clear_extracted_cache() + return False + return has_app + + def _validate_extracted_apks(self): + if not self.apps_dir or not self.apps_dir.exists(): + return False, self.t('err_missing_apps') + apks = list(self.apps_dir.glob("*.apk")) + if not apks: + return False, self.t('err_empty_apps') + zero_apks = [apk.name for apk in apks if apk.stat().st_size <= 0] + if zero_apks: + preview = ", ".join(zero_apks[:5]) + suffix = "..." if len(zero_apks) > 5 else "" + return False, self.tf('err_zero_apks', files=f"{preview}{suffix}") + return True, "" + + def find_voice_patch_modules(self): + if not self.temp_dir or not self.temp_dir.exists(): + return [] + + search_roots = [] + if self.apps_dir and self.apps_dir.exists(): + search_roots.extend([self.apps_dir.parent, self.apps_dir]) + search_roots.append(self.temp_dir) + + found = [] + seen_roots = set() + for module_name in self.voice_patch_module_names: + module_path = None + for root in search_roots: + try: + resolved = Path(root).resolve() + except Exception: + resolved = Path(root) + root_key = (module_name, resolved) + if root_key in seen_roots: + continue + seen_roots.add(root_key) + direct = Path(root) / module_name + if direct.exists(): + module_path = direct + break + if not module_path: + matches = list(self.temp_dir.rglob(module_name)) + if matches: + module_path = matches[0] + if not module_path: + return [] + found.append(module_path) + return found + + def validate_voice_patch_modules(self, module_zips): + if len(module_zips) != len(self.voice_patch_module_names): + return False, self.t('err_module_prop_missing') + module_ids = [] + for zip_path in module_zips: + mod_id = self.read_module_id_from_zip(zip_path) + if not mod_id: + return False, self.tf('err_module_zip_invalid', file=Path(zip_path).name) + module_ids.append(mod_id) + if len(set(module_ids)) != len(module_ids): + return False, self.t('err_duplicate_module_id') + return True, "" + + def _cache_dir_path(self): + local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) + return Path(local_appdata) / ".cache" / "system" / ".android" / self.CACHE_DIR_NAME + + def _runtime_cache_dir_path(self): + local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) + return Path(local_appdata) / ".cache" / "system" / ".android" / "apps_cache_Mazda_EZ60_runtime" + + def _remove_dir_tree(self, path): + if not path or not path.exists(): + return + for _ in range(3): + try: + if sys.platform == 'win32': + subprocess.run( + f'attrib -r -s -h "{path}" /s /d', + shell=True, + capture_output=True, + creationflags=subprocess.CREATE_NO_WINDOW + ) + shutil.rmtree(path, ignore_errors=False) + return + except Exception: + time.sleep(0.3) + shutil.rmtree(path, ignore_errors=True) + + def _clear_extracted_cache(self): + cache_dirs = [] + if self.temp_dir: + cache_dirs.append(self.temp_dir) + if self.runtime_cache_dir: + cache_dirs.append(self.runtime_cache_dir) + cache_dirs.append(self._cache_dir_path()) + cache_dirs.append(self._runtime_cache_dir_path()) + + seen = set() + for cache_dir in cache_dirs: + try: + resolved = cache_dir.resolve() + except Exception: + resolved = cache_dir + if resolved in seen: + continue + seen.add(resolved) + self._remove_dir_tree(cache_dir) + + time.sleep(0.2) + self.apps_dir = None + self.voice_module_zips = [] + self.temp_dir = None + self.runtime_cache_dir = None + + def _schedule_cache_cleanup_after_exit(self): + if sys.platform != 'win32': + return + cache_dir = str(self._cache_dir_path()) + runtime_cache_dir = str(self._runtime_cache_dir_path()) + ps_command = ( + "Start-Sleep -Seconds 2; " + f"$paths = @('{cache_dir}', '{runtime_cache_dir}'); " + "foreach ($p in $paths) { " + "if (Test-Path -LiteralPath $p) { " + "attrib -r -s -h $p /s /d 2>$null; " + "Remove-Item -LiteralPath $p -Recurse -Force -ErrorAction SilentlyContinue " + "} " + "}" + ) + try: + subprocess.Popen( + ['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-Command', ps_command], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + creationflags=subprocess.CREATE_NO_WINDOW + ) + except Exception: + pass + + def cleanup_cache_on_exit(self): + self._clear_extracted_cache() + self._schedule_cache_cleanup_after_exit() + + def on_close(self): + self.cleanup_cache_on_exit() + self.root.destroy() + + def _format_extract_error(self, err_msg, return_code): + text = (err_msg or "").lower() + if any(marker in text for marker in ( + "wrong password", + "incorrect password", + "password is incorrect", + "data error in encrypted file", + "can not open encrypted archive", + )): + return self.t('err_extract_wrong_password') + if "data error" in text: + return self.t('err_extract_data') + if "headers error" in text or "unexpected end" in text: + return self.t('err_extract_headers') + if err_msg.strip(): + return self.tf('err_extract_detail', error=err_msg.strip()[:300]) + return self.tf('err_extract_code', code=return_code) + + def _decode_7z_output(self, *outputs): + """解码 7za 输出,兼容中文 Windows 控制台编码。""" + parts = [] + for output in outputs: + if not output: + continue + for enc in ('gbk', 'utf-8'): + try: + parts.append(output.decode(enc, errors='replace')) + break + except Exception: + continue + return ''.join(parts).strip() + + def _extract_7za_with_progress(self, archive_path=None, output_dir=None, password=DEFAULT_EXTRACT_PASSWORD, progress_cb=None): + """流式运行 7za 并解析百分比输出。""" + archive_path = archive_path or self.package_file + output_dir = output_dir or self.temp_dir + if password is DEFAULT_EXTRACT_PASSWORD: + password = self.extract_password + cmd = [self.sz, 'x', str(archive_path)] + if password: + cmd.append(f'-p{password}') + cmd.extend([f'-o{output_dir}', '-y']) + supports_progress = getattr(self, '_seven_zip_supports_progress_stream', lambda: False)() + if supports_progress: + cmd.extend(['-bsp1', '-bso0', '-bse1']) + + if getattr(self, 'debug_mode', False): + self.log(f"7ZA CMD: {subprocess.list2cmdline(cmd)}", "CMD") + + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.DEVNULL, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + + output = bytearray() + progress_window = 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) + progress_window.extend(chunk) + if len(progress_window) > 1024: + del progress_window[:-1024] + if not self.debug_mode and len(output) > 60000: + del output[:-60000] + + matches = re.findall(rb"(\d{1,3})%", bytes(progress_window[-512:])) + if matches: + percent = min(100, int(matches[-1])) + if percent != last_percent: + last_percent = percent + if progress_cb: + progress_cb(percent, 100, self.t('progress_loading')) + else: + self.update_progress(percent, 100, self.t('progress_loading')) + + return_code = proc.wait() + decoded_output = self._decode_7z_output(bytes(output)) + + if getattr(self, 'debug_mode', False): + self.log(f"7ZA RET: {return_code}", "CMD" if return_code == 0 else "ERROR") + if decoded_output.strip(): + self.log(f"7ZA OUTPUT:\n{decoded_output.strip()}", "CMD" if return_code == 0 else "ERROR") + + return return_code, decoded_output + + def _seven_zip_supports_progress_stream(self): + """检测当前 7za 是否支持进度流参数。""" + try: + result = subprocess.run( + [self.sz], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + output = self._decode_7z_output(result.stdout, result.stderr) + return '-bs{o|e|p}' in output + except Exception: + return False + + def extract_package_silent(self): + """静默解压语言包(带进度)—— 逸动版仅处理 app 目录""" + if not self.package_file.exists(): + self.log(self.tf('log_package_missing', path=self.package_file), "ERROR") + return False + + if not self.extract_password: + self.log(self.t('log_extract_password_missing'), "ERROR") + return False + + if not os.path.exists(self.sz): + self.log(self.tf('log_7za_missing', path=self.sz), "ERROR") + return False + + try: + # 使用用户目录,无需管理员权限 + hidden_path = self._cache_dir_path().parent + hidden_path.mkdir(parents=True, exist_ok=True) + + self.temp_dir = self._cache_dir_path() + + # 如果已存在,先清理 + 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(self.t('log_extracting'), "INFO") + + self.update_progress(0, 100, self.t('progress_loading')) + return_code, err_msg = self._extract_7za_with_progress() + if return_code != 0: + self.log(self._format_extract_error(err_msg, return_code), "ERROR") + self._clear_extracted_cache() + return False + self.update_progress(100, 100, self.t('progress_loaded')) + + # 查找 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(self.t('log_apps_missing'), "WARNING") + self._clear_extracted_cache() + return False + + apk_count = len(list(self.apps_dir.glob("*.apk"))) + ok, reason = self._validate_extracted_apks() + if not ok: + self.log(self.tf('log_resource_invalid', reason=reason), "ERROR") + self._clear_extracted_cache() + return False + self.log(self.tf('log_resource_ready', count=apk_count), "SUCCESS") + return True + + except Exception as e: + if getattr(self, 'debug_mode', False): + self.log(self.tf('log_data_prepare_failed', error=str(e)), "ERROR") + import traceback + self.log(traceback.format_exc(), "ERROR") + else: + self.log(self.t('log_resource_failed'), "ERROR") + self._clear_extracted_cache() + return False + + def check_environment(self): + """检查环境""" + # 修改hosts文件 + self.modify_hosts() + # 刷新热点显示 + self.refresh_hotspot_display() + try: + result = subprocess.run(f'{self.adb} version', shell=True, capture_output=True, text=True) + if result.returncode == 0: + self.refresh_device_status() + if not self.package_file.exists(): + self.log(self.t('log_no_package'), "WARNING") + if not os.path.exists(self.fastboot): + self.log(self.t('log_no_fastboot'), "WARNING") + else: + self.log(self.t('log_no_adb'), "ERROR") + except FileNotFoundError: + self.log(self.t('log_no_adb'), "ERROR") + self.root.after(800, self.check_fastboot_driver_on_startup) + + def _run_command_capture(self, command, timeout=30): + creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + result = subprocess.run( + command, + capture_output=True, + text=True, + timeout=timeout, + creationflags=creationflags + ) + output = (result.stdout or "") + (result.stderr or "") + return result.returncode, output.strip() + + def ask_ok_cancel_on_ui_thread(self, title, message): + result = {"value": False} + done = threading.Event() + + def prompt(): + try: + result["value"] = messagebox.askokcancel(title, message, parent=self.root) + finally: + done.set() + + self.run_on_ui_thread(prompt) + done.wait() + return result["value"] + + def is_fastboot_driver_installed(self): + if sys.platform != 'win32': + return True, "" + try: + code, output = self._run_command_capture(['pnputil', '/enum-drivers'], timeout=40) + if code != 0: + return False, output or "pnputil enum failed" + normalized = output.lower() + installed = ( + 'android_winusb.inf'.lower() in normalized + or 'android bootloader interface' in normalized + or 'android adb interface' in normalized + or 'fastboot' in normalized + ) + return installed, output + except Exception as e: + return False, str(e) + + def _install_driver_with_uac(self, inf_path): + quoted_inf = str(inf_path).replace("'", "''") + ps_command = ( + "$proc = Start-Process -FilePath pnputil " + "-ArgumentList @('/add-driver', '{0}', '/install') " + "-Verb RunAs -WindowStyle Hidden -PassThru; " + "if ($null -eq $proc) {{ exit 1 }}; " + "$proc.WaitForExit(); " + "Write-Output $proc.ExitCode" + ).format(quoted_inf) + creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + result = subprocess.run( + ['powershell', '-NoProfile', '-Command', ps_command], + capture_output=True, + text=True, + creationflags=creationflags + ) + output = ((result.stdout or "") + (result.stderr or "")).strip() + exit_code = None + for line in reversed(output.splitlines()): + text = line.strip() + if text.isdigit(): + exit_code = int(text) + break + if exit_code is None and result.returncode == 0: + exit_code = 0 + return exit_code == 0, output or f"powershell exit={result.returncode}" + + def install_fastboot_driver(self, prompt=True): + def worker(): + try: + if not self.driver_inf.exists(): + detail = f"{self.driver_inf} not found" + self.log(self.tf('log_driver_install_failed', error=detail), "ERROR") + self.run_on_ui_thread( + lambda: messagebox.showerror( + self.t('msg_driver_missing_title'), + self.tf('msg_driver_install_failed', error=detail), + parent=self.root + ) + ) + return + + if prompt: + confirmed = self.ask_ok_cancel_on_ui_thread( + self.t('msg_driver_missing_title'), + self.t('msg_driver_install_confirm') + ) + if not confirmed: + return + + self.log(self.t('log_driver_install_start'), "STATUS") + ok, output = self._install_driver_with_uac(self.driver_inf) + if not ok: + detail = output or "unknown error" + self.log(self.tf('log_driver_install_failed', error=detail), "ERROR") + self.run_on_ui_thread( + lambda: messagebox.showerror( + self.t('msg_driver_missing_title'), + self.tf('msg_driver_install_failed', error=detail), + parent=self.root + ) + ) + return + + self.log(self.t('log_driver_install_success'), "SUCCESS") + self.run_on_ui_thread( + lambda: messagebox.showinfo( + self.t('msg_driver_missing_title'), + self.t('msg_driver_install_done'), + parent=self.root + ) + ) + except Exception as e: + self.log(self.tf('log_driver_install_failed', error=str(e)), "ERROR") + self.run_on_ui_thread( + lambda: messagebox.showerror( + self.t('msg_driver_missing_title'), + self.tf('msg_driver_install_failed', error=str(e)), + parent=self.root + ) + ) + + threading.Thread(target=worker, daemon=True).start() + + def check_fastboot_driver_on_startup(self): + if self.driver_prompted: + return + self.driver_prompted = True + + def worker(): + installed, detail = self.is_fastboot_driver_installed() + if installed: + return + if self.debug_mode and detail: + self.log(detail[:500], "CMD") + + def notify_and_install(): + messagebox.showinfo( + self.t('msg_driver_missing_title'), + self.t('msg_driver_missing'), + parent=self.root + ) + self.install_fastboot_driver(prompt=False) + + self.run_on_ui_thread(notify_and_install) + + threading.Thread(target=worker, daemon=True).start() + + 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(self.t('log_device_connected'), "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(self.tf('log_vin', vin=vin), "SUCCESS") + authorized = self.check_authorization(vin) + self.update_device_status(True, vin, authorized) + else: + self.log(self.t('log_vin_unavailable'), "WARNING") + self.update_device_status(True, None, False) + else: + if was_connected: + self.log(self.t('log_device_disconnected'), "WARNING") + self.update_device_status(False) + + self._refreshing = False + + threading.Thread(target=refresh, daemon=True).start() + + def query_authorization_info(self, vin): + 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')) + payload = data.get('data', {}) if isinstance(data, dict) else {} + vehicle_name = payload.get('vehicleName') or payload.get('vehicle_name') or "" + vehicle_name = str(vehicle_name).strip() + if data.get('authorized') is True and vehicle_name: + self.vehicle_name = vehicle_name + return data.get('authorized') is True, vehicle_name, data + + def _post_json(self, url, payload, timeout=10): + body = json.dumps(payload).encode('utf-8') + req = Request( + url, + data=body, + method='POST', + headers={ + 'User-Agent': 'Mozilla/5.0', + 'Content-Type': 'application/json', + }, + ) + with urlopen(req, timeout=timeout) as response: + return json.loads(response.read().decode('utf-8')) + + def check_authorization(self, vin): + """检查授权""" + if self.debug_mode: + self.log(self.t('log_debug_skip_auth'), "WARNING") + return True + self.log(self.t('log_auth_checking'), "SUCCESS") + + try: + authorized, vehicle_name, _ = self.query_authorization_info(vin) + if authorized: + self.log(self.t('log_auth_success'), "SUCCESS") + if vehicle_name: + self.log(self.tf('log_vehicle_name', vehicle=vehicle_name), "SUCCESS") + return True + self.log(self.t('log_auth_failed'), "ERROR") + return False + + except Exception: + if self.debug_mode: + import traceback + self.log(traceback.format_exc(), "ERROR") + self.log(self.t('log_auth_failed'), "ERROR") + return False + + def fetch_package_password(self): + """从服务端获取资源包解压密码""" + if not self.vin: + self.log(self.t('log_need_adb'), "ERROR") + return False + + try: + vehicle_name = self.vehicle_name + if not vehicle_name: + authorized, vehicle_name, _ = self.query_authorization_info(self.vin) + if not authorized: + self.log(self.t('log_auth_failed'), "ERROR") + return False + if not vehicle_name: + self.log(self.t('log_no_vehicle_name'), "ERROR") + return False + + pwd_api_url = "https://api.changan.softwindy.cn/api/authorizations/package-key" + query = urlencode({ + "vin": self.vin, + "vehicleName": vehicle_name, + }) + url = f"{pwd_api_url}?{query}" + if self.debug_mode: + self.log(f"PACKAGE KEY URL: {url}", "CMD") + 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'] + if self.debug_mode: + self.log("PACKAGE KEY: password received", "CMD") + return True + else: + if self.debug_mode: + self.log(f"PACKAGE KEY RESPONSE: {data}", "CMD") + self.log(self.tf('log_data_prepare_failed', error=data.get('message', 'unknown error')), "ERROR") + return False + + except Exception as e: + if self.debug_mode: + import traceback + self.log(traceback.format_exc(), "ERROR") + self.log(self.tf('log_data_prepare_failed', error=str(e)), "ERROR") + return False + + def fetch_runtime_password(self): + """runtime.dat 默认复用 package-key 返回的资源密码。""" + if self.runtime_password: + return True + if not self.extract_password: + if not self.fetch_package_password(): + return False + self.runtime_password = self.extract_password + return True + + def extract_runtime_base_apk(self): + """解压 runtime.dat 到临时缓存,产物必须是 base.apk。""" + if not self.runtime_file.exists(): + self.log(self.tf('log_runtime_missing', path=self.runtime_file), "ERROR") + return None + if not os.path.exists(self.sz): + self.log(self.tf('log_7za_missing', path=self.sz), "ERROR") + return None + if not self.fetch_runtime_password(): + return None + + cache_dir = self._runtime_cache_dir_path() + self.runtime_cache_dir = cache_dir + if cache_dir.exists(): + self._remove_dir_tree(cache_dir) + time.sleep(0.2) + cache_dir.mkdir(parents=True, exist_ok=True) + if sys.platform == 'win32': + subprocess.run(f'attrib +h "{cache_dir.parent}"', shell=True, capture_output=True) + subprocess.run(f'attrib +h "{cache_dir}"', shell=True, capture_output=True) + + def runtime_progress(percent, total, label): + self.update_progress(percent, total, self.t('progress_prepare_runtime'), is_push=True) + + return_code, err_msg = self._extract_7za_with_progress( + self.runtime_file, + cache_dir, + self.runtime_password, + runtime_progress + ) + if return_code != 0: + text = (err_msg or "").lower() + if "password" in text or "data error" in text: + return_code, err_msg = self._extract_7za_with_progress( + self.runtime_file, + cache_dir, + None, + runtime_progress + ) + if return_code != 0: + self.log(self._format_extract_error(err_msg, return_code), "ERROR") + return None + + base_apks = list(cache_dir.rglob("base.apk")) + if not base_apks: + any_apks = list(cache_dir.rglob("*.apk")) + if len(any_apks) == 1: + target = cache_dir / "base.apk" + shutil.move(str(any_apks[0]), str(target)) + base_apks = [target] + if not base_apks or base_apks[0].stat().st_size <= 0: + self.log(self.t('log_base_apk_invalid'), "ERROR") + return None + self.log(self.t('log_runtime_ready'), "INFO") + return base_apks[0] + + def install_runtime_base_apk(self, base_apk): + """安装 runtime 解出的 base.apk;setprop 必须通过自动密码 shell 执行。""" + temp_apk_path = "/data/local/tmp/base.apk" + self.run_adb_shell('mkdir -p /data/local/tmp', timeout=20) + ok, err = self.run_adb_shell('setprop vecentek.model 1', timeout=20) + if not ok: + return False, self.tf('err_install_failed', error=err) + + ok, err = self.run_adb_command(f'adb -d push "{base_apk}" {temp_apk_path}') + if not ok: + return False, self.tf('err_push_failed', error=err) + + ok, err = self.run_adb_shell(f'pm install -r -d -f {temp_apk_path}', timeout=120) + self.run_adb_shell(f'rm -f {temp_apk_path}', timeout=20) + if not ok: + return False, self.tf('err_install_failed', error=err) + return True, "" + + 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, self.tf('err_push_failed', error=err) + + ok, err = self.run_adb_shell(f'pm install -r -d {temp_apk_path}') + self.run_adb_shell(f'rm -f {temp_apk_path}') + if not ok: + return False, self.tf('err_install_failed', error=err) + + return True, "" + + def collect_boot_device_info(self): + info = { + "vin": self.vin or "", + "toolVersion": self.tool_version, + } + ok, output = self.run_adb_command('adb -d get-serialno') + if ok: + info["adbSerial"] = output.strip() + + props = { + "ro.serialno": "roSerialno", + "ro.boot.serialno": "roBootSerialno", + "ro.product.manufacturer": "manufacturer", + "ro.product.model": "model", + "ro.product.device": "device", + "ro.build.fingerprint": "fingerprint", + } + for prop, key in props.items(): + ok, output = self.run_adb_shell(f'getprop {prop}') + if ok: + info[key] = output.strip() + return {k: str(v).strip() for k, v in info.items() if str(v).strip()} + + def _decode_key_material(self, key_text): + text = str(key_text).strip() + if text.startswith("raw:"): + key = text.split(":", 1)[1].encode("utf-8") + if len(key) != 32: + raise ValueError("key must decode to 32 bytes") + return key + if text.startswith("sha256:"): + return bytes.fromhex(text.split(":", 1)[1]) + if len(text) == 64 and all(c in '0123456789abcdefABCDEF' for c in text): + return bytes.fromhex(text) + raw = text.encode('utf-8') + if len(raw) == 32: + return raw + padded = text + ("=" * (-len(text) % 4)) + return base64.urlsafe_b64decode(padded.encode('ascii')) + + def fetch_permission_resource_key(self): + """通过 boot challenge 协议获取 EZ60 init_boot 资源解密密钥。""" + if self.permission_resource_key: + return True + if not self.vin: + self.log(self.t('log_need_adb'), "ERROR") + return False + + try: + device_info = self.collect_boot_device_info() + challenge_data = self._post_json(self.boot_challenge_api_url, { + "vin": self.vin, + "toolVersion": self.tool_version, + "deviceInfo": device_info, + }) + challenge_payload = challenge_data.get('data', {}) if isinstance(challenge_data, dict) else {} + challenge_id = str(challenge_payload.get('challengeId') or '').strip() + nonce = str(challenge_payload.get('nonce') or '').strip() + if not challenge_data.get('success') or not challenge_id or not nonce: + self.log(self.tf('log_boot_challenge_failed', error=challenge_data.get('message', 'unknown error')), "ERROR") + return False + + key_data = self._post_json(self.boot_key_api_url, { + "vin": self.vin, + "challengeId": challenge_id, + "nonce": nonce, + "timestamp": int(time.time() * 1000), + "toolVersion": self.tool_version, + "deviceInfo": device_info, + }) + key_payload = key_data.get('data', {}) if isinstance(key_data, dict) else {} + key_text = key_payload.get('sessionKey') + if not key_data.get('success') or not key_text: + self.log(self.tf('log_boot_key_failed', error=key_data.get('message', 'unknown error')), "ERROR") + return False + key_bytes = self._decode_key_material(key_text) + if len(key_bytes) != 32: + self.log(self.t('log_boot_key_len_error'), "ERROR") + return False + self.permission_resource_key = key_bytes + self.log(self.t('log_boot_key_success'), "INFO") + return True + except Exception as e: + if self.debug_mode: + import traceback + self.log(traceback.format_exc(), "ERROR") + self.log(self.tf('log_boot_key_failed', error=str(e)), "ERROR") + return False + + def _parse_permission_resource_payload(self, blob): + if blob.startswith(b'EZ60R2\x00'): + header_len = struct.unpack('>I', blob[7:11])[0] + header_start = 11 + header_end = header_start + header_len + payload = json.loads(blob[header_start:header_end].decode('utf-8')) + ciphertext = blob[header_end:] + return payload, ciphertext + if blob.startswith(b'Q05R2\x00'): + header_len = struct.unpack('>I', blob[6:10])[0] + header_start = 10 + header_end = header_start + header_len + payload = json.loads(blob[header_start:header_end].decode('utf-8')) + ciphertext = blob[header_end:] + return payload, ciphertext + + payload = json.loads(blob.decode('utf-8')) + ciphertext = base64.urlsafe_b64decode(payload['ciphertext'] + "=" * (-len(payload['ciphertext']) % 4)) + return payload, ciphertext + + def decrypt_permission_resource_to_temp_file(self): + """解密 EZ60_resource.dat 到随机临时 img 文件,调用者必须尽快删除。""" + if AESGCM is None: + self.log(self.t('log_crypto_missing'), "ERROR") + return None + if not self.permission_resource_file.exists(): + self.log(self.tf('log_permission_resource_missing', path=self.permission_resource_file), "ERROR") + return None + if not self.fetch_permission_resource_key(): + return None + + try: + blob = self.permission_resource_file.read_bytes() + payload, ciphertext = self._parse_permission_resource_payload(blob) + if payload.get('format') not in ('ez60-resource-v2', 'q05-lidar-resource-v2', 'q05-lidar-resource-v1'): + self.log(self.t('log_permission_resource_format_unsupported'), "ERROR") + return None + if payload.get('cipher') != 'AES-256-GCM': + self.log(self.t('log_permission_resource_algorithm_unsupported'), "ERROR") + return None + + nonce = base64.urlsafe_b64decode(payload['nonce'] + "=" * (-len(payload['nonce']) % 4)) + aad = payload.get('aad', 'Mazda-EZ60 init_boot resource v1').encode('utf-8') + plain = AESGCM(self.permission_resource_key).decrypt(nonce, ciphertext, aad) + if payload.get('compression') == 'zlib': + plain = zlib.decompress(plain) + + expected_sha = payload.get('sha256', '').lower() + actual_sha = hashlib.sha256(plain).hexdigest() + if expected_sha and actual_sha != expected_sha: + self.log(self.t('log_permission_resource_decrypt_auth_failed'), "ERROR") + return None + + fd, temp_name = tempfile.mkstemp(prefix='ez60_', suffix='.img') + try: + with os.fdopen(fd, 'wb') as fp: + fp.write(plain) + fp.flush() + os.fsync(fp.fileno()) + finally: + plain = b'' + self.log(self.t('log_permission_resource_decrypt_ready'), "INFO") + return Path(temp_name) + except Exception as e: + if self.debug_mode: + import traceback + self.log(traceback.format_exc(), "ERROR") + self.log(self.tf('log_permission_resource_decrypt_failed', error=str(e) or e.__class__.__name__), "ERROR") + return None + + def secure_delete_file(self, path): + """尽力覆盖并删除临时镜像。""" + try: + p = Path(path) + if not p.exists(): + return + size = p.stat().st_size + with p.open('r+b') as fp: + first_chunk = min(size, 1024 * 1024) + fp.write(os.urandom(first_chunk)) + remaining = size - first_chunk + zero = b'\x00' * 1024 * 1024 + while remaining > 0: + chunk = min(remaining, len(zero)) + fp.write(zero[:chunk]) + remaining -= chunk + fp.flush() + os.fsync(fp.fileno()) + p.unlink() + self.log(self.t('log_temp_img_deleted'), "INFO") + except Exception as e: + self.log(self.tf('log_temp_img_delete_failed', error=e), "WARNING") + + def run_fastboot_command(self, args, timeout=60): + command = [self.fastboot] + list(args) + if self.debug_mode: + self.log("CMD: " + " ".join(f'"{x}"' if " " in str(x) else str(x) for x in command), "CMD") + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + encoding='utf-8', + errors='replace', + timeout=timeout, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + output = ((result.stdout or "") + "\n" + (result.stderr or "")).strip() + if result.returncode == 0: + return True, output + return False, output + except subprocess.TimeoutExpired: + return False, self.t('err_fastboot_timeout') + except Exception as e: + return False, str(e) + + def fastboot_device_connected(self, output): + for line in str(output or "").splitlines(): + parts = line.strip().split() + if len(parts) >= 2 and parts[1].lower() == "fastboot": + return True + return False + + def fastboot_output_has_okay(self, output): + text = str(output or "").upper() + return "OKAY" in text and "FAILED" not in text + + def wait_for_fastboot(self, timeout=180, interval=5): + deadline = time.time() + timeout + while time.time() < deadline: + ok, output = self.run_fastboot_command(['devices'], timeout=10) + if ok and self.fastboot_device_connected(output): + return True + time.sleep(interval) + return False + + def prepare_ez60_permission(self): + """获取权限:安装 base.apk、重启到 fastboot、刷入 init_boot、立即重启。""" + if not self.check_device_connection(): + return + if not self.vin: + messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_need_vin')) + return + + answer = messagebox.askyesno( + self.t('msg_confirm_permission_title'), + self.t('msg_confirm_permission') + ) + if not answer: + return + + def worker(): + temp_img = None + permission_done = False + try: + if not self.check_authorization(self.vin): + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized')) + ) + return + + self.show_progress(True, is_push=True) + self.update_progress(1, 7, self.t('progress_prepare_runtime'), is_push=True) + base_apk = self.extract_runtime_base_apk() + if not base_apk: + return + + self.update_progress(2, 7, self.t('progress_install_runtime'), is_push=True) + ok, err = self.install_runtime_base_apk(base_apk) + if not ok: + self.log(self.tf('log_runtime_install_failed', error=err), "ERROR") + return + self.log(self.t('log_runtime_install_success'), "INFO") + + self.update_progress(3, 7, self.t('progress_fetch_boot_key'), is_push=True) + if not self.fetch_permission_resource_key(): + return + + self.update_progress(4, 7, self.t('progress_reboot_fastboot'), is_push=True) + ok, output = self.run_adb_shell('reboot fastboot') + if not ok and self.debug_mode: + self.log(self.tf('log_fastboot_enter_failed', output=output), "CMD") + self.log(self.t('log_fastboot_wait'), "WARNING") + if not self.wait_for_fastboot(): + self.log(self.t('log_fastboot_missing'), "ERROR") + return + + self.update_progress(5, 7, self.t('progress_decrypt_init_boot'), is_push=True) + temp_img = self.decrypt_permission_resource_to_temp_file() + if not temp_img: + return + + self.update_progress(6, 7, self.t('progress_flash_init_boot'), is_push=True) + ok, output = self.run_fastboot_command(['flash', 'init_boot', str(temp_img)], timeout=120) + if not ok or not self.fastboot_output_has_okay(output): + self.log(self.tf('log_init_boot_flash_failed', output=output), "ERROR") + return + self.log(self.t('log_init_boot_flash_success'), "INFO") + + self.update_progress(7, 7, self.t('progress_reboot_device'), is_push=True) + ok, output = self.run_fastboot_command(['reboot'], timeout=30) + if ok: + permission_done = True + self.log(self.t('log_permission_success'), "SUCCESS") + self.run_on_ui_thread( + lambda: messagebox.showinfo(self.t('msg_done_title'), self.t('msg_permission_done')) + ) + else: + self.log(self.tf('log_fastboot_reboot_failed', output=output), "WARNING") + finally: + self.permission_resource_key = None + if temp_img: + self.secure_delete_file(temp_img) + self.show_progress(False, is_push=True) + if not permission_done: + self.log(self.t('log_permission_failed'), "ERROR") + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('log_permission_failed')) + ) + + threading.Thread(target=worker, daemon=True).start() + + 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 run_mazda_post_install_tasks(self): + """语言包安装完成后启用 Mazda overlay 并禁用指定应用。""" + self.log(self.t('log_post_config_start'), "INFO") + + overlay_ok = True + for package_name in self.mazda_overlay_packages: + ok, output = self.run_adb_shell(f'cmd overlay enable {package_name}') + if ok: + if self.debug_mode: + self.log(self.tf('log_overlay_enabled', package=package_name), "SUCCESS") + else: + overlay_ok = False + if self.debug_mode: + self.log(self.tf('log_overlay_failed', package=package_name, output=output), "ERROR") + + disabled_count = 0 + for package_name in self.mazda_disable_packages: + ok, output = self.run_adb_shell(f'pm disable-user {package_name}') + if ok: + disabled_count += 1 + if self.debug_mode: + self.log(self.tf('log_disabled_package', package=package_name), "SUCCESS") + else: + if self.debug_mode: + self.log(self.tf('log_disable_package_failed', package=package_name, output=output), "ERROR") + + if self.debug_mode: + self.log( + self.tf( + 'log_post_config_done', + overlay_count=len(self.mazda_overlay_packages), + disabled_count=disabled_count, + total_count=len(self.mazda_disable_packages), + ), + "INFO", + ) + return overlay_ok and disabled_count == len(self.mazda_disable_packages) + + def install_voice_assistant_patch(self): + """安装语音助理 Magisk 模块补丁。""" + if not self.check_device_connection(): + return + if not self.vin: + messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_need_vin')) + return + + def do_install_patch(): + if not self.check_authorization(self.vin): + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized')) + ) + return + + if not self.extract_password: + if not self.fetch_package_password(): + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_data_prepare_failed')) + ) + return + + if not self.check_package_extracted(): + self.show_progress(True, is_push=False) + if not self.extract_package_silent(): + self.show_progress(False, is_push=False) + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_prepare_failed')) + ) + return + self.show_progress(False, is_push=False) + + module_zips = self.find_voice_patch_modules() + ok, reason = self.validate_voice_patch_modules(module_zips) + if not ok: + self.log(reason, "ERROR") + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_prepare_failed')) + ) + return + self.voice_module_zips = module_zips + + self.show_progress(True, is_push=True) + try: + self.log(self.t('log_voice_patch_start'), "SUCCESS") + if not self.open_magisk_and_check_root(): + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_root_failed')) + ) + return + + total = len(self.voice_module_zips) + for idx, zip_path in enumerate(self.voice_module_zips, 1): + self.update_progress(idx - 1, total, self.t('progress_install_module'), is_push=True) + ok, result = self.install_magisk_module_zip(zip_path) + if not ok: + self.log(self.tf('log_module_install_failed', error=result), "ERROR") + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('log_voice_patch_failed')) + ) + return + if self.debug_mode: + self.log(f"MODULE OK: {result}", "CMD") + + self.install_magisk_manager_cleanup_module() + self.update_progress(total, total, self.t('progress_flash_done'), is_push=True) + self.log(self.t('log_voice_patch_done'), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('msg_success_title'), + self.t('log_voice_patch_done') + ) + finally: + self.show_progress(False, is_push=True) + + threading.Thread(target=do_install_patch, daemon=True).start() + + def push_all_apks(self): + """推送APK并安装 —— 逸动版仅处理 app 目录,使用 pm install""" + # 检查设备连接(仅 UI 层检查在主线程,其余工作进后台线程) + if not self.check_device_connection(): + return + + if not self.vin: + messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_need_vin')) + return + + def do_push_all(): + # 验证授权 + if not self.check_authorization(self.vin): + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized')) + ) + return + + # 获取解压密码 + if not self.extract_password: + if not self.fetch_package_password(): + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_data_prepare_failed')) + ) + return + + # 解压 + if not self.check_package_extracted(): + self.log(self.t('log_extracting'), "INFO") + self.show_progress(True, is_push=False) + if not self.extract_package_silent(): + self.show_progress(False, is_push=False) + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_prepare_failed')) + ) + return + self.show_progress(False, is_push=False) + + if not self.apps_dir or not self.apps_dir.exists(): + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_dir_missing')) + ) + return + + # 开始刷入 + self.show_progress(True, is_push=True) + self.log(self.t('log_flash_start'), "SUCCESS") + 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(self.t('log_no_language_files'), "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: + if self.debug_mode: + self.log(self.tf('log_install_success', name=apk_name), "SUCCESS") + success_count += 1 + else: + if self.debug_mode: + self.log(self.tf('log_install_failed', name=apk_name), "ERROR") + else: + self.log(self.tf('log_flash_item_failed', current=i, total=total), "ERROR") + self.update_progress(i, total, self.t('progress_flashing'), is_push=True) + + self.update_progress(total, total, self.t('progress_flash_done'), is_push=True) + self.run_adb_shell('setprop vecentek.model 0') + + if success_count == total: + self.log(self.t('log_flash_done_config'), "SUCCESS") + elif success_count > 0: + self.log(self.t('log_flash_partial_config'), "WARNING") + else: + self.log(self.t('log_flash_failed_config'), "WARNING") + + if self.run_mazda_post_install_tasks(): + self.log(self.t('log_post_config_all_done'), "SUCCESS") + else: + self.log(self.t('log_post_config_partial'), "WARNING") + + 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(self.t('msg_warn_title'), self.t('msg_need_vin')) + return + if not self.check_authorization(self.vin): + messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized_action')) + return + + # 使用当前目录下的apks文件夹 + apk_dir = self.base_dir / "apks" + if not apk_dir.exists(): + apk_dir = find_resource("apks") + + # 检查apk文件夹是否存在 + if not apk_dir.exists(): + messagebox.showerror(self.t('msg_error_title'), self.t('msg_apks_dir_missing')) + self.log(self.t('msg_apks_dir_missing'), "ERROR") + return + + # 查找所有apk文件 + apk_files = list(apk_dir.glob("*.apk")) + if not apk_files: + messagebox.showerror(self.t('msg_error_title'), self.t('msg_apks_empty')) + self.log(self.t('msg_apks_empty'), "ERROR") + return + + # 询问是否确认安装 + result = messagebox.askyesno( + self.t('msg_install_confirm_title'), + self.tf('msg_install_confirm_folder', count=len(apk_files)) + ) + if not result: + return + + def install(): + self.show_progress(True, is_push=True) + total = len(apk_files) + self.log(self.tf('log_batch_install_start', count=total), "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, self.t('progress_installing'), is_push=True) + if self._push_and_install(apk_path, apk_name): + self.log(self.tf('log_install_success', name=apk_name), "SUCCESS") + success_count += 1 + else: + self.log(self.tf('log_install_failed', name=apk_name), "ERROR") + + self.run_adb_shell('setprop vecentek.model 0') + self.update_progress(total, total, self.t('progress_install_done'), is_push=True) + self.show_progress(False, is_push=True) + + if success_count == total: + self.run_on_ui_thread( + messagebox.showinfo, + self.t('msg_install_done_title'), + self.tf('msg_install_done_all', count=total) + ) + elif success_count > 0: + self.run_on_ui_thread( + messagebox.showwarning, + self.t('msg_install_partial_title'), + self.tf('msg_install_partial', success=success_count, failed=total - success_count) + ) + else: + self.log(self.t('log_install_failed_simple'), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_install_failed_title'), + self.t('msg_install_failed_all') + ) + + 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(self.t('msg_warn_title'), self.t('msg_need_vin')) + return + if not self.check_authorization(self.vin): + messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized_action')) + return + + file_path = filedialog.askopenfilename( + title=self.t('file_select_apk_title'), + filetypes=[(self.t('filetype_apk'), "*.apk"), (self.t('filetype_all'), "*.*")] + ) + + 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, self.t('progress_installing'), 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, self.t('progress_done'), is_push=True) + if success: + self.log(self.tf('log_install_success', name=apk_name), "SUCCESS") + else: + self.log(self.t('log_install_failed_simple'), "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(self.t('quick_lang_title')) + popup.geometry("520x320") + popup.configure(bg=self.colors['bg_dark']) + popup.resizable(False, False) + + # 居中显示 + popup.update_idletasks() + x = self.root.winfo_x() + (self.root.winfo_width() - 520) // 2 + y = self.root.winfo_y() + (self.root.winfo_height() - 320) // 2 + popup.geometry(f"+{x}+{y}") + popup.transient(self.root) + popup.grab_set() + + # 标题 + header = tk.Label(popup, text=self.t('quick_lang_header'), + font=('Microsoft YaHei', 13, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + header.pack(pady=(15, 10)) + + hint = tk.Label(popup, text=self.t('quick_lang_hint'), + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + hint.pack(pady=(0, 12)) + + # 语言列表:(显示名, locale_code) + locale_codes = ["zh-CN", "en-US", "ru-RU", "fr-FR", "es-ES", "pt-BR", "it-IT", "ar-SA"] + languages = list(zip(self.t('quick_lang_names'), locale_codes)) + + # 创建按钮容器 + btn_frame = tk.Frame(popup, bg=self.colors['bg_dark']) + btn_frame.pack(pady=(0, 10)) + + btn_colors = [ + self.colors['accent'], self.colors['info'], + self.colors['success'], self.colors['warning'], + '#e17055', '#00b894', + '#6c5ce7', '#0984e3', + ] + + for i, (label, locale) in enumerate(languages): + row = i // 4 + col = i % 4 + + def make_cmd(loc=locale, lbl=label): + return lambda: self._quick_set_language(loc, lbl, popup) + + btn = tk.Button(btn_frame, text=label, + command=make_cmd(), + font=('Microsoft YaHei', 10), + fg='white', + bg=btn_colors[i], + relief=tk.FLAT, + cursor='hand2', + width=12, height=2) + btn.grid(row=row, column=col, padx=5, pady=5) + + # 底部分隔 + 打开系统设置入口 + sep = tk.Frame(popup, bg=self.colors['border'], height=1) + sep.pack(fill=tk.X, padx=20, pady=(8, 6)) + + sys_btn = tk.Button(popup, text=self.t('quick_lang_system'), + command=lambda: self._open_sys_and_close(popup), + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + sys_btn.pack(pady=(0, 10)) + + def _quick_set_language(self, locale_code, language_name, popup): + """执行快捷语言设置""" + popup.destroy() + + def do_set(): + self.log(f"{self.t('progress_installing')}: {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") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('quick_lang_success_title'), + self.tf('quick_lang_success', language=language_name) + ) + else: + self.log(f"✗ {output}", "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('quick_lang_failed_title'), + self.tf('quick_lang_failed', output=output) + ) + + threading.Thread(target=do_set, daemon=True).start() + + def _open_sys_and_close(self, popup): + """关闭弹窗并打开系统语言设置""" + popup.destroy() + self.open_language_settings() + + def open_timezone_settings(self): + """打开时区设置""" + if not self.check_device_connection(): + return + self.run_adb_shell('am start -a android.settings.TIMEZONE_SETTINGS') + + def open_android_settings(self): + """打开安卓原生设置""" + if not self.check_device_connection(): + return + self.run_adb_shell('am start -a android.settings.SETTINGS') + + def reboot_device(self): + """重启设备""" + if not self.check_device_connection(): + return + if messagebox.askyesno(self.t('msg_reboot_title'), self.t('msg_reboot_confirm')): + self.run_adb_shell('reboot') + self.log(self.t('log_rebooting'), "INFO") + self.update_device_status(False) + + def on_disable_upgrade(self): + """禁用系统升级""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 弹窗确认 + result = messagebox.askyesno( + self.t('msg_disable_ota_title'), + self.t('msg_disable_ota_confirm') + ) + + if not result: + self.log(self.t('log_disable_ota_cancelled'), "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(self.t('log_disable_ota_success'), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('msg_success_title'), self.t('msg_disable_ota_success')) + else: + self.log(self.t('log_disable_ota_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.tf('msg_disable_ota_failed', output=output)) + + self.show_progress(False, is_push=False) + + threading.Thread(target=disable, daemon=True).start() + + def _on_vin_input_focus_in(self, event): + """输入框获得焦点时清除占位符""" + if self.is_placeholder_vin(self.vin_input.get()): + self.vin_input.delete(0, tk.END) + self.vin_input.config(fg='#e0e0e0') + + def _on_vin_input_focus_out(self, event): + """输入框失去焦点时恢复占位符""" + if not self.vin_input.get(): + self.vin_input.insert(0, self.t('vin_placeholder')) + self.vin_input.config(fg='#636e72') + + def query_password_by_vin(self): + """通过VIN查询密码""" + vin = self.vin_input.get().strip() + if not vin or self.is_placeholder_vin(vin): + messagebox.showwarning(self.t('msg_hint_title'), self.t('msg_input_vin')) + return + + def do_query(): + try: + api_url = "https://api.changan.softwindy.cn/api/authorizations/generate-password-by-vin" + url = f"{api_url}?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', 'unknown') + self.pwd_result_label.config( + text=self.tf('pwd_success', password=pwd), + fg=self.colors['success'] + ) + self.log(self.tf('log_pwd_success', vin=vin, password=pwd), "SUCCESS") + else: + msg = data.get('message', 'failed') + self.pwd_result_label.config( + text=self.tf('pwd_failed', message=msg), + fg=self.colors['error'] + ) + self.log(self.tf('log_pwd_failed', message=msg), "ERROR") + + self.run_on_ui_thread(update_ui) + + except Exception as e: + def update_ui_error(): + self.pwd_result_label.config( + text=self.t('pwd_request_failed'), + fg=self.colors['error'] + ) + self.log(self.tf('log_pwd_request_failed', error=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(self.t('log_debug_off'), "WARNING") + self.status_text.config(text=self.t('status_ready')) + self.set_debug_buttons_visible(False) + self.refresh_device_status() + return + + pwd = simpledialog.askstring(self.t('debug_title'), self.t('debug_prompt'), show='*', parent=self.root) + if not pwd: + return + + self.log(self.t('debug_password_verifying'), "WARNING") + + def verify(): + valid, message = self.verify_debug_mode_password(pwd) + if valid: + def enable_debug(): + self.debug_mode = True + self.update_device_status(True, "", True) + self.log(self.t('log_debug_on'), "WARNING") + self.status_text.config(text=self.t('debug_status')) + self.set_debug_buttons_visible(True) + self.run_on_ui_thread(enable_debug) + else: + def show_failed(): + msg = message or self.t('msg_debug_wrong_password') + self.log(self.tf('debug_verify_failed', message=msg), "WARNING") + messagebox.showwarning(self.t('msg_error_title'), msg) + self.run_on_ui_thread(show_failed) + + threading.Thread(target=verify, daemon=True).start() + + def verify_debug_mode_password(self, password): + try: + data = self._post_json(self.debug_password_api_url, {"password": password}) + if data.get('success') is True and data.get('valid') is True: + return True, data.get('message', '') + return False, data.get('message') or self.t('msg_debug_wrong_password') + except Exception as e: + return False, str(e) + + def _require_debug_mode(self): + if self.debug_mode: + return True + messagebox.showwarning(self.t('debug_status'), self.t('debug_need_enable')) + return False + + def _ensure_debug_vin_from_input(self): + if self.vin: + return True + vin = self.vin_input.get().strip().upper() + if vin and not self.is_placeholder_vin(vin): + self.vin = vin + return True + return False + + def debug_test_package_extract(self): + if not self._require_debug_mode(): + return + if not self._ensure_debug_vin_from_input(): + messagebox.showwarning(self.t('debug_status'), self.t('debug_need_vin')) + return + + def worker(): + self.log(self.t('log_debug_extract_start'), "INFO") + self.extract_password = None + if not self.fetch_package_password(): + self.log(self.t('log_debug_extract_failed'), "ERROR") + return + self.show_progress(True) + try: + if self.extract_package_silent(): + module_zips = self.find_voice_patch_modules() + ok, reason = self.validate_voice_patch_modules(module_zips) + if not ok: + self.log(reason, "ERROR") + self.log(self.t('log_debug_extract_failed'), "ERROR") + return + apk_count = len(list(self.apps_dir.glob("*.apk"))) if self.apps_dir and self.apps_dir.exists() else 0 + self.log(self.tf( + 'log_debug_extract_success', + apk_count=apk_count, + module_count=len(module_zips) + ), "SUCCESS") + else: + self.log(self.t('log_debug_extract_failed'), "ERROR") + finally: + self.show_progress(False) + + threading.Thread(target=worker, daemon=True).start() + + def debug_test_boot_extract(self): + if not self._require_debug_mode(): + return + if not self._ensure_debug_vin_from_input(): + key_text = simpledialog.askstring( + self.t('debug_boot_title'), + self.t('debug_boot_prompt'), + show='*', + parent=self.root + ) + if not key_text: + return + try: + key_bytes = self._decode_key_material(key_text) + if len(key_bytes) != 32: + messagebox.showwarning(self.t('debug_boot_title'), self.t('debug_key_len_error')) + return + self.permission_resource_key = key_bytes + except Exception as e: + messagebox.showwarning(self.t('debug_boot_title'), self.tf('debug_key_format_error', error=e)) + return + + def worker(): + temp_img = None + try: + self.log(self.t('log_debug_boot_start'), "INFO") + temp_img = self.decrypt_permission_resource_to_temp_file() + if not temp_img: + self.log(self.t('log_debug_boot_failed'), "ERROR") + return + size = temp_img.stat().st_size + sha = hashlib.sha256(temp_img.read_bytes()).hexdigest() + self.log(self.tf('log_debug_boot_success', size=size, sha=sha), "SUCCESS") + finally: + self.permission_resource_key = None + if temp_img: + self.secure_delete_file(temp_img) + + threading.Thread(target=worker, daemon=True).start() + + def install_apps(self): + """安装App — 支持单选或多选APK文件""" + if not self.check_device_connection(): + return + if not self.vin and not self.debug_mode: + messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_need_vin')) + return + if not self.check_authorization(self.vin): + return + + file_paths = filedialog.askopenfilenames( + title=self.t('file_select_apk_title'), + filetypes=[(self.t('filetype_apk'), "*.apk"), (self.t('filetype_all'), "*.*")] + ) + if not file_paths: + return + + count = len(file_paths) + result = messagebox.askyesno( + self.t('msg_install_confirm_title'), + self.tf('msg_install_confirm_many', count=count) + ) + if not result: + return + + def install(): + self.show_progress(True, is_push=True) + self.log(self.tf('log_install_many_start', count=count), "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, self.tf('progress_installing_name', name=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, self.t('progress_install_done'), is_push=True) + self.show_progress(False, is_push=True) + + if success_count == count: + self.log(self.tf('log_install_done_all', count=count), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('msg_install_done_title'), + self.tf('msg_install_done_all', count=count) + ) + elif success_count > 0: + self.log(self.tf('log_install_done_partial', success=success_count, count=count), "WARNING") + self.run_on_ui_thread( + messagebox.showwarning, + self.t('msg_install_partial_title'), + self.tf('msg_install_partial', success=success_count, failed=count - success_count) + ) + else: + self.log(self.t('log_install_failed_simple'), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_install_failed_title'), + self.t('msg_install_failed_all') + ) + + threading.Thread(target=install, daemon=True).start() + + # ============================================================ + # hosts 文件修改 + # ============================================================ + + def modify_hosts(self): + """修改hosts文件,添加云端认证DNS映射""" + hosts_path = r"C:\Windows\System32\drivers\etc\hosts" + host_ip = "103.236.55.140" + host_name = "spm.auto-pai.com" + entry = f"{host_ip} {host_name}" + + try: + try: + with open(hosts_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + except UnicodeDecodeError: + with open(hosts_path, 'r', encoding='gbk', errors='replace') as f: + lines = f.readlines() + + new_lines = [] + found_target = False + changed = False + for line in lines: + stripped = line.strip() + if not stripped or stripped.startswith('#'): + new_lines.append(line) + continue + + body, _, _ = line.partition('#') + parts = body.split() + if len(parts) >= 2 and host_name.lower() in [p.lower() for p in parts[1:]]: + if not found_target: + if parts[0] != host_ip or len(parts) != 2: + changed = True + new_lines.append(f"{entry}\n") + found_target = True + else: + changed = True + continue + + new_lines.append(line) + + if found_target and not changed: + return True + + if not found_target: + if not new_lines or (new_lines[-1] and not new_lines[-1].endswith(('\n', '\r'))): + new_lines.append('\n') + new_lines.append(f"{entry}\n") + + with open(hosts_path, 'w', encoding='utf-8', newline='') as f: + f.writelines(new_lines) + return True + except PermissionError: + self.log(self.t('log_env_config_failed_admin'), "WARNING") + return False + except Exception as e: + if self.debug_mode: + self.log(self.tf('log_env_config_failed_detail', error=str(e)), "WARNING") + else: + self.log(self.t('log_env_config_failed'), "WARNING") + return False + + def _run_netsh(self, command): + """执行 netsh 命令,返回 (returncode, output)""" + try: + for enc in ['utf-8', 'gbk']: + try: + r = subprocess.run( + command, + shell=True, + capture_output=True, + text=True, + encoding=enc, + errors='replace', + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + output = (r.stdout or '') + (r.stderr or '') + return r.returncode, output.strip() + except (UnicodeDecodeError, LookupError): + continue + return -1, self.t('err_decode_failed') + except Exception as e: + return -1, str(e) + + def start_hotspot_action(self): + """打开热点设置并自动轮询检测""" + # 打开设置页面 + try: + subprocess.Popen( + 'start ms-settings:network-mobilehotspot', + shell=True, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + except: + pass + self.log(self.t('log_hotspot_opening'), "INFO") + + # 后台轮询,查到为止(最多 30 秒) + def poll(): + for _ in range(15): + time.sleep(2) + ssid, pwd, status = self.get_hotspot_info() + self.refresh_hotspot_display() + if ssid and self._hotspot_started(status): + self.log(self.tf('log_hotspot_detected', ssid=ssid, password=pwd), "SUCCESS") + return + self.log(self.t('log_hotspot_not_detected'), "WARNING") + + threading.Thread(target=poll, daemon=True).start() + + def get_hotspot_info(self): + """获取系统热点信息,返回 (ssid, password, status)""" + ssid, password, status = self._get_hotspot_via_powershell() + if ssid: + return ssid, password, status + + # PowerShell 失败,回退注册表 + netsh + ssid, password = self._read_hotspot_registry() + status = "stopped" + try: + _, out = self._run_netsh('netsh wlan show hostednetwork') + for line in out.split('\n'): + s = line.strip() + if ('状态' in s or 'status' in s.lower()) and ('已启动' in s or 'started' in s.lower()): + status = "started" + if not ssid and 'ssid' in s.lower() and ':' in s: + val = s.split(':', 1)[-1].strip().strip('"') + if val and 'not set' not in val.lower(): + ssid = val + except: + pass + return ssid, password, status + + def _hotspot_started(self, status): + status_text = (status or "").lower() + return '已启动' in status_text or 'started' in status_text or 'on' in status_text or 'inoperation' in status_text + + def _get_hotspot_via_powershell(self): + """通过 PowerShell 获取 Windows 移动热点配置""" + try: + ps_cmd = ( + '$cp = [Windows.Networking.Connectivity.NetworkInformation,' + 'Windows.Networking.Connectivity,ContentType=WindowsRuntime]' + '::GetInternetConnectionProfile();' + '$tm = [Windows.Networking.NetworkOperators.NetworkOperatorTetheringManager,' + 'Windows.Networking.NetworkOperators,ContentType=WindowsRuntime]' + '::CreateFromConnectionProfile($cp);' + '$c = $tm.GetCurrentAccessPointConfiguration();' + 'Write-Output $c.Ssid; Write-Output $c.Passphrase;' + 'Write-Output $tm.TetheringOperationalState' + ) + r = subprocess.run( + ['powershell', '-NoProfile', '-Command', ps_cmd], + capture_output=True, text=True, timeout=10, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + lines = [l.strip() for l in (r.stdout or '').split('\n') if l.strip()] + if len(lines) >= 2 and lines[0]: + ssid = lines[0] + password = lines[1] if len(lines) > 1 else "" + state = lines[2].lower() if len(lines) > 2 else "" + status = "started" if ('on' in state or 'inoperation' in state) else "stopped" + return ssid, password, status + except: + pass + return "", "", "stopped" + + def _read_hotspot_registry(self): + """从注册表读取 Windows 移动热点的 SSID 和密码""" + try: + import winreg + key = winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Microsoft\WlanSvc\HostedNetworkSettings" + ) + data, _ = winreg.QueryValueEx(key, "HostedNetworkSettings") + winreg.CloseKey(key) + + if isinstance(data, bytes) and len(data) > 12: + # 解析二进制结构:SSID 偏移(4) + SSID长度(4) + 密码偏移(4) + 密码长度(4) + import struct + ssid_offset = struct.unpack_from(' 0 and ssid_offset + ssid_len * 2 <= len(data): + raw = data[ssid_offset:ssid_offset + ssid_len * 2] + ssid = raw.decode('utf-16-le', errors='replace').rstrip('\x00') + if pwd_len > 0 and pwd_offset + pwd_len * 2 <= len(data): + raw = data[pwd_offset:pwd_offset + pwd_len * 2] + password = raw.decode('utf-16-le', errors='replace').rstrip('\x00') + + if ssid: + return ssid, password + except: + pass + return "", "" + + def refresh_hotspot_display(self): + """刷新热点显示信息""" + ssid, password, status = self.get_hotspot_info() + self.run_on_ui_thread(self._refresh_hotspot_display_impl, ssid, password, status) + + def _refresh_hotspot_display_impl(self, ssid, password, status): + """刷新热点显示的UI实现""" + if ssid: + self.hotspot_ssid_label.config(text=self.tf('hotspot_name_value', ssid=ssid)) + else: + self.hotspot_ssid_label.config(text=self.t('hotspot_name_unset')) + if password: + self.hotspot_pwd_label.config(text=self.tf('hotspot_pwd_value', password=password)) + else: + self.hotspot_pwd_label.config(text=self.t('hotspot_pwd_default')) + started = self._hotspot_started(status) + status_text = self.t('hotspot_started') if started else self.t('hotspot_stopped') + self.hotspot_status_label.config( + text=self.tf('hotspot_status_value', status=status_text), + fg=self.colors['success'] if started else self.colors['warning'] + ) + + def run(self): + """运行程序""" + self.root.mainloop() + +def main(): + """主函数""" + if sys.version_info < (3, 6): + print("Error: Python 3.6 or later is required") + sys.exit(1) + + try: + app = ADKAPKGUI() + app.run() + except Exception as e: + print(f"Startup failed: {e}") + import traceback + traceback.print_exc() + messagebox.showerror("Error", f"Program failed to start: {e}") + +if __name__ == "__main__": + main() diff --git a/Mazda-EZ60/Mazda-EZ60_1.2_voice-assistant_backup.py b/Mazda-EZ60/Mazda-EZ60_1.2_voice-assistant_backup.py new file mode 100644 index 0000000..76f5a3b --- /dev/null +++ b/Mazda-EZ60/Mazda-EZ60_1.2_voice-assistant_backup.py @@ -0,0 +1,3037 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import subprocess +import json +import threading +import re +import atexit +import base64 +import hashlib +import struct +import tempfile +import zlib +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 +try: + from cryptography.hazmat.primitives.ciphers.aead import AESGCM +except ImportError: + AESGCM = None + + +def set_windows_app_id(): + if sys.platform != 'win32': + return + try: + import ctypes + app_id = 'YibinKeyi.MazdaEZ60.LanguageInstaller.1.2' + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id) + except Exception: + pass + + +def get_app_dir(): + return Path(sys.executable).resolve().parent if getattr(sys, 'frozen', False) else Path(__file__).resolve().parent + + +def resource_candidates(file_name): + base_dir = get_app_dir() + candidates = [] + if getattr(sys, 'frozen', False): + candidates.append(Path(getattr(sys, '_MEIPASS', base_dir)) / file_name) + candidates.extend([ + base_dir / file_name, + base_dir / 'tools' / file_name, + base_dir / 'shared' / file_name, + base_dir.parent / 'tools' / file_name, + base_dir.parent / 'shared' / file_name, + base_dir.parent / file_name, + ]) + unique = [] + for candidate in candidates: + if candidate not in unique: + unique.append(candidate) + return unique + + +def find_resource(file_name): + candidates = resource_candidates(file_name) + for candidate in candidates: + if candidate.exists(): + return candidate + return candidates[0] + + +def find_tool(file_name, fallback=None): + path = find_resource(file_name) + if path.exists(): + return str(path) + return fallback or str(path) +class ADKAPKGUI: + CACHE_DIR_NAME = "apps_cache_Mazda_EZ60" + + def __init__(self): + set_windows_app_id() + self.root = tk.Tk() + self.root.title("Mazda-EZ60_OS-1.2适用") + self.root.geometry("900x620") + self.root.resizable(True, True) + self.set_window_icon() + self.root.after(200, self.set_window_icon) + + # 设置颜色主题 + 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': 'Mazda-EZ60_OS-1.2适用', + 'btn_permission': '🔓 获取权限', + 'btn_push': '📦 刷入语言包', + 'btn_install': '📱 安装App', + 'btn_language': '🌐 语言设置', + 'btn_timezone': '⏰ 时区设置', + 'btn_settings': '⚙️ 安卓设置', + 'btn_reboot': '🔄 重启设备', + 'btn_disable_upgrade': '❌ 禁用升级', + 'btn_clear_log': '🗑 清空日志', + 'btn_query_pwd': '查询密码', + 'device_label': '设备:', + 'vin_label': 'VIN码:', + 'auth_label': '授权:', + 'log_title': '📋 运行日志', + 'status_ready': '就绪', + 'status_connected': '已连接', + 'status_disconnected': '未连接', + 'status_detecting': '未检测', + 'vin_none': '未获取', + 'auth_none': '未验证', + 'auth_yes': '已授权', + 'auth_no': '未授权', + 'btn_refresh': '🔄 检查', + 'hint_factory': '🔧 关闭车辆WI-FI和4G网络,拨号获取的密码进入工程模式', + 'hotspot_icon': '📶', + 'hotspot_title': '电脑热点', + 'hotspot_start': '🔧 打开热点设置', + 'hint_icon': '💡', + 'hint_title': '使用提示', + 'theme_dark': '🌙 暗色', + 'theme_light': '☀️ 亮色', + 'lang_zh': '中', + 'lang_en': 'EN', + 'pwd_query_label': '工程密码查询:', + 'vin_placeholder': '请输入VIN', + 'vin_query_hint': '💡 请输入VIN或者VIN后八位查询。', + 'pwd_empty': '', + 'pwd_success': '密码: *#{password}#*', + 'pwd_failed': '失败: {message}', + 'pwd_request_failed': '请求失败', + 'hotspot_name_detecting': '名称: 检测中...', + 'hotspot_name_unset': '名称: 未配置', + 'hotspot_name_value': '名称: {ssid}', + 'hotspot_pwd_default': '密码: changan2024', + 'hotspot_pwd_value': '密码: {password}', + 'hotspot_status_off': '状态: 未启动', + 'hotspot_status_value': '状态: {status}', + 'hotspot_started': '已启动', + 'hotspot_stopped': '未启动', + 'hint_lines': [ + '1. 确保电脑已开启热点', + '2. 拨号进入工厂模式,点击调试工具', + '3. 需要云端认证时,连接右侧显示的电脑热点', + '4. 连接后点击车机“云端认证”按钮', + '5. 打开 ADB 后即可刷入语言包', + ], + 'log_lang_changed': '语言已切换为中文', + 'log_cleared': '日志已清空', + 'msg_warn_title': '警告', + 'msg_error_title': '错误', + 'msg_success_title': '成功', + 'msg_hint_title': '提示', + 'msg_device_not_connected_title': '设备未连接', + 'msg_device_not_connected': '请先连接设备并点击「检查」按钮刷新状态!', + 'msg_need_vin': '请先刷新设备状态并获取VIN码', + 'msg_auth_failed_title': '授权失败', + 'msg_device_unauthorized': '设备未授权', + 'msg_device_unauthorized_action': '设备未授权,无法执行此操作', + 'msg_data_prepare_failed': '资源初始化失败!', + 'msg_resource_prepare_failed': '资源准备失败!', + 'msg_resource_dir_missing': '资源目录未找到', + 'msg_input_vin': '请输入VIN码', + 'msg_done_title': '完成', + 'msg_confirm_permission_title': '确认获取权限', + 'msg_confirm_permission': '即将获取系统权限,过程中请勿断开数据连接或关闭程序。\n\n是否继续?', + 'msg_permission_done': '获取成功,设备正在重启。', + 'msg_reboot_title': '确认重启', + 'msg_reboot_confirm': '确定要重启设备吗?', + 'msg_disable_ota_title': '确认禁用升级', + 'msg_disable_ota_confirm': '⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n是否确定要禁用系统升级应用?', + 'msg_disable_ota_success': '系统升级已成功禁用!', + 'msg_disable_ota_failed': '禁用失败:{output}', + 'file_select_apk_title': '选择APK文件', + 'filetype_apk': 'APK文件', + 'filetype_all': '所有文件', + 'msg_install_confirm_title': '确认安装', + 'msg_install_confirm_many': '已选择 {count} 个APK文件\n\n是否开始安装?', + 'msg_install_confirm_folder': '找到 {count} 个APK文件\n\n是否开始批量安装?', + 'msg_install_done_title': '安装完成', + 'msg_install_done_all': '成功安装 {count} 个APK!', + 'msg_install_partial_title': '部分成功', + 'msg_install_partial': '成功: {success}\n失败: {failed}', + 'msg_install_failed_title': '安装失败', + 'msg_install_failed_all': '所有APK安装失败!', + 'msg_apks_dir_missing': '未找到apks文件夹!\n请在程序目录下创建apks文件夹并放入APK文件。', + 'msg_apks_empty': 'apks文件夹中没有找到APK文件!', + 'quick_lang_title': '快捷语言设置', + 'quick_lang_header': '选择目标语言', + 'quick_lang_hint': '点击按钮即可将系统语言切换为对应语言,重启后生效', + 'quick_lang_system': '⚙️ 打开系统语言设置(手动选择)', + 'quick_lang_success_title': '设置成功', + 'quick_lang_success': '系统语言已设置为 {language}\n\n⚠️ 请重启设备使其生效。', + 'quick_lang_failed_title': '设置失败', + 'quick_lang_failed': '语言设置失败!', + 'quick_lang_names': ['🇨🇳 中文', '英 English', '俄 Русский', '法 Français', '西 Español', '葡 Português', '意 Italiano', '阿 العربية'], + 'debug_title': '调试模式', + 'debug_prompt': '请输入调试密码:', + 'debug_password_verifying': '正在校验调试模式密码...', + 'debug_verify_failed': '调试模式密码校验失败: {message}', + 'debug_status': '🔧 调试模式', + 'msg_debug_wrong_password': '密码错误', + 'status_debug': '🔧 调试模式', + 'progress_loading': '资源加载中', + 'progress_loaded': '资源加载完成', + 'progress_preparing': '正在准备资源', + 'progress_fetch_boot_key': '正在获取权限中', + 'progress_reboot_fastboot': '正在获取权限中', + 'progress_decrypt_init_boot': '正在获取权限中', + 'progress_flash_init_boot': '正在获取权限中', + 'progress_reboot_device': '正在获取权限中', + 'progress_flashing': '正在刷入', + 'progress_flash_done': '刷入完成', + 'progress_installing': '安装中', + 'progress_installing_name': '安装中 ({name})', + 'progress_install_done': '安装完成', + 'progress_done': '完成', + 'log_device_disconnected': '设备已断开连接', + 'log_device_connected': '设备已连接', + 'log_no_package': '资源文件缺失', + 'log_no_adb': '未找到adb命令,请将ADB文件放入本目录', + 'log_no_fastboot': '运行环境缺失', + 'log_vin': 'VIN: {vin}', + 'log_vin_unavailable': '无法获取VIN,请确认设备已进入工厂模式', + 'log_auth_checking': '正在验证授权状态...', + 'log_auth_success': '授权验证通过', + 'log_auth_failed': '授权验证失败', + 'log_debug_skip_auth': '调试模式: 跳过授权验证', + 'log_vehicle_name': '车辆名称: {vehicle}', + 'log_data_prepare_failed': '资源初始化失败', + 'log_boot_challenge_failed': '获取失败', + 'log_boot_key_failed': '获取失败', + 'log_boot_key_len_error': '获取失败', + 'log_boot_key_success': '正在获取权限中', + 'log_crypto_missing': '获取失败', + 'log_permission_resource_missing': '获取失败', + 'log_permission_resource_format_unsupported': '获取失败', + 'log_permission_resource_algorithm_unsupported': '获取失败', + 'log_permission_resource_decrypt_auth_failed': '获取失败', + 'log_permission_resource_decrypt_ready': '正在获取权限中', + 'log_permission_resource_decrypt_failed': '获取失败', + 'log_fastboot_wait': '正在获取权限,请不要关闭程序或断开数据连接!', + 'log_fastboot_missing': '获取失败', + 'log_fastboot_enter_failed': '获取失败', + 'log_init_boot_flash_failed': '获取失败', + 'log_init_boot_flash_success': '正在获取权限中', + 'log_fastboot_reboot_failed': '获取失败', + 'log_permission_success': '获取成功', + 'log_permission_failed': '获取失败', + 'log_temp_img_deleted': '临时文件已清理', + 'log_temp_img_delete_failed': '临时文件清理失败', + 'log_need_adb': '请先连接设备', + 'log_no_vehicle_name': '资源初始化失败', + 'log_package_missing': '资源文件缺失', + 'log_extract_password_missing': '资源初始化失败', + 'log_7za_missing': '运行环境缺失', + 'log_extracting': '资源准备中...', + 'log_apps_missing': '警告:未找到 apps 目录', + 'log_resource_invalid': '资源校验失败', + 'log_resource_ready': '资源准备完成', + 'log_resource_failed': '资源准备失败,请检查网络连接后重试', + 'log_cache_invalid': '资源缓存无效', + 'log_no_language_files': '未找到语言包文件', + 'log_flash_start': '开始刷入语言包,请勿断电或重启电脑和车机。', + 'log_install_success': '安装成功', + 'log_install_failed': '安装失败', + 'log_flash_item_failed': '语言包刷入失败: {current}/{total}', + 'log_flash_done_config': '语言包刷入完成,开始执行 Mazda-EZ60 安装后配置', + 'log_flash_partial_config': '语言包部分刷入成功,仍继续执行 Mazda-EZ60 安装后配置', + 'log_flash_failed_config': '语言包刷入失败,仍继续执行 Mazda-EZ60 安装后配置', + 'log_post_config_start': '正在执行 Mazda-EZ60 安装后配置...', + 'log_overlay_enabled': '配置项已完成', + 'log_overlay_failed': '配置项执行失败', + 'log_disabled_package': '清理项已完成', + 'log_disable_package_failed': '清理项执行失败', + 'log_post_config_done': 'Mazda-EZ60 安装后配置完成', + 'log_post_config_all_done': 'Mazda-EZ60 安装后配置全部完成,重启设备后生效', + 'log_post_config_partial': 'Mazda-EZ60 安装后配置部分失败,请查看日志', + 'log_batch_install_start': '开始批量安装 {count} 个APK...', + 'log_install_many_start': '开始安装 {count} 个APK...', + 'log_install_done_all': '安装完成:全部 {count} 个成功', + 'log_install_done_partial': '安装完成:{success}/{count} 成功', + 'log_install_failed_simple': '安装失败', + 'log_rebooting': '设备正在重启...', + 'log_disable_ota_cancelled': '已取消禁用升级操作', + 'log_disable_ota_success': '系统升级已禁用', + 'log_disable_ota_failed': '禁用系统升级失败', + 'log_pwd_success': '密码查询成功', + 'log_pwd_failed': '密码查询失败', + 'log_pwd_request_failed': '密码查询请求失败', + 'log_debug_off': '调试模式已关闭', + 'log_debug_on': '调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志', + 'log_env_config_failed_admin': '环境配置失败,请以管理员身份运行', + 'log_env_config_failed': '环境配置失败', + 'log_env_config_failed_detail': '环境配置失败: {error}', + 'log_hotspot_opening': '已打开热点设置,正在检测热点...', + 'log_hotspot_detected': '检测到热点: {ssid} / {password}', + 'log_hotspot_not_detected': '未检测到热点,请确认已开启', + 'err_extract_wrong_password': '资源准备失败', + 'err_extract_data': '资源准备失败', + 'err_extract_headers': '资源准备失败', + 'err_extract_detail': '资源准备失败', + 'err_extract_code': '资源准备失败', + 'err_missing_apps': '资源目录异常', + 'err_empty_apps': '资源目录异常', + 'err_zero_apks': '资源文件异常', + 'err_cmd_timeout': '命令超时', + 'err_fastboot_timeout': '获取失败', + 'err_push_failed': 'push失败: {error}', + 'err_install_failed': 'install失败: {error}', + 'err_decode_failed': '解码失败', + 'msg_start_failed_title': '错误', + 'msg_start_failed': '程序启动失败: {error}', + 'print_python_required': '错误:需要Python 3.6或更高版本', + 'print_start_failed': '启动失败: {error}', + }, + 'en': { + 'title': 'Mazda-EZ60_OS-1.2适用', + 'btn_permission': '🔓 Unlock', + 'btn_push': '📦 Flash Lang Pkg', + 'btn_install': '📱 Install App', + 'btn_language': '🌐 Language', + 'btn_timezone': '⏰ Timezone', + 'btn_settings': '⚙️ Settings', + 'btn_reboot': '🔄 Reboot', + 'btn_disable_upgrade': '❌ Disable OTA', + 'btn_clear_log': '🗑 Clear Log', + 'btn_query_pwd': 'Query Pwd', + 'device_label': 'Device:', + 'vin_label': 'VIN:', + 'auth_label': 'Auth:', + 'log_title': '📋 Log', + 'status_ready': 'Ready', + 'status_connected': 'Connected', + 'status_disconnected': 'Disconnected', + 'status_detecting': 'Detecting', + 'vin_none': 'None', + 'auth_none': 'Unknown', + 'auth_yes': 'Authorized', + 'auth_no': 'Unauthorized', + 'btn_refresh': '🔄 Check', + 'hint_factory': '🔧 Turn off WiFi & 4G, enter factory mode with dial code', + 'hotspot_icon': '📶', + 'hotspot_title': 'Hotspot', + 'hotspot_start': '🔧 Open Hotspot Settings', + 'hint_icon': '💡', + 'hint_title': 'Tips', + 'theme_dark': '🌙 Dark', + 'theme_light': '☀️ Light', + 'lang_zh': '中', + 'lang_en': 'EN', + 'pwd_query_label': 'Factory password:', + 'vin_placeholder': 'Enter VIN', + 'vin_query_hint': '💡 Enter the VIN or the last 8 digits of the VIN to query.', + 'pwd_empty': '', + 'pwd_success': 'Password: *#{password}#*', + 'pwd_failed': 'Failed: {message}', + 'pwd_request_failed': 'Request failed', + 'hotspot_name_detecting': 'Name: detecting...', + 'hotspot_name_unset': 'Name: not configured', + 'hotspot_name_value': 'Name: {ssid}', + 'hotspot_pwd_default': 'Password: changan2024', + 'hotspot_pwd_value': 'Password: {password}', + 'hotspot_status_off': 'Status: stopped', + 'hotspot_status_value': 'Status: {status}', + 'hotspot_started': 'started', + 'hotspot_stopped': 'stopped', + 'hint_lines': [ + '1. Turn on the PC hotspot', + '2. Enter factory mode with the dial password and open debug tools', + '3. For cloud authentication, connect the head unit to the hotspot shown above', + '4. Tap “Cloud Authentication” on the head unit after connecting', + '5. Enable ADB, then flash the language package', + ], + 'log_lang_changed': 'Language switched to English', + 'log_cleared': 'Log cleared', + 'msg_warn_title': 'Warning', + 'msg_error_title': 'Error', + 'msg_success_title': 'Success', + 'msg_hint_title': 'Hint', + 'msg_device_not_connected_title': 'Device not connected', + 'msg_device_not_connected': 'Connect the device and click "Check" to refresh status first.', + 'msg_need_vin': 'Refresh device status and get VIN first', + 'msg_auth_failed_title': 'Authorization failed', + 'msg_device_unauthorized': 'Device is not authorized', + 'msg_device_unauthorized_action': 'Device is not authorized. This action cannot continue.', + 'msg_data_prepare_failed': 'Resource initialization failed!', + 'msg_resource_prepare_failed': 'Resource preparation failed!', + 'msg_resource_dir_missing': 'Resource directory not found', + 'msg_input_vin': 'Enter VIN', + 'msg_done_title': 'Done', + 'msg_confirm_permission_title': 'Confirm unlock', + 'msg_confirm_permission': 'The tool will get system permission. Do not disconnect the data cable or close the program during the process.\n\nContinue?', + 'msg_permission_done': 'Permission acquired. The device is rebooting.', + 'msg_reboot_title': 'Confirm reboot', + 'msg_reboot_confirm': 'Reboot the device now?', + 'msg_disable_ota_title': 'Confirm Disable OTA', + 'msg_disable_ota_confirm': 'Warning: after disabling OTA, the system will not receive updates.\n\nDisable the OTA app now?', + 'msg_disable_ota_success': 'System OTA has been disabled.', + 'msg_disable_ota_failed': 'Disable failed: {output}', + 'file_select_apk_title': 'Select APK files', + 'filetype_apk': 'APK files', + 'filetype_all': 'All files', + 'msg_install_confirm_title': 'Confirm install', + 'msg_install_confirm_many': 'Selected {count} APK file(s).\n\nStart installing?', + 'msg_install_confirm_folder': 'Found {count} APK file(s).\n\nStart batch install?', + 'msg_install_done_title': 'Install complete', + 'msg_install_done_all': 'Successfully installed {count} APK file(s).', + 'msg_install_partial_title': 'Partially complete', + 'msg_install_partial': 'Succeeded: {success}\nFailed: {failed}', + 'msg_install_failed_title': 'Install failed', + 'msg_install_failed_all': 'All APK installs failed.', + 'msg_apks_dir_missing': 'apks folder not found.\nCreate an apks folder next to the program and place APK files in it.', + 'msg_apks_empty': 'No APK files found in the apks folder.', + 'quick_lang_title': 'Quick Language', + 'quick_lang_header': 'Select Target Language', + 'quick_lang_hint': 'Tap a language to switch the system locale. Reboot to apply.', + 'quick_lang_system': '⚙️ Open system language settings', + 'quick_lang_success_title': 'Set Successfully', + 'quick_lang_success': 'System language has been set to {language}.\n\nReboot the device to apply.', + 'quick_lang_failed_title': 'Set Failed', + 'quick_lang_failed': 'Language setting failed.', + 'quick_lang_names': ['🇨🇳 Chinese', 'English', 'Russian', 'French', 'Spanish', 'Portuguese', 'Italian', 'Arabic'], + 'debug_title': 'Debug Mode', + 'debug_prompt': 'Enter debug password:', + 'debug_password_verifying': 'Verifying debug mode password...', + 'debug_verify_failed': 'Debug mode password verification failed: {message}', + 'debug_status': '🔧 Debug Mode', + 'msg_debug_wrong_password': 'Wrong password', + 'status_debug': '🔧 Debug mode', + 'progress_loading': 'Preparing resources', + 'progress_loaded': 'Resources ready', + 'progress_preparing': 'Preparing resources', + 'progress_fetch_boot_key': 'Getting permission', + 'progress_reboot_fastboot': 'Getting permission', + 'progress_decrypt_init_boot': 'Getting permission', + 'progress_flash_init_boot': 'Getting permission', + 'progress_reboot_device': 'Getting permission', + 'progress_flashing': 'Flashing', + 'progress_flash_done': 'Flash complete', + 'progress_installing': 'Installing', + 'progress_installing_name': 'Installing ({name})', + 'progress_install_done': 'Install complete', + 'progress_done': 'Done', + 'log_device_disconnected': 'Device disconnected', + 'log_device_connected': 'Device connected', + 'log_no_package': 'Resource file is missing', + 'log_no_adb': 'adb not found. Place ADB files in this folder.', + 'log_no_fastboot': 'Runtime environment is incomplete', + 'log_vin': 'VIN: {vin}', + 'log_vin_unavailable': 'Unable to read VIN. Confirm the device is in factory mode.', + 'log_auth_checking': 'Checking authorization...', + 'log_auth_success': 'Authorization passed', + 'log_auth_failed': 'Authorization failed', + 'log_debug_skip_auth': 'Debug mode: skipping authorization', + 'log_vehicle_name': 'Vehicle name: {vehicle}', + 'log_data_prepare_failed': 'Resource initialization failed', + 'log_boot_challenge_failed': 'Permission failed', + 'log_boot_key_failed': 'Permission failed', + 'log_boot_key_len_error': 'Permission failed', + 'log_boot_key_success': 'Getting permission', + 'log_crypto_missing': 'Permission failed', + 'log_permission_resource_missing': 'Permission failed', + 'log_permission_resource_format_unsupported': 'Permission failed', + 'log_permission_resource_algorithm_unsupported': 'Permission failed', + 'log_permission_resource_decrypt_auth_failed': 'Permission failed', + 'log_permission_resource_decrypt_ready': 'Getting permission', + 'log_permission_resource_decrypt_failed': 'Permission failed', + 'log_fastboot_wait': 'Getting permission. Do not close the program or disconnect the data cable.', + 'log_fastboot_missing': 'Permission failed', + 'log_fastboot_enter_failed': 'Permission failed', + 'log_init_boot_flash_failed': 'Permission failed', + 'log_init_boot_flash_success': 'Getting permission', + 'log_fastboot_reboot_failed': 'Permission failed', + 'log_permission_success': 'Permission acquired', + 'log_permission_failed': 'Permission failed', + 'log_temp_img_deleted': 'Temporary file cleaned', + 'log_temp_img_delete_failed': 'Failed to clean temporary file', + 'log_need_adb': 'Connect the device first.', + 'log_no_vehicle_name': 'Resource initialization failed', + 'log_package_missing': 'Resource file is missing', + 'log_extract_password_missing': 'Resource initialization failed', + 'log_7za_missing': 'Runtime environment is incomplete', + 'log_extracting': 'Preparing resources...', + 'log_apps_missing': 'Warning: apps directory not found', + 'log_resource_invalid': 'Resource validation failed', + 'log_resource_ready': 'Resources ready', + 'log_resource_failed': 'Resource preparation failed. Check the network and try again.', + 'log_cache_invalid': 'Resource cache is invalid', + 'log_no_language_files': 'No language package files found', + 'log_flash_start': 'Starting language package flashing. Do not power off or restart the computer or vehicle head unit.', + 'log_install_success': 'Install succeeded', + 'log_install_failed': 'Install failed', + 'log_flash_item_failed': 'Language package flash failed: {current}/{total}', + 'log_flash_done_config': 'Language packages flashed. Running Mazda-EZ60 post-install configuration.', + 'log_flash_partial_config': 'Some language packages flashed. Continuing Mazda-EZ60 post-install configuration.', + 'log_flash_failed_config': 'Language package flashing failed. Still running Mazda-EZ60 post-install configuration.', + 'log_post_config_start': 'Running Mazda-EZ60 post-install configuration...', + 'log_overlay_enabled': 'Configuration item completed', + 'log_overlay_failed': 'Configuration item failed', + 'log_disabled_package': 'Cleanup item completed', + 'log_disable_package_failed': 'Cleanup item failed', + 'log_post_config_done': 'Mazda-EZ60 post-install configuration complete', + 'log_post_config_all_done': 'Mazda-EZ60 post-install configuration complete. Reboot the device to apply.', + 'log_post_config_partial': 'Mazda-EZ60 post-install configuration partly failed. Check the log.', + 'log_batch_install_start': 'Starting batch install for {count} APK file(s)...', + 'log_install_many_start': 'Starting install for {count} APK file(s)...', + 'log_install_done_all': 'Install complete: all {count} succeeded', + 'log_install_done_partial': 'Install complete: {success}/{count} succeeded', + 'log_install_failed_simple': 'Install failed', + 'log_rebooting': 'Device is rebooting...', + 'log_disable_ota_cancelled': 'Disable OTA operation cancelled', + 'log_disable_ota_success': 'System OTA disabled', + 'log_disable_ota_failed': 'Disable OTA failed', + 'log_pwd_success': 'Password query succeeded', + 'log_pwd_failed': 'Password query failed', + 'log_pwd_request_failed': 'Password query request failed', + 'log_debug_off': 'Debug mode disabled', + 'log_debug_on': 'Debug mode enabled - authorization/device checks skipped, detailed ADB logs shown', + 'log_env_config_failed_admin': 'Environment configuration failed. Run as administrator.', + 'log_env_config_failed': 'Environment configuration failed', + 'log_env_config_failed_detail': 'Environment configuration failed: {error}', + 'log_hotspot_opening': 'Opened hotspot settings. Detecting hotspot...', + 'log_hotspot_detected': 'Hotspot detected: {ssid} / {password}', + 'log_hotspot_not_detected': 'Hotspot not detected. Make sure it is turned on.', + 'err_extract_wrong_password': 'Resource preparation failed', + 'err_extract_data': 'Resource preparation failed', + 'err_extract_headers': 'Resource preparation failed', + 'err_extract_detail': 'Resource preparation failed', + 'err_extract_code': 'Resource preparation failed', + 'err_missing_apps': 'Resource directory is invalid', + 'err_empty_apps': 'Resource directory is invalid', + 'err_zero_apks': 'Resource file is invalid', + 'err_cmd_timeout': 'Command timed out', + 'err_fastboot_timeout': 'Permission failed', + 'err_push_failed': 'push failed: {error}', + 'err_install_failed': 'install failed: {error}', + 'err_decode_failed': 'Decode failed', + 'msg_start_failed_title': 'Error', + 'msg_start_failed': 'Program failed to start: {error}', + 'print_python_required': 'Error: Python 3.6 or later is required', + 'print_start_failed': 'Startup failed: {error}', + } + } + + self.base_dir = get_app_dir() + self.adb = find_tool('adb.exe', 'adb') + self.fastboot = find_tool('fastboot.exe', 'fastboot') + self.sz = find_tool('7za.exe') + self.package_file = find_resource("package.bin") + self.permission_resource_file = find_resource("EZ60_resource.dat") + self.extract_password = None + self.permission_resource_key = None + self.apps_dir = None + self.temp_dir = None + self.api_url = "https://api.changan.softwindy.cn/api/authorizations/auth-check" + self.boot_challenge_api_url = "https://api.changan.softwindy.cn/api/authorizations/boot-challenge" + self.boot_key_api_url = "https://api.changan.softwindy.cn/api/authorizations/boot-key" + self.debug_password_api_url = "https://api.changan.softwindy.cn/api/authorizations/verify-debug-mode-password" + self.tool_version = "Mazda-EZ60_1.2/1.2.0" + self.vin = None + self.vehicle_name = "" + self.device_connected = False + self._refreshing = False # 防止并发刷新 + self.debug_mode = False # 调试模式 + self.mazda_overlay_packages = [ + "com.tinnove.launcher.overlay", + "com.tinnove.scenemode.overlay", + "com.incall.dvr.overlay", + ] + self.mazda_disable_packages = [ + "com.carinno.p1", + "com.wtcl.electronicdirections", + "com.ximalaya.ting.android.car", + "com.tinnove.netease.music", + "com.migu.miguplay.car", + "cn.cmvideo.car.play", + "com.tinnove.carshow", + "com.tinnove.changba", + "com.qiyi.video.iv", + "com.changan.appmarket", + "com.incall.apps.softmanager" + ] + atexit.register(self.cleanup_cache_on_exit) + + # 设置样式 + self.setup_styles() + self.setup_ui() + self.root.protocol("WM_DELETE_WINDOW", self.on_close) + self.center_window() + self._clear_extracted_cache() + + # 检查环境 + 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) + + # 左侧内容区 + left_frame = tk.Frame(main_frame, bg=self.colors['bg_dark']) + left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # 右侧提示面板 + right_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1, width=235) + right_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=(10, 0)) + right_frame.pack_propagate(False) + + # 顶部标题栏 + title_frame = tk.Frame(left_frame, bg=self.colors['bg_dark'], height=65) + title_frame.pack(fill=tk.X, pady=(0, 10)) + title_frame.pack_propagate(False) + + title_content_frame = tk.Frame(title_frame, bg=self.colors['bg_dark']) + title_content_frame.pack(fill=tk.X, expand=True) + + # 标题 + self.title_label = tk.Label(title_content_frame, + text="🚀 " + self.t('title'), + font=('Microsoft YaHei', 18, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.title_label.pack(side=tk.LEFT, expand=True, padx=(0, 10)) + + self.btn_lang_switch = tk.Button(title_content_frame, text=self.t('lang_en'), + command=self.toggle_lang, + font=('Microsoft YaHei', 9, 'bold'), + fg='white', + bg=self.colors['accent'], + activeforeground='white', + activebackground=self.colors['accent_hover'], + relief=tk.FLAT, + cursor='hand2', + width=7, + height=1) + self.btn_lang_switch.pack(side=tk.RIGHT, padx=(8, 4)) + + # 工程密码查询区域 + pwd_query_frame = tk.Frame(left_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + pwd_query_frame.pack(fill=tk.X, pady=(0, 5), padx=5) + + pwd_query_row = tk.Frame(pwd_query_frame, bg=self.colors['bg_light']) + pwd_query_row.pack(fill=tk.X) + + self.pwd_query_label = tk.Label(pwd_query_row, text=self.t('pwd_query_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.pwd_query_label.pack(side=tk.LEFT, padx=(10, 5), pady=5) + + self.vin_input = tk.Entry(pwd_query_row, + font=('Consolas', 9), + bg='#2d2d3d', + fg='#636e72', + insertbackground='white', + relief=tk.FLAT, + width=20) + self.vin_input.insert(0, self.t('vin_placeholder')) + self.vin_input.bind("", 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_row, text=self.t('btn_query_pwd'), + command=self.query_password_by_vin, + font=('Microsoft YaHei', 8), + fg='white', + bg=self.colors['accent'], + relief=tk.FLAT, + cursor='hand2') + self.btn_query_pwd.pack(side=tk.LEFT, padx=5, pady=5) + + self.pwd_result_label = tk.Label(pwd_query_row, 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) + + self.vin_query_hint_label = tk.Label(pwd_query_frame, text=self.t('vin_query_hint'), + font=('Microsoft YaHei', 8, 'bold'), + fg=self.colors['warning'], + bg=self.colors['bg_light'], + anchor='w') + self.vin_query_hint_label.pack(fill=tk.X, padx=(10, 10), pady=(0, 6)) + + + # 工厂模式提示 + factory_hint_frame = tk.Frame(left_frame, bg=self.colors['bg_dark']) + factory_hint_frame.pack(fill=tk.X, pady=(0, 3)) + self.hint_label = tk.Label(factory_hint_frame, text=self.t('hint_factory'), + font=('Microsoft YaHei', 8), + fg=self.colors['warning'], + bg=self.colors['bg_dark']) + self.hint_label.pack(side=tk.LEFT, padx=2) + + # 按钮区域(两排,每排5个) + button_frame = tk.Frame(left_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + button_frame.pack(fill=tk.X, pady=(0, 10), padx=5) + + # 按钮样式参数 + btn_params = { + 'font': ('Microsoft YaHei', 9), + 'fg': 'white', + 'relief': tk.FLAT, + 'cursor': 'hand2', + 'height': 1, + 'width': 14 + } + + # 第一排按钮 + row1_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + row1_frame.pack(pady=(8, 4)) + + self.btn_permission = tk.Button(row1_frame, text=self.t('btn_permission'), + command=self.prepare_ez60_permission, + bg=self.colors['warning'], + **btn_params) + self.btn_permission.pack(side=tk.LEFT, padx=4) + + self.btn_push = tk.Button(row1_frame, text=self.t('btn_push'), + command=self.push_all_apks, + bg=self.colors['accent'], + **btn_params) + self.btn_push.pack(side=tk.LEFT, padx=4) + + self.btn_install_all = tk.Button(row1_frame, text=self.t('btn_install'), + command=self.install_apps, + bg=self.colors['accent'], + **btn_params) + self.btn_install_all.pack(side=tk.LEFT, padx=4) + + self.btn_language = tk.Button(row1_frame, text=self.t('btn_language'), + command=self.open_language_quick_set, + bg=self.colors['accent'], + **btn_params) + self.btn_language.pack(side=tk.LEFT, padx=4) + + # 第二排按钮 + row2_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + row2_frame.pack(pady=(4, 8)) + + self.btn_timezone = tk.Button(row2_frame, text=self.t('btn_timezone'), + command=self.open_timezone_settings, + bg=self.colors['accent'], + **btn_params) + self.btn_timezone.pack(side=tk.LEFT, padx=4) + + self.btn_settings = tk.Button(row2_frame, text=self.t('btn_settings'), + command=self.open_android_settings, + bg=self.colors['accent'], + **btn_params) + self.btn_settings.pack(side=tk.LEFT, padx=4) + + self.btn_reboot = tk.Button(row2_frame, text=self.t('btn_reboot'), + command=self.reboot_device, + bg=self.colors['warning'], + **btn_params) + self.btn_reboot.pack(side=tk.LEFT, padx=4) + + self.btn_exit = tk.Button(row2_frame, text=self.t('btn_disable_upgrade'), + command=self.on_disable_upgrade, + bg=self.colors['error'], + **btn_params) + self.btn_exit.pack(side=tk.LEFT, padx=4) + + # 设备状态栏(横条) + status_bar_frame = tk.Frame(left_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + status_bar_frame.pack(fill=tk.X, pady=(0, 5)) + + # 状态指示器 + status_indicator_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + status_indicator_frame.pack(side=tk.LEFT, padx=10, pady=5) + + self.status_indicator = tk.Canvas(status_indicator_frame, width=10, height=10, + bg=self.colors['bg_light'], highlightthickness=0) + self.status_indicator.pack(side=tk.LEFT) + self.status_dot = self.status_indicator.create_oval(2, 2, 8, 8, fill='#636e72') + + self.device_label = tk.Label(status_indicator_frame, text=self.t('device_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.device_label.pack(side=tk.LEFT, padx=(5, 3)) + + self.device_status_label = tk.Label(status_indicator_frame, text=self.t('status_detecting'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light']) + self.device_status_label.pack(side=tk.LEFT) + + # VIN信息 + vin_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + vin_frame.pack(side=tk.LEFT, padx=20, pady=5) + self.vin_label_title = tk.Label(vin_frame, text=self.t('vin_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.vin_label_title.pack(side=tk.LEFT) + self.vin_label = tk.Label(vin_frame, text=self.t('vin_none'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light']) + self.vin_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 授权状态 + auth_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + auth_frame.pack(side=tk.LEFT, padx=20, pady=5) + self.auth_label_title = tk.Label(auth_frame, text=self.t('auth_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.auth_label_title.pack(side=tk.LEFT) + self.auth_label = tk.Label(auth_frame, text=self.t('auth_none'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light']) + self.auth_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 刷新按钮 + self.btn_refresh = tk.Button(status_bar_frame, text=self.t('btn_refresh'), + command=lambda: self.refresh_device_status(force=True), + font=('Microsoft YaHei', 8), + fg=self.colors['accent'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + self.btn_refresh.pack(side=tk.RIGHT, padx=10, pady=5) + + # 解压进度条框架 + progress_frame = tk.Frame(left_frame, bg=self.colors['bg_dark']) + progress_frame.pack(fill=tk.X, pady=(5, 5)) + + self.progress_label = tk.Label(progress_frame, text="", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + self.progress_label.pack() + + self.progress = ttk.Progressbar(progress_frame, mode='determinate', style='TProgressbar') + self.progress.pack(fill=tk.X, pady=(2, 0)) + + # 推送进度条 + self.push_progress_label = tk.Label(progress_frame, text="", + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + + self.push_progress = ttk.Progressbar(progress_frame, mode='determinate', style='TProgressbar') + + # 日志区域(下方) + log_card = tk.Frame(left_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) + log_card.pack(fill=tk.BOTH, expand=True, pady=(5, 0)) + + # 日志标题栏 + log_title_frame = tk.Frame(log_card, bg=self.colors['bg_dark'], height=30) + log_title_frame.pack(fill=tk.X) + log_title_frame.pack_propagate(False) + + self.log_title_label = tk.Label(log_title_frame, text=self.t('log_title'), + font=('Microsoft YaHei', 10, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.log_title_label.pack(side=tk.LEFT, padx=10) + + self.btn_clear = tk.Button(log_title_frame, text=self.t('btn_clear_log'), + command=self.clear_log, + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark'], + relief=tk.FLAT, + cursor='hand2') + self.btn_clear.pack(side=tk.RIGHT, padx=10) + + # 日志文本框 + text_frame = tk.Frame(log_card, bg=self.colors['bg_light']) + text_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + self.log_text = scrolledtext.ScrolledText(text_frame, + height=12, + wrap=tk.WORD, + font=('Consolas', 9), + bg='#2d2d3d', + fg='#e0e0e0', + insertbackground='white', + relief=tk.FLAT, + borderwidth=0) + self.log_text.pack(fill=tk.BOTH, expand=True) + + # 配置日志颜色标签 + self.log_text.tag_config('INFO', foreground='#74b9ff') + self.log_text.tag_config('SUCCESS', foreground='#55efc4') + self.log_text.tag_config('ERROR', foreground='#ff7675') + self.log_text.tag_config('WARNING', foreground='#ffeaa7') + self.log_text.tag_config('CMD', foreground='#a29bfe') + + # 底部状态栏 + bottom_status = tk.Frame(left_frame, bg=self.colors['bg_light'], height=22) + bottom_status.pack(fill=tk.X, pady=(5, 0)) + bottom_status.pack_propagate(False) + + self.status_text = tk.Label(bottom_status, text=self.t('status_ready'), + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light']) + self.status_text.pack(side=tk.LEFT, padx=10) + + # 主题切换按钮 + self.btn_theme_switch = tk.Button(bottom_status, text=self.t('theme_dark'), + 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.root.bind('', self._toggle_debug) + + # ========== 右侧提示面板 ========== + # 热点信息卡片 + hotspot_card = tk.Frame(right_frame, bg=self.colors['bg_dark'], relief=tk.RAISED, bd=1) + hotspot_card.pack(fill=tk.X, padx=5, pady=(10, 5)) + + hotspot_title_row = tk.Frame(hotspot_card, bg=self.colors['bg_dark']) + hotspot_title_row.pack(anchor='w', fill=tk.X, padx=10, pady=(8, 5)) + + self.hotspot_icon_label = tk.Label(hotspot_title_row, text=self.t('hotspot_icon'), + font=('Segoe UI Emoji', 12), + fg=self.colors['accent'], + bg=self.colors['bg_dark'], + width=2, + anchor='center') + self.hotspot_icon_label.pack(side=tk.LEFT, padx=(0, 4)) + + self.hotspot_title_label = tk.Label(hotspot_title_row, text=self.t('hotspot_title'), + font=('Microsoft YaHei', 11, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark'], + anchor='w') + self.hotspot_title_label.pack(side=tk.LEFT, fill=tk.X, expand=True) + + self.hotspot_ssid_label = tk.Label(hotspot_card, text=self.t('hotspot_name_detecting'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_dark']) + self.hotspot_ssid_label.pack(anchor='w', padx=10, pady=2) + + self.hotspot_pwd_label = tk.Label(hotspot_card, text=self.t('hotspot_pwd_default'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_dark']) + self.hotspot_pwd_label.pack(anchor='w', padx=10, pady=2) + + self.hotspot_status_label = tk.Label(hotspot_card, text=self.t('hotspot_status_off'), + font=('Microsoft YaHei', 9), + fg=self.colors['warning'], + bg=self.colors['bg_dark']) + self.hotspot_status_label.pack(anchor='w', padx=10, pady=2) + + self.btn_hotspot = tk.Button(hotspot_card, text=self.t('hotspot_start'), + command=self.start_hotspot_action, + font=('Microsoft YaHei', 8), + fg='white', + bg=self.colors['accent'], + relief=tk.FLAT, + cursor='hand2') + self.btn_hotspot.pack(pady=8, padx=10, fill=tk.X) + + # 分隔线 + tk.Frame(right_frame, bg=self.colors['border'], height=1).pack(fill=tk.X, padx=8, pady=5) + + # 使用提示卡片 + hint_card = tk.Frame(right_frame, bg=self.colors['bg_dark'], relief=tk.RAISED, bd=1) + hint_card.pack(fill=tk.X, padx=5, pady=5) + + hint_title_row = tk.Frame(hint_card, bg=self.colors['bg_dark']) + hint_title_row.pack(anchor='w', fill=tk.X, padx=10, pady=(8, 5)) + + self.hint_icon_label = tk.Label(hint_title_row, text=self.t('hint_icon'), + font=('Segoe UI Emoji', 12), + fg=self.colors['warning'], + bg=self.colors['bg_dark'], + width=2, + anchor='center') + self.hint_icon_label.pack(side=tk.LEFT, padx=(0, 4)) + + self.hint_title_label = tk.Label(hint_title_row, text=self.t('hint_title'), + font=('Microsoft YaHei', 11, 'bold'), + fg=self.colors['warning'], + bg=self.colors['bg_dark'], + anchor='w') + self.hint_title_label.pack(side=tk.LEFT, fill=tk.X, expand=True) + + self.hint_lines_frame = tk.Frame(hint_card, bg=self.colors['bg_dark']) + self.hint_lines_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) + self._render_hint_lines() + + # 绑定悬停效果 + self.bind_hover_effects() + + def bind_hover_effects(self): + """绑定按钮悬停效果""" + buttons = [self.btn_permission, 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, + self.btn_hotspot] + + for btn in buttons: + original_bg = btn.cget('bg') + def on_enter(e, btn=btn, bg=original_bg): + btn.config(bg=self.lighten_color(bg)) + def on_leave(e, btn=btn, bg=original_bg): + btn.config(bg=bg) + btn.bind('', 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 set_window_icon(self): + """Set the Tk window/taskbar icon at runtime; PyInstaller --icon only sets the exe file icon.""" + try: + icon_path = find_resource("app.ico") + if icon_path.exists(): + self.root.iconbitmap(str(icon_path)) + if sys.platform == 'win32': + import ctypes + hwnd = self.root.winfo_id() + image = ctypes.windll.user32.LoadImageW( + None, str(icon_path), 1, 0, 0, 0x00000010 + ) + if image: + ctypes.windll.user32.SendMessageW(hwnd, 0x0080, 0, image) + ctypes.windll.user32.SendMessageW(hwnd, 0x0080, 1, image) + except Exception: + pass + + def center_window(self): + """将窗口居中显示在屏幕上""" + self.root.update_idletasks() + screen_w = self.root.winfo_screenwidth() + screen_h = self.root.winfo_screenheight() + win_w = self.root.winfo_reqwidth() + win_h = self.root.winfo_reqheight() + x = (screen_w - win_w) // 2 + y = (screen_h - win_h) // 2 + self.root.geometry(f"+{x}+{y}") + + def run_on_ui_thread(self, func, *args, **kwargs): + """将函数调度到主线程执行,确保线程安全""" + self.root.after(0, lambda: func(*args, **kwargs)) + + def t(self, key): + return self.T.get(self.lang, self.T['zh']).get(key, key) + + def tf(self, key, **kwargs): + try: + return self.t(key).format(**kwargs) + except Exception: + return self.t(key) + + def is_placeholder_vin(self, value): + return value in ( + self.T['zh'].get('vin_placeholder'), + self.T['en'].get('vin_placeholder'), + ) + + def toggle_lang(self): + self.lang = 'en' if self.lang == 'zh' else 'zh' + self.btn_lang_switch.config(text=self.t('lang_en') if self.lang == 'zh' else self.t('lang_zh')) + self._refresh_ui_texts() + self.log(self.t('log_lang_changed'), "INFO") + + def 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 _render_hint_lines(self): + if not hasattr(self, 'hint_lines_frame'): + return + for child in self.hint_lines_frame.winfo_children(): + child.destroy() + for line in self.t('hint_lines'): + tk.Label(self.hint_lines_frame, + text=line, + font=('Microsoft YaHei', 8), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark'], + justify=tk.LEFT, + anchor='w', + wraplength=190).pack(anchor='w', fill=tk.X, pady=1) + + def _refresh_ui_texts(self): + t = self.t + widgets = [ + (getattr(self, 'title_label', None), 'title', None), + (getattr(self, 'pwd_query_label', None), 'pwd_query_label', None), + (getattr(self, 'btn_permission', None), 'btn_permission', None), + (getattr(self, 'btn_push', None), 'btn_push', None), + (getattr(self, 'btn_install_all', None), 'btn_install', None), + (getattr(self, 'btn_language', None), 'btn_language', None), + (getattr(self, 'btn_timezone', None), 'btn_timezone', None), + (getattr(self, 'btn_settings', None), 'btn_settings', None), + (getattr(self, 'btn_reboot', None), 'btn_reboot', None), + (getattr(self, 'btn_exit', None), 'btn_disable_upgrade', None), + (getattr(self, 'btn_clear', None), 'btn_clear_log', None), + (getattr(self, 'btn_query_pwd', None), 'btn_query_pwd', None), + (getattr(self, 'log_title_label', None), 'log_title', None), + (getattr(self, 'status_text', None), 'status_ready', None), + (getattr(self, 'device_label', None), 'device_label', None), + (getattr(self, 'vin_label_title', None), 'vin_label', None), + (getattr(self, 'auth_label_title', None), 'auth_label', None), + (getattr(self, 'btn_refresh', None), 'btn_refresh', None), + (getattr(self, 'hint_label', None), 'hint_factory', None), + (getattr(self, 'vin_query_hint_label', None), 'vin_query_hint', None), + (getattr(self, 'hotspot_icon_label', None), 'hotspot_icon', None), + (getattr(self, 'hotspot_title_label', None), 'hotspot_title', None), + (getattr(self, 'btn_hotspot', None), 'hotspot_start', None), + (getattr(self, 'hint_icon_label', None), 'hint_icon', None), + (getattr(self, 'hint_title_label', None), 'hint_title', None), + ] + for w, key, _ in widgets: + if not w: + continue + text = t(key) + if key == 'title': + text = "🚀 " + text + w.config(text=text) + self.btn_theme_switch.config(text=t('theme_light') if self.theme == 'dark' else t('theme_dark')) + self.btn_lang_switch.config(text=t('lang_en') if self.lang == 'zh' else t('lang_zh')) + if self.is_placeholder_vin(self.vin_input.get()): + self.vin_input.delete(0, tk.END) + self.vin_input.insert(0, t('vin_placeholder')) + self._render_hint_lines() + self.refresh_hotspot_display() + if self.vin: + self._update_device_status_impl(self.device_connected, self.vin, + getattr(self, '_last_authorized', False)) + + def _sanitize_user_log_message(self, message): + """Hide low-level commands, paths, package names, and APK names in normal logs. + VIN and vehicle names are operator-facing identifiers and are intentionally kept visible. + """ + text = str(message) + replacements = [ + (r'com\.[\w.\-]+', '相关应用'), + (r'cn\.[\w.\-]+', '相关应用'), + (r'[\w.\-]+\.apk', '文件'), + (r'[\w.\-]+\.img', '文件'), + (r'EZ60_resource\.dat', '资源文件'), + (r'package\.bin', '资源文件'), + (r'7za(?:\.exe)?', '资源工具'), + (r'adb(?:\.exe)?', '设备连接工具'), + (r'fastboot(?:\.exe)?', '设备工具'), + (r'init_boot', '系统资源'), + (r'pm\s+\S+', '系统操作'), + (r'cmd\s+overlay\s+\S+', '系统配置'), + (r'(? 0 + if has_app: + ok, reason = self._validate_extracted_apks() + if not ok: + self.log(self.tf('log_cache_invalid', reason=reason), "ERROR") + self._clear_extracted_cache() + return False + return has_app + + def _validate_extracted_apks(self): + if not self.apps_dir or not self.apps_dir.exists(): + return False, self.t('err_missing_apps') + apks = list(self.apps_dir.glob("*.apk")) + if not apks: + return False, self.t('err_empty_apps') + zero_apks = [apk.name for apk in apks if apk.stat().st_size <= 0] + if zero_apks: + preview = ", ".join(zero_apks[:5]) + suffix = "..." if len(zero_apks) > 5 else "" + return False, self.tf('err_zero_apks', files=f"{preview}{suffix}") + return True, "" + + def _cache_dir_path(self): + local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) + return Path(local_appdata) / ".cache" / "system" / ".android" / self.CACHE_DIR_NAME + + def _remove_dir_tree(self, path): + if not path or not path.exists(): + return + for _ in range(3): + try: + if sys.platform == 'win32': + subprocess.run( + f'attrib -r -s -h "{path}" /s /d', + shell=True, + capture_output=True, + creationflags=subprocess.CREATE_NO_WINDOW + ) + shutil.rmtree(path, ignore_errors=False) + return + except Exception: + time.sleep(0.3) + shutil.rmtree(path, ignore_errors=True) + + def _clear_extracted_cache(self): + cache_dirs = [] + if self.temp_dir: + cache_dirs.append(self.temp_dir) + cache_dirs.append(self._cache_dir_path()) + + seen = set() + for cache_dir in cache_dirs: + try: + resolved = cache_dir.resolve() + except Exception: + resolved = cache_dir + if resolved in seen: + continue + seen.add(resolved) + self._remove_dir_tree(cache_dir) + + time.sleep(0.2) + self.apps_dir = None + self.temp_dir = None + + def _schedule_cache_cleanup_after_exit(self): + if sys.platform != 'win32': + return + cache_dir = str(self._cache_dir_path()) + ps_command = ( + "Start-Sleep -Seconds 2; " + f"$p = '{cache_dir}'; " + "if (Test-Path -LiteralPath $p) { " + "attrib -r -s -h $p /s /d 2>$null; " + "Remove-Item -LiteralPath $p -Recurse -Force -ErrorAction SilentlyContinue " + "}" + ) + try: + subprocess.Popen( + ['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-Command', ps_command], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + creationflags=subprocess.CREATE_NO_WINDOW + ) + except Exception: + pass + + def cleanup_cache_on_exit(self): + self._clear_extracted_cache() + self._schedule_cache_cleanup_after_exit() + + def on_close(self): + self.cleanup_cache_on_exit() + self.root.destroy() + + def _format_extract_error(self, err_msg, return_code): + text = (err_msg or "").lower() + if any(marker in text for marker in ( + "wrong password", + "incorrect password", + "password is incorrect", + "data error in encrypted file", + "can not open encrypted archive", + )): + return self.t('err_extract_wrong_password') + if "data error" in text: + return self.t('err_extract_data') + if "headers error" in text or "unexpected end" in text: + return self.t('err_extract_headers') + if err_msg.strip(): + return self.tf('err_extract_detail', error=err_msg.strip()[:300]) + return self.tf('err_extract_code', code=return_code) + + def _decode_7z_output(self, *outputs): + """解码 7za 输出,兼容中文 Windows 控制台编码。""" + parts = [] + for output in outputs: + if not output: + continue + for enc in ('gbk', 'utf-8'): + try: + parts.append(output.decode(enc, errors='replace')) + break + except Exception: + continue + return ''.join(parts).strip() + + def _extract_7za_with_progress(self): + """流式运行 7za 并解析百分比输出。""" + cmd = [self.sz, 'x', str(self.package_file), f'-p{self.extract_password}', f'-o{self.temp_dir}', '-y'] + supports_progress = getattr(self, '_seven_zip_supports_progress_stream', lambda: False)() + if supports_progress: + cmd.extend(['-bsp1', '-bso0', '-bse1']) + + if getattr(self, 'debug_mode', False): + self.log(f"7ZA CMD: {subprocess.list2cmdline(cmd)}", "CMD") + + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.DEVNULL, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + + output = bytearray() + progress_window = 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) + progress_window.extend(chunk) + if len(progress_window) > 1024: + del progress_window[:-1024] + if not self.debug_mode and len(output) > 60000: + del output[:-60000] + + matches = re.findall(rb"(\d{1,3})%", bytes(progress_window[-512:])) + if matches: + percent = min(100, int(matches[-1])) + if percent != last_percent: + last_percent = percent + self.update_progress(percent, 100, self.t('progress_loading')) + + return_code = proc.wait() + decoded_output = self._decode_7z_output(bytes(output)) + + if getattr(self, 'debug_mode', False): + self.log(f"7ZA RET: {return_code}", "CMD" if return_code == 0 else "ERROR") + if decoded_output.strip(): + self.log(f"7ZA OUTPUT:\n{decoded_output.strip()}", "CMD" if return_code == 0 else "ERROR") + + return return_code, decoded_output + + def _seven_zip_supports_progress_stream(self): + """检测当前 7za 是否支持进度流参数。""" + try: + result = subprocess.run( + [self.sz], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + output = self._decode_7z_output(result.stdout, result.stderr) + return '-bs{o|e|p}' in output + except Exception: + return False + + def extract_package_silent(self): + """静默解压语言包(带进度)—— 逸动版仅处理 app 目录""" + if not self.package_file.exists(): + self.log(self.tf('log_package_missing', path=self.package_file), "ERROR") + return False + + if not self.extract_password: + self.log(self.t('log_extract_password_missing'), "ERROR") + return False + + if not os.path.exists(self.sz): + self.log(self.tf('log_7za_missing', path=self.sz), "ERROR") + return False + + try: + # 使用用户目录,无需管理员权限 + hidden_path = self._cache_dir_path().parent + hidden_path.mkdir(parents=True, exist_ok=True) + + self.temp_dir = self._cache_dir_path() + + # 如果已存在,先清理 + 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(self.t('log_extracting'), "INFO") + + self.update_progress(0, 100, self.t('progress_loading')) + return_code, err_msg = self._extract_7za_with_progress() + if return_code != 0: + self.log(self._format_extract_error(err_msg, return_code), "ERROR") + self._clear_extracted_cache() + return False + self.update_progress(100, 100, self.t('progress_loaded')) + + # 查找 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(self.t('log_apps_missing'), "WARNING") + self._clear_extracted_cache() + return False + + apk_count = len(list(self.apps_dir.glob("*.apk"))) + ok, reason = self._validate_extracted_apks() + if not ok: + self.log(self.tf('log_resource_invalid', reason=reason), "ERROR") + self._clear_extracted_cache() + return False + self.log(self.tf('log_resource_ready', count=apk_count), "SUCCESS") + return True + + except Exception as e: + if getattr(self, 'debug_mode', False): + self.log(self.tf('log_data_prepare_failed', error=str(e)), "ERROR") + import traceback + self.log(traceback.format_exc(), "ERROR") + else: + self.log(self.t('log_resource_failed'), "ERROR") + self._clear_extracted_cache() + return False + + def check_environment(self): + """检查环境""" + # 修改hosts文件 + self.modify_hosts() + # 刷新热点显示 + self.refresh_hotspot_display() + try: + result = subprocess.run(f'{self.adb} version', shell=True, capture_output=True, text=True) + if result.returncode == 0: + self.refresh_device_status() + if not self.package_file.exists(): + self.log(self.t('log_no_package'), "WARNING") + if not os.path.exists(self.fastboot): + self.log(self.t('log_no_fastboot'), "WARNING") + else: + self.log(self.t('log_no_adb'), "ERROR") + except FileNotFoundError: + self.log(self.t('log_no_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(self.t('log_device_connected'), "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(self.tf('log_vin', vin=vin), "SUCCESS") + authorized = self.check_authorization(vin) + self.update_device_status(True, vin, authorized) + else: + self.log(self.t('log_vin_unavailable'), "WARNING") + self.update_device_status(True, None, False) + else: + if was_connected: + self.log(self.t('log_device_disconnected'), "WARNING") + self.update_device_status(False) + + self._refreshing = False + + threading.Thread(target=refresh, daemon=True).start() + + def query_authorization_info(self, vin): + 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')) + payload = data.get('data', {}) if isinstance(data, dict) else {} + vehicle_name = payload.get('vehicleName') or payload.get('vehicle_name') or "" + vehicle_name = str(vehicle_name).strip() + if data.get('authorized') is True and vehicle_name: + self.vehicle_name = vehicle_name + return data.get('authorized') is True, vehicle_name, data + + def _post_json(self, url, payload, timeout=10): + body = json.dumps(payload).encode('utf-8') + req = Request( + url, + data=body, + method='POST', + headers={ + 'User-Agent': 'Mozilla/5.0', + 'Content-Type': 'application/json', + }, + ) + with urlopen(req, timeout=timeout) as response: + return json.loads(response.read().decode('utf-8')) + + def check_authorization(self, vin): + """检查授权""" + if self.debug_mode: + self.log(self.t('log_debug_skip_auth'), "WARNING") + return True + self.log(self.t('log_auth_checking'), "SUCCESS") + + try: + authorized, vehicle_name, _ = self.query_authorization_info(vin) + if authorized: + self.log(self.t('log_auth_success'), "SUCCESS") + if vehicle_name: + self.log(self.tf('log_vehicle_name', vehicle=vehicle_name), "SUCCESS") + return True + self.log(self.t('log_auth_failed'), "ERROR") + return False + + except Exception: + if self.debug_mode: + import traceback + self.log(traceback.format_exc(), "ERROR") + self.log(self.t('log_auth_failed'), "ERROR") + return False + + def fetch_package_password(self): + """从服务端获取资源包解压密码""" + if not self.vin: + self.log(self.t('log_need_adb'), "ERROR") + return False + + try: + vehicle_name = self.vehicle_name + if not vehicle_name: + authorized, vehicle_name, _ = self.query_authorization_info(self.vin) + if not authorized: + self.log(self.t('log_auth_failed'), "ERROR") + return False + if not vehicle_name: + self.log(self.t('log_no_vehicle_name'), "ERROR") + return False + + pwd_api_url = "https://api.changan.softwindy.cn/api/authorizations/package-key" + query = urlencode({ + "vin": self.vin, + "vehicleName": vehicle_name, + }) + url = f"{pwd_api_url}?{query}" + if self.debug_mode: + self.log(f"PACKAGE KEY URL: {url}", "CMD") + 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'] + if self.debug_mode: + self.log("PACKAGE KEY: password received", "CMD") + return True + else: + if self.debug_mode: + self.log(f"PACKAGE KEY RESPONSE: {data}", "CMD") + self.log(self.tf('log_data_prepare_failed', error=data.get('message', 'unknown error')), "ERROR") + return False + + except Exception as e: + if self.debug_mode: + import traceback + self.log(traceback.format_exc(), "ERROR") + self.log(self.tf('log_data_prepare_failed', error=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, self.tf('err_push_failed', error=err) + + ok, err = self.run_adb_shell(f'pm install -r -d {temp_apk_path}') + self.run_adb_shell(f'rm -f {temp_apk_path}') + if not ok: + return False, self.tf('err_install_failed', error=err) + + return True, "" + + def collect_boot_device_info(self): + info = { + "vin": self.vin or "", + "toolVersion": self.tool_version, + } + ok, output = self.run_adb_command('adb -d get-serialno') + if ok: + info["adbSerial"] = output.strip() + + props = { + "ro.serialno": "roSerialno", + "ro.boot.serialno": "roBootSerialno", + "ro.product.manufacturer": "manufacturer", + "ro.product.model": "model", + "ro.product.device": "device", + "ro.build.fingerprint": "fingerprint", + } + for prop, key in props.items(): + ok, output = self.run_adb_shell(f'getprop {prop}') + if ok: + info[key] = output.strip() + return {k: str(v).strip() for k, v in info.items() if str(v).strip()} + + def _decode_key_material(self, key_text): + text = str(key_text).strip() + if text.startswith("sha256:"): + return bytes.fromhex(text.split(":", 1)[1]) + if len(text) == 64 and all(c in '0123456789abcdefABCDEF' for c in text): + return bytes.fromhex(text) + padded = text + ("=" * (-len(text) % 4)) + return base64.urlsafe_b64decode(padded.encode('ascii')) + + def fetch_permission_resource_key(self): + """通过 boot challenge 协议获取 EZ60 init_boot 资源解密密钥。""" + if self.permission_resource_key: + return True + if not self.vin: + self.log(self.t('log_need_adb'), "ERROR") + return False + + try: + device_info = self.collect_boot_device_info() + challenge_data = self._post_json(self.boot_challenge_api_url, { + "vin": self.vin, + "toolVersion": self.tool_version, + "deviceInfo": device_info, + }) + challenge_payload = challenge_data.get('data', {}) if isinstance(challenge_data, dict) else {} + challenge_id = str(challenge_payload.get('challengeId') or '').strip() + nonce = str(challenge_payload.get('nonce') or '').strip() + if not challenge_data.get('success') or not challenge_id or not nonce: + self.log(self.tf('log_boot_challenge_failed', error=challenge_data.get('message', 'unknown error')), "ERROR") + return False + + key_data = self._post_json(self.boot_key_api_url, { + "vin": self.vin, + "challengeId": challenge_id, + "nonce": nonce, + "timestamp": int(time.time() * 1000), + "toolVersion": self.tool_version, + "deviceInfo": device_info, + }) + key_payload = key_data.get('data', {}) if isinstance(key_data, dict) else {} + key_text = key_payload.get('sessionKey') + if not key_data.get('success') or not key_text: + self.log(self.tf('log_boot_key_failed', error=key_data.get('message', 'unknown error')), "ERROR") + return False + key_bytes = self._decode_key_material(key_text) + if len(key_bytes) != 32: + self.log(self.t('log_boot_key_len_error'), "ERROR") + return False + self.permission_resource_key = key_bytes + self.log(self.t('log_boot_key_success'), "INFO") + return True + except Exception as e: + if self.debug_mode: + import traceback + self.log(traceback.format_exc(), "ERROR") + self.log(self.tf('log_boot_key_failed', error=str(e)), "ERROR") + return False + + def _parse_permission_resource_payload(self, blob): + if blob.startswith(b'EZ60R2\x00'): + header_len = struct.unpack('>I', blob[7:11])[0] + header_start = 11 + header_end = header_start + header_len + payload = json.loads(blob[header_start:header_end].decode('utf-8')) + ciphertext = blob[header_end:] + return payload, ciphertext + if blob.startswith(b'Q05R2\x00'): + header_len = struct.unpack('>I', blob[6:10])[0] + header_start = 10 + header_end = header_start + header_len + payload = json.loads(blob[header_start:header_end].decode('utf-8')) + ciphertext = blob[header_end:] + return payload, ciphertext + + payload = json.loads(blob.decode('utf-8')) + ciphertext = base64.urlsafe_b64decode(payload['ciphertext'] + "=" * (-len(payload['ciphertext']) % 4)) + return payload, ciphertext + + def decrypt_permission_resource_to_temp_file(self): + """解密 EZ60_resource.dat 到随机临时 img 文件,调用者必须尽快删除。""" + if AESGCM is None: + self.log(self.t('log_crypto_missing'), "ERROR") + return None + if not self.permission_resource_file.exists(): + self.log(self.tf('log_permission_resource_missing', path=self.permission_resource_file), "ERROR") + return None + if not self.fetch_permission_resource_key(): + return None + + try: + blob = self.permission_resource_file.read_bytes() + payload, ciphertext = self._parse_permission_resource_payload(blob) + if payload.get('format') not in ('ez60-resource-v2', 'q05-lidar-resource-v2', 'q05-lidar-resource-v1'): + self.log(self.t('log_permission_resource_format_unsupported'), "ERROR") + return None + if payload.get('cipher') != 'AES-256-GCM': + self.log(self.t('log_permission_resource_algorithm_unsupported'), "ERROR") + return None + + nonce = base64.urlsafe_b64decode(payload['nonce'] + "=" * (-len(payload['nonce']) % 4)) + aad = payload.get('aad', 'Mazda-EZ60 init_boot resource v1').encode('utf-8') + plain = AESGCM(self.permission_resource_key).decrypt(nonce, ciphertext, aad) + if payload.get('compression') == 'zlib': + plain = zlib.decompress(plain) + + expected_sha = payload.get('sha256', '').lower() + actual_sha = hashlib.sha256(plain).hexdigest() + if expected_sha and actual_sha != expected_sha: + self.log(self.t('log_permission_resource_decrypt_auth_failed'), "ERROR") + return None + + fd, temp_name = tempfile.mkstemp(prefix='ez60_', suffix='.img') + try: + with os.fdopen(fd, 'wb') as fp: + fp.write(plain) + fp.flush() + os.fsync(fp.fileno()) + finally: + plain = b'' + self.log(self.t('log_permission_resource_decrypt_ready'), "INFO") + return Path(temp_name) + except Exception as e: + if self.debug_mode: + import traceback + self.log(traceback.format_exc(), "ERROR") + self.log(self.tf('log_permission_resource_decrypt_failed', error=str(e) or e.__class__.__name__), "ERROR") + return None + + def secure_delete_file(self, path): + """尽力覆盖并删除临时镜像。""" + try: + p = Path(path) + if not p.exists(): + return + size = p.stat().st_size + with p.open('r+b') as fp: + first_chunk = min(size, 1024 * 1024) + fp.write(os.urandom(first_chunk)) + remaining = size - first_chunk + zero = b'\x00' * 1024 * 1024 + while remaining > 0: + chunk = min(remaining, len(zero)) + fp.write(zero[:chunk]) + remaining -= chunk + fp.flush() + os.fsync(fp.fileno()) + p.unlink() + self.log(self.t('log_temp_img_deleted'), "INFO") + except Exception as e: + self.log(self.tf('log_temp_img_delete_failed', error=e), "WARNING") + + def run_fastboot_command(self, args, timeout=60): + command = [self.fastboot] + list(args) + if self.debug_mode: + self.log("CMD: " + " ".join(f'"{x}"' if " " in str(x) else str(x) for x in command), "CMD") + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + encoding='utf-8', + errors='replace', + timeout=timeout, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + output = ((result.stdout or "") + "\n" + (result.stderr or "")).strip() + if result.returncode == 0: + return True, output + return False, output + except subprocess.TimeoutExpired: + return False, self.t('err_fastboot_timeout') + except Exception as e: + return False, str(e) + + def fastboot_device_connected(self, output): + for line in str(output or "").splitlines(): + parts = line.strip().split() + if len(parts) >= 2 and parts[1].lower() == "fastboot": + return True + return False + + def fastboot_output_has_okay(self, output): + text = str(output or "").upper() + return "OKAY" in text and "FAILED" not in text + + def wait_for_fastboot(self, timeout=180, interval=5): + deadline = time.time() + timeout + while time.time() < deadline: + ok, output = self.run_fastboot_command(['devices'], timeout=10) + if ok and self.fastboot_device_connected(output): + return True + time.sleep(interval) + return False + + def prepare_ez60_permission(self): + """获取权限:重启到 fastboot、刷入 init_boot、立即重启。""" + if not self.check_device_connection(): + return + if not self.vin: + messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_need_vin')) + return + + answer = messagebox.askyesno( + self.t('msg_confirm_permission_title'), + self.t('msg_confirm_permission') + ) + if not answer: + return + + def worker(): + temp_img = None + permission_done = False + try: + if not self.check_authorization(self.vin): + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized')) + ) + return + + self.show_progress(True, is_push=True) + self.update_progress(1, 5, self.t('progress_fetch_boot_key'), is_push=True) + if not self.fetch_permission_resource_key(): + return + + self.update_progress(2, 5, self.t('progress_reboot_fastboot'), is_push=True) + ok, output = self.run_adb_shell('reboot fastboot') + if not ok and self.debug_mode: + self.log(self.tf('log_fastboot_enter_failed', output=output), "CMD") + self.log(self.t('log_fastboot_wait'), "WARNING") + if not self.wait_for_fastboot(): + self.log(self.t('log_fastboot_missing'), "ERROR") + return + + self.update_progress(3, 5, self.t('progress_decrypt_init_boot'), is_push=True) + temp_img = self.decrypt_permission_resource_to_temp_file() + if not temp_img: + return + + self.update_progress(4, 5, self.t('progress_flash_init_boot'), is_push=True) + ok, output = self.run_fastboot_command(['flash', 'init_boot', str(temp_img)], timeout=120) + if not ok or not self.fastboot_output_has_okay(output): + self.log(self.tf('log_init_boot_flash_failed', output=output), "ERROR") + return + self.log(self.t('log_init_boot_flash_success'), "INFO") + + self.update_progress(5, 5, self.t('progress_reboot_device'), is_push=True) + ok, output = self.run_fastboot_command(['reboot'], timeout=30) + if ok: + permission_done = True + self.log(self.t('log_permission_success'), "SUCCESS") + self.run_on_ui_thread( + lambda: messagebox.showinfo(self.t('msg_done_title'), self.t('msg_permission_done')) + ) + else: + self.log(self.tf('log_fastboot_reboot_failed', output=output), "WARNING") + finally: + self.permission_resource_key = None + if temp_img: + self.secure_delete_file(temp_img) + self.show_progress(False, is_push=True) + if not permission_done: + self.log(self.t('log_permission_failed'), "ERROR") + + threading.Thread(target=worker, daemon=True).start() + + 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 run_mazda_post_install_tasks(self): + """语言包安装完成后启用 Mazda overlay 并禁用指定应用。""" + self.log(self.t('log_post_config_start'), "INFO") + + overlay_ok = True + for package_name in self.mazda_overlay_packages: + ok, output = self.run_adb_shell(f'cmd overlay enable {package_name}') + if ok: + if self.debug_mode: + self.log(self.tf('log_overlay_enabled', package=package_name), "SUCCESS") + else: + overlay_ok = False + if self.debug_mode: + self.log(self.tf('log_overlay_failed', package=package_name, output=output), "ERROR") + + disabled_count = 0 + for package_name in self.mazda_disable_packages: + ok, output = self.run_adb_shell(f'pm disable-user {package_name}') + if ok: + disabled_count += 1 + if self.debug_mode: + self.log(self.tf('log_disabled_package', package=package_name), "SUCCESS") + else: + if self.debug_mode: + self.log(self.tf('log_disable_package_failed', package=package_name, output=output), "ERROR") + + if self.debug_mode: + self.log( + self.tf( + 'log_post_config_done', + overlay_count=len(self.mazda_overlay_packages), + disabled_count=disabled_count, + total_count=len(self.mazda_disable_packages), + ), + "INFO", + ) + return overlay_ok and disabled_count == len(self.mazda_disable_packages) + + def push_all_apks(self): + """推送APK并安装 —— 逸动版仅处理 app 目录,使用 pm install""" + # 检查设备连接(仅 UI 层检查在主线程,其余工作进后台线程) + if not self.check_device_connection(): + return + + if not self.vin: + messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_need_vin')) + return + + def do_push_all(): + # 验证授权 + if not self.check_authorization(self.vin): + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized')) + ) + return + + # 获取解压密码 + if not self.extract_password: + if not self.fetch_package_password(): + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_data_prepare_failed')) + ) + return + + # 解压 + if not self.check_package_extracted(): + self.log(self.t('log_extracting'), "INFO") + self.show_progress(True, is_push=False) + if not self.extract_package_silent(): + self.show_progress(False, is_push=False) + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_prepare_failed')) + ) + return + self.show_progress(False, is_push=False) + + if not self.apps_dir or not self.apps_dir.exists(): + self.run_on_ui_thread( + lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_dir_missing')) + ) + return + + # 开始刷入 + self.show_progress(True, is_push=True) + self.log(self.t('log_flash_start'), "SUCCESS") + 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(self.t('log_no_language_files'), "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: + if self.debug_mode: + self.log(self.tf('log_install_success', name=apk_name), "SUCCESS") + success_count += 1 + else: + if self.debug_mode: + self.log(self.tf('log_install_failed', name=apk_name), "ERROR") + else: + self.log(self.tf('log_flash_item_failed', current=i, total=total), "ERROR") + self.update_progress(i, total, self.t('progress_flashing'), is_push=True) + + self.update_progress(total, total, self.t('progress_flash_done'), is_push=True) + self.run_adb_shell('setprop vecentek.model 0') + + if success_count == total: + self.log(self.t('log_flash_done_config'), "SUCCESS") + elif success_count > 0: + self.log(self.t('log_flash_partial_config'), "WARNING") + else: + self.log(self.t('log_flash_failed_config'), "WARNING") + + if self.run_mazda_post_install_tasks(): + self.log(self.t('log_post_config_all_done'), "SUCCESS") + else: + self.log(self.t('log_post_config_partial'), "WARNING") + + 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(self.t('msg_warn_title'), self.t('msg_need_vin')) + return + if not self.check_authorization(self.vin): + messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized_action')) + return + + # 使用当前目录下的apks文件夹 + apk_dir = self.base_dir / "apks" + if not apk_dir.exists(): + apk_dir = find_resource("apks") + + # 检查apk文件夹是否存在 + if not apk_dir.exists(): + messagebox.showerror(self.t('msg_error_title'), self.t('msg_apks_dir_missing')) + self.log(self.t('msg_apks_dir_missing'), "ERROR") + return + + # 查找所有apk文件 + apk_files = list(apk_dir.glob("*.apk")) + if not apk_files: + messagebox.showerror(self.t('msg_error_title'), self.t('msg_apks_empty')) + self.log(self.t('msg_apks_empty'), "ERROR") + return + + # 询问是否确认安装 + result = messagebox.askyesno( + self.t('msg_install_confirm_title'), + self.tf('msg_install_confirm_folder', count=len(apk_files)) + ) + if not result: + return + + def install(): + self.show_progress(True, is_push=True) + total = len(apk_files) + self.log(self.tf('log_batch_install_start', count=total), "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, self.t('progress_installing'), is_push=True) + if self._push_and_install(apk_path, apk_name): + self.log(self.tf('log_install_success', name=apk_name), "SUCCESS") + success_count += 1 + else: + self.log(self.tf('log_install_failed', name=apk_name), "ERROR") + + self.run_adb_shell('setprop vecentek.model 0') + self.update_progress(total, total, self.t('progress_install_done'), is_push=True) + self.show_progress(False, is_push=True) + + if success_count == total: + self.run_on_ui_thread( + messagebox.showinfo, + self.t('msg_install_done_title'), + self.tf('msg_install_done_all', count=total) + ) + elif success_count > 0: + self.run_on_ui_thread( + messagebox.showwarning, + self.t('msg_install_partial_title'), + self.tf('msg_install_partial', success=success_count, failed=total - success_count) + ) + else: + self.log(self.t('log_install_failed_simple'), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_install_failed_title'), + self.t('msg_install_failed_all') + ) + + 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(self.t('msg_warn_title'), self.t('msg_need_vin')) + return + if not self.check_authorization(self.vin): + messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized_action')) + return + + file_path = filedialog.askopenfilename( + title=self.t('file_select_apk_title'), + filetypes=[(self.t('filetype_apk'), "*.apk"), (self.t('filetype_all'), "*.*")] + ) + + 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, self.t('progress_installing'), 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, self.t('progress_done'), is_push=True) + if success: + self.log(self.tf('log_install_success', name=apk_name), "SUCCESS") + else: + self.log(self.t('log_install_failed_simple'), "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(self.t('quick_lang_title')) + popup.geometry("520x320") + popup.configure(bg=self.colors['bg_dark']) + popup.resizable(False, False) + + # 居中显示 + popup.update_idletasks() + x = self.root.winfo_x() + (self.root.winfo_width() - 520) // 2 + y = self.root.winfo_y() + (self.root.winfo_height() - 320) // 2 + popup.geometry(f"+{x}+{y}") + popup.transient(self.root) + popup.grab_set() + + # 标题 + header = tk.Label(popup, text=self.t('quick_lang_header'), + font=('Microsoft YaHei', 13, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + header.pack(pady=(15, 10)) + + hint = tk.Label(popup, text=self.t('quick_lang_hint'), + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + hint.pack(pady=(0, 12)) + + # 语言列表:(显示名, locale_code) + locale_codes = ["zh-CN", "en-US", "ru-RU", "fr-FR", "es-ES", "pt-BR", "it-IT", "ar-SA"] + languages = list(zip(self.t('quick_lang_names'), locale_codes)) + + # 创建按钮容器 + btn_frame = tk.Frame(popup, bg=self.colors['bg_dark']) + btn_frame.pack(pady=(0, 10)) + + btn_colors = [ + self.colors['accent'], self.colors['info'], + self.colors['success'], self.colors['warning'], + '#e17055', '#00b894', + '#6c5ce7', '#0984e3', + ] + + for i, (label, locale) in enumerate(languages): + row = i // 4 + col = i % 4 + + def make_cmd(loc=locale, lbl=label): + return lambda: self._quick_set_language(loc, lbl, popup) + + btn = tk.Button(btn_frame, text=label, + command=make_cmd(), + font=('Microsoft YaHei', 10), + fg='white', + bg=btn_colors[i], + relief=tk.FLAT, + cursor='hand2', + width=12, height=2) + btn.grid(row=row, column=col, padx=5, pady=5) + + # 底部分隔 + 打开系统设置入口 + sep = tk.Frame(popup, bg=self.colors['border'], height=1) + sep.pack(fill=tk.X, padx=20, pady=(8, 6)) + + sys_btn = tk.Button(popup, text=self.t('quick_lang_system'), + command=lambda: self._open_sys_and_close(popup), + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + sys_btn.pack(pady=(0, 10)) + + def _quick_set_language(self, locale_code, language_name, popup): + """执行快捷语言设置""" + popup.destroy() + + def do_set(): + self.log(f"{self.t('progress_installing')}: {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") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('quick_lang_success_title'), + self.tf('quick_lang_success', language=language_name) + ) + else: + self.log(f"✗ {output}", "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('quick_lang_failed_title'), + self.tf('quick_lang_failed', output=output) + ) + + threading.Thread(target=do_set, daemon=True).start() + + def _open_sys_and_close(self, popup): + """关闭弹窗并打开系统语言设置""" + popup.destroy() + self.open_language_settings() + + def open_timezone_settings(self): + """打开时区设置""" + if not self.check_device_connection(): + return + self.run_adb_shell('am start -a android.settings.TIMEZONE_SETTINGS') + + def open_android_settings(self): + """打开安卓原生设置""" + if not self.check_device_connection(): + return + self.run_adb_shell('am start -a android.settings.SETTINGS') + + def reboot_device(self): + """重启设备""" + if not self.check_device_connection(): + return + if messagebox.askyesno(self.t('msg_reboot_title'), self.t('msg_reboot_confirm')): + self.run_adb_shell('reboot') + self.log(self.t('log_rebooting'), "INFO") + self.update_device_status(False) + + def on_disable_upgrade(self): + """禁用系统升级""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 弹窗确认 + result = messagebox.askyesno( + self.t('msg_disable_ota_title'), + self.t('msg_disable_ota_confirm') + ) + + if not result: + self.log(self.t('log_disable_ota_cancelled'), "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(self.t('log_disable_ota_success'), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('msg_success_title'), self.t('msg_disable_ota_success')) + else: + self.log(self.t('log_disable_ota_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.tf('msg_disable_ota_failed', output=output)) + + self.show_progress(False, is_push=False) + + threading.Thread(target=disable, daemon=True).start() + + def _on_vin_input_focus_in(self, event): + """输入框获得焦点时清除占位符""" + if self.is_placeholder_vin(self.vin_input.get()): + self.vin_input.delete(0, tk.END) + self.vin_input.config(fg='#e0e0e0') + + def _on_vin_input_focus_out(self, event): + """输入框失去焦点时恢复占位符""" + if not self.vin_input.get(): + self.vin_input.insert(0, self.t('vin_placeholder')) + self.vin_input.config(fg='#636e72') + + def query_password_by_vin(self): + """通过VIN查询密码""" + vin = self.vin_input.get().strip() + if not vin or self.is_placeholder_vin(vin): + messagebox.showwarning(self.t('msg_hint_title'), self.t('msg_input_vin')) + return + + def do_query(): + try: + api_url = "https://api.changan.softwindy.cn/api/authorizations/generate-password-by-vin" + url = f"{api_url}?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', 'unknown') + self.pwd_result_label.config( + text=self.tf('pwd_success', password=pwd), + fg=self.colors['success'] + ) + self.log(self.tf('log_pwd_success', vin=vin, password=pwd), "SUCCESS") + else: + msg = data.get('message', 'failed') + self.pwd_result_label.config( + text=self.tf('pwd_failed', message=msg), + fg=self.colors['error'] + ) + self.log(self.tf('log_pwd_failed', message=msg), "ERROR") + + self.run_on_ui_thread(update_ui) + + except Exception as e: + def update_ui_error(): + self.pwd_result_label.config( + text=self.t('pwd_request_failed'), + fg=self.colors['error'] + ) + self.log(self.tf('log_pwd_request_failed', error=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(self.t('log_debug_off'), "WARNING") + self.status_text.config(text=self.t('status_ready')) + self.refresh_device_status() + return + + pwd = simpledialog.askstring(self.t('debug_title'), self.t('debug_prompt'), show='*', parent=self.root) + if not pwd: + return + + self.log(self.t('debug_password_verifying'), "WARNING") + + def verify(): + valid, message = self.verify_debug_mode_password(pwd) + if valid: + def enable_debug(): + self.debug_mode = True + self.update_device_status(True, "", True) + self.log(self.t('log_debug_on'), "WARNING") + self.status_text.config(text=self.t('debug_status')) + self.run_on_ui_thread(enable_debug) + else: + def show_failed(): + msg = message or self.t('msg_debug_wrong_password') + self.log(self.tf('debug_verify_failed', message=msg), "WARNING") + messagebox.showwarning(self.t('msg_error_title'), msg) + self.run_on_ui_thread(show_failed) + + threading.Thread(target=verify, daemon=True).start() + + def verify_debug_mode_password(self, password): + try: + data = self._post_json(self.debug_password_api_url, {"password": password}) + if data.get('success') is True and data.get('valid') is True: + return True, data.get('message', '') + return False, data.get('message') or self.t('msg_debug_wrong_password') + except Exception as e: + return False, str(e) + + def install_apps(self): + """安装App — 支持单选或多选APK文件""" + if not self.check_device_connection(): + return + if not self.vin and not self.debug_mode: + messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_need_vin')) + return + if not self.check_authorization(self.vin): + return + + file_paths = filedialog.askopenfilenames( + title=self.t('file_select_apk_title'), + filetypes=[(self.t('filetype_apk'), "*.apk"), (self.t('filetype_all'), "*.*")] + ) + if not file_paths: + return + + count = len(file_paths) + result = messagebox.askyesno( + self.t('msg_install_confirm_title'), + self.tf('msg_install_confirm_many', count=count) + ) + if not result: + return + + def install(): + self.show_progress(True, is_push=True) + self.log(self.tf('log_install_many_start', count=count), "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, self.tf('progress_installing_name', name=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, self.t('progress_install_done'), is_push=True) + self.show_progress(False, is_push=True) + + if success_count == count: + self.log(self.tf('log_install_done_all', count=count), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('msg_install_done_title'), + self.tf('msg_install_done_all', count=count) + ) + elif success_count > 0: + self.log(self.tf('log_install_done_partial', success=success_count, count=count), "WARNING") + self.run_on_ui_thread( + messagebox.showwarning, + self.t('msg_install_partial_title'), + self.tf('msg_install_partial', success=success_count, failed=count - success_count) + ) + else: + self.log(self.t('log_install_failed_simple'), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_install_failed_title'), + self.t('msg_install_failed_all') + ) + + threading.Thread(target=install, daemon=True).start() + + # ============================================================ + # hosts 文件修改 + # ============================================================ + + def modify_hosts(self): + """修改hosts文件,添加云端认证DNS映射""" + hosts_path = r"C:\Windows\System32\drivers\etc\hosts" + host_ip = "103.236.55.140" + host_name = "spm.auto-pai.com" + entry = f"{host_ip} {host_name}" + + try: + try: + with open(hosts_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + except UnicodeDecodeError: + with open(hosts_path, 'r', encoding='gbk', errors='replace') as f: + lines = f.readlines() + + new_lines = [] + found_target = False + changed = False + for line in lines: + stripped = line.strip() + if not stripped or stripped.startswith('#'): + new_lines.append(line) + continue + + body, _, _ = line.partition('#') + parts = body.split() + if len(parts) >= 2 and host_name.lower() in [p.lower() for p in parts[1:]]: + if not found_target: + if parts[0] != host_ip or len(parts) != 2: + changed = True + new_lines.append(f"{entry}\n") + found_target = True + else: + changed = True + continue + + new_lines.append(line) + + if found_target and not changed: + return True + + if not found_target: + if not new_lines or (new_lines[-1] and not new_lines[-1].endswith(('\n', '\r'))): + new_lines.append('\n') + new_lines.append(f"{entry}\n") + + with open(hosts_path, 'w', encoding='utf-8', newline='') as f: + f.writelines(new_lines) + return True + except PermissionError: + self.log(self.t('log_env_config_failed_admin'), "WARNING") + return False + except Exception as e: + if self.debug_mode: + self.log(self.tf('log_env_config_failed_detail', error=str(e)), "WARNING") + else: + self.log(self.t('log_env_config_failed'), "WARNING") + return False + + def _run_netsh(self, command): + """执行 netsh 命令,返回 (returncode, output)""" + try: + for enc in ['utf-8', 'gbk']: + try: + r = subprocess.run( + command, + shell=True, + capture_output=True, + text=True, + encoding=enc, + errors='replace', + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + output = (r.stdout or '') + (r.stderr or '') + return r.returncode, output.strip() + except (UnicodeDecodeError, LookupError): + continue + return -1, self.t('err_decode_failed') + except Exception as e: + return -1, str(e) + + def start_hotspot_action(self): + """打开热点设置并自动轮询检测""" + # 打开设置页面 + try: + subprocess.Popen( + 'start ms-settings:network-mobilehotspot', + shell=True, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + except: + pass + self.log(self.t('log_hotspot_opening'), "INFO") + + # 后台轮询,查到为止(最多 30 秒) + def poll(): + for _ in range(15): + time.sleep(2) + ssid, pwd, status = self.get_hotspot_info() + self.refresh_hotspot_display() + if ssid and self._hotspot_started(status): + self.log(self.tf('log_hotspot_detected', ssid=ssid, password=pwd), "SUCCESS") + return + self.log(self.t('log_hotspot_not_detected'), "WARNING") + + threading.Thread(target=poll, daemon=True).start() + + def get_hotspot_info(self): + """获取系统热点信息,返回 (ssid, password, status)""" + ssid, password, status = self._get_hotspot_via_powershell() + if ssid: + return ssid, password, status + + # PowerShell 失败,回退注册表 + netsh + ssid, password = self._read_hotspot_registry() + status = "stopped" + try: + _, out = self._run_netsh('netsh wlan show hostednetwork') + for line in out.split('\n'): + s = line.strip() + if ('状态' in s or 'status' in s.lower()) and ('已启动' in s or 'started' in s.lower()): + status = "started" + if not ssid and 'ssid' in s.lower() and ':' in s: + val = s.split(':', 1)[-1].strip().strip('"') + if val and 'not set' not in val.lower(): + ssid = val + except: + pass + return ssid, password, status + + def _hotspot_started(self, status): + status_text = (status or "").lower() + return '已启动' in status_text or 'started' in status_text or 'on' in status_text or 'inoperation' in status_text + + def _get_hotspot_via_powershell(self): + """通过 PowerShell 获取 Windows 移动热点配置""" + try: + ps_cmd = ( + '$cp = [Windows.Networking.Connectivity.NetworkInformation,' + 'Windows.Networking.Connectivity,ContentType=WindowsRuntime]' + '::GetInternetConnectionProfile();' + '$tm = [Windows.Networking.NetworkOperators.NetworkOperatorTetheringManager,' + 'Windows.Networking.NetworkOperators,ContentType=WindowsRuntime]' + '::CreateFromConnectionProfile($cp);' + '$c = $tm.GetCurrentAccessPointConfiguration();' + 'Write-Output $c.Ssid; Write-Output $c.Passphrase;' + 'Write-Output $tm.TetheringOperationalState' + ) + r = subprocess.run( + ['powershell', '-NoProfile', '-Command', ps_cmd], + capture_output=True, text=True, timeout=10, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + lines = [l.strip() for l in (r.stdout or '').split('\n') if l.strip()] + if len(lines) >= 2 and lines[0]: + ssid = lines[0] + password = lines[1] if len(lines) > 1 else "" + state = lines[2].lower() if len(lines) > 2 else "" + status = "started" if ('on' in state or 'inoperation' in state) else "stopped" + return ssid, password, status + except: + pass + return "", "", "stopped" + + def _read_hotspot_registry(self): + """从注册表读取 Windows 移动热点的 SSID 和密码""" + try: + import winreg + key = winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Microsoft\WlanSvc\HostedNetworkSettings" + ) + data, _ = winreg.QueryValueEx(key, "HostedNetworkSettings") + winreg.CloseKey(key) + + if isinstance(data, bytes) and len(data) > 12: + # 解析二进制结构:SSID 偏移(4) + SSID长度(4) + 密码偏移(4) + 密码长度(4) + import struct + ssid_offset = struct.unpack_from(' 0 and ssid_offset + ssid_len * 2 <= len(data): + raw = data[ssid_offset:ssid_offset + ssid_len * 2] + ssid = raw.decode('utf-16-le', errors='replace').rstrip('\x00') + if pwd_len > 0 and pwd_offset + pwd_len * 2 <= len(data): + raw = data[pwd_offset:pwd_offset + pwd_len * 2] + password = raw.decode('utf-16-le', errors='replace').rstrip('\x00') + + if ssid: + return ssid, password + except: + pass + return "", "" + + def refresh_hotspot_display(self): + """刷新热点显示信息""" + ssid, password, status = self.get_hotspot_info() + self.run_on_ui_thread(self._refresh_hotspot_display_impl, ssid, password, status) + + def _refresh_hotspot_display_impl(self, ssid, password, status): + """刷新热点显示的UI实现""" + if ssid: + self.hotspot_ssid_label.config(text=self.tf('hotspot_name_value', ssid=ssid)) + else: + self.hotspot_ssid_label.config(text=self.t('hotspot_name_unset')) + if password: + self.hotspot_pwd_label.config(text=self.tf('hotspot_pwd_value', password=password)) + else: + self.hotspot_pwd_label.config(text=self.t('hotspot_pwd_default')) + started = self._hotspot_started(status) + status_text = self.t('hotspot_started') if started else self.t('hotspot_stopped') + self.hotspot_status_label.config( + text=self.tf('hotspot_status_value', status=status_text), + fg=self.colors['success'] if started else self.colors['warning'] + ) + + def run(self): + """运行程序""" + self.root.mainloop() + +def main(): + """主函数""" + if sys.version_info < (3, 6): + print("Error: Python 3.6 or later is required") + sys.exit(1) + + try: + app = ADKAPKGUI() + app.run() + except Exception as e: + print(f"Startup failed: {e}") + import traceback + traceback.print_exc() + messagebox.showerror("Error", f"Program failed to start: {e}") + +if __name__ == "__main__": + main() diff --git a/Mazda-EZ60/Mazda_EZ60-Language-Install_v1.0.py b/Mazda-EZ60/Mazda_EZ60-Language-Install_v1.0.py new file mode 100644 index 0000000..b9ad237 --- /dev/null +++ b/Mazda-EZ60/Mazda_EZ60-Language-Install_v1.0.py @@ -0,0 +1,2446 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import subprocess +import json +import re +import threading +import tkinter as tk +import atexit +from tkinter import ttk, scrolledtext, filedialog, messagebox, simpledialog +from pathlib import Path +from urllib.request import urlopen, Request +from urllib.error import URLError, HTTPError +from urllib.parse import urlencode +from datetime import datetime +import zipfile +try: + import pyzipper +except ImportError: + pyzipper = None +import shutil +import time + + +def get_app_dir(): + return Path(sys.executable).resolve().parent if getattr(sys, 'frozen', False) else Path(__file__).resolve().parent + + +def resource_candidates(file_name): + base_dir = get_app_dir() + candidates = [] + if getattr(sys, 'frozen', False): + candidates.append(Path(getattr(sys, '_MEIPASS', base_dir)) / file_name) + candidates.extend([ + base_dir / file_name, + base_dir / 'tools' / file_name, + base_dir / 'shared' / file_name, + base_dir.parent / 'tools' / file_name, + base_dir.parent / 'shared' / file_name, + base_dir.parent / file_name, + ]) + unique = [] + for candidate in candidates: + if candidate not in unique: + unique.append(candidate) + return unique + + +def find_resource(file_name): + candidates = resource_candidates(file_name) + for candidate in candidates: + if candidate.exists(): + return candidate + return candidates[0] + + +def find_tool(file_name, fallback=None): + path = find_resource(file_name) + if path.exists(): + return str(path) + return fallback or str(path) + + +def set_windows_app_user_model_id(): + if sys.platform != 'win32': + return + try: + import ctypes + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + "yibin.keyi.mazda.ez60.language.installer" + ) + except Exception: + pass + + +class ADKAPKGUI: + CACHE_DIR_NAME = "apps_cache_Mazda_EZ60_1_0" + PACKAGE_KEY_VEHICLE_NAME = "EZ60_1.0" + + def __init__(self): + set_windows_app_user_model_id() + self.root = tk.Tk() + self.root.title("长安语言安装工具") + self.root.geometry("650x640") + self.root.resizable(True, True) + self.set_window_icon() + + # 固定颜色 + 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.lang = 'zh' + self.T = { + 'zh': { + 'title': '马自达EZ60刷机工具_OS-1.0', + 'btn_root': '🔓 获取权限', + 'btn_push': '📦 刷入语言包', + 'btn_install': '📱 安装App', + 'btn_language': '🌐 语言设置', + 'btn_timezone': '⏰ 时区设置', + 'btn_settings': '⚙️ 安卓设置', + 'btn_reboot': '🔄 重启设备', + 'btn_disable_upgrade': '❌ 禁用升级', + 'btn_clear_log': '🗑 清空日志', + 'btn_query_pwd': '查询密码', + 'btn_debug_extract': '解压测试', + 'device_label': '设备:', + 'vin_label': 'VIN码:', + 'auth_label': '授权:', + 'log_title': '📋 运行日志', + 'status_ready': '就绪', + 'status_connected': '已连接', + 'status_disconnected': '未连接', + 'status_detecting': '未检测', + 'vin_none': '未获取', + 'auth_none': '未验证', + 'auth_yes': '已授权', + 'auth_no': '未授权', + 'btn_refresh': '🔄 检查', + 'hint_factory': '🔧 关闭车辆WI-FI和4G网络,拨号获取的密码进入工程模式', + 'warn_no_device': '设备未连接', + 'warn_connect_first': '请先连接设备并点击「检查」按钮刷新状态!', + 'warn_no_vin': '请先刷新设备状态并获取VIN码', + 'err_auth_fail': '授权失败', + 'err_device_not_auth': '设备未授权', + 'err_resource_fail': '资源准备失败!', + 'err_no_resource_dir': '资源目录未找到', + 'info_flash_start': '开始刷入语言包...', + 'info_flash_done': '语言包刷入完成,重启设备后生效', + 'info_flash_fail': '语言包刷入失败', + 'warn_no_apk': '未找到语言包文件', + 'info_installing': '安装中...', + 'info_install_done': '安装完成', + 'info_log_cleared': '日志已清空', + 'confirm_reboot': '确定要重启设备吗?', + 'info_rebooting': '设备正在重启...', + 'warn_device_disconnected': '设备已断开连接', + 'info_device_connected': '设备已连接', + 'info_checking_auth': '正在验证授权状态...', + 'info_auth_pass': '✅ 授权验证通过!', + 'info_auth_fail': '❌ 授权验证失败', + 'info_preparing': '正在准备资源...', + 'err_no_package': '错误:未找到资源包', + 'err_no_password': '错误:解压密码未设置', + 'err_no_7za': '错误:未找到 7za.exe', + 'err_extract_fail': '解压失败', + 'info_extracting': '资源准备中...', + 'info_extract_done': '资源准备完成', + 'err_extract_user': '资源准备失败,请检查网络连接后重试', + 'warn_no_app_dir': '警告:未找到 app/priv-app 目录', + 'progress_resource_loading': '资源准备中...', + 'progress_resource_done': '资源准备完成', + 'progress_extracting_percent': '资源准备中 {percent}%', + 'progress_flashing': '正在刷入', + 'progress_flash_done': '刷入完成', + 'progress_aborted': '已终止', + 'progress_installing': '安装中', + 'progress_installing_name': '安装中 ({name})', + 'progress_done': '完成', + 'lang_zh': '中', + 'lang_en': 'EN', + 'switch_lang': '语言 / Language', + 'pwd_query_label': '工程密码查询:', + 'vin_placeholder': '请输入VIN', + 'pwd_success': '密码: *#{password}#*', + 'pwd_failed': '失败: {message}', + 'pwd_request_failed': '请求失败', + 'tip_1': '1. 安装语言过程中请保持车辆和电脑的电量充足,不可中途停止。', + 'tip_2': '2. 获取权限以后,车辆自动重启以后再进入语言刷入。', + 'tip_3': '3. 部分语言需要重启后生效,可以一切工作完成以后再重启。', + 'warn_flash_warning': '⚠️ 重要提示', + 'warn_flash_msg': '刷入过程中请勿:\n ● 重启车机\n ● 退出本程序\n ● 关闭电脑\n\n否则可能导致车机系统损坏!', + 'err_wrong_password': '请检查密码是否正确', + 'title_pop_lang': '快捷语言设置', + 'quick_lang_header': '选择目标语言', + 'quick_lang_hint': '点击按钮即可将系统语言切换为对应语言,重启后生效', + 'quick_lang_system': '⚙️ 打开系统语言设置(手动选择)', + 'msg_warn_title': '警告', + 'msg_error_title': '错误', + 'msg_done_title': '完成', + 'msg_success_title': '成功', + 'msg_auth_failed_title': '授权失败', + 'msg_device_unauthorized': '设备未授权', + 'msg_input_vin': '请输入VIN码', + 'msg_resource_prepare_failed': '资源准备失败!', + 'msg_resource_dir_missing': '资源目录未找到', + 'msg_no_apk_in_folder': '所选文件夹中没有APK文件!', + 'msg_confirm_install_title': '确认安装', + 'msg_confirm_install_many': '已选择 {count} 个APK文件\n\n是否开始安装?', + 'msg_confirm_install_folder': '找到 {count} 个APK文件\n\n是否开始批量安装?', + 'msg_install_success_many': '成功安装 {count} 个APK!', + 'msg_install_partial': '成功: {success}\n失败: {failed}', + 'msg_install_all_failed': '所有APK安装失败!', + 'msg_install_exception': '安装过程异常:{error}', + 'msg_quick_lang_success': '系统语言已设置为 {language}\n\n⚠️ 请重启设备使其生效。', + 'msg_quick_lang_failed': '语言设置失败!\n\n{output}', + 'msg_disable_confirm_title': '确认禁用升级', + 'msg_disable_confirm': '⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n是否确定要禁用系统升级应用?', + 'msg_disable_success': '系统升级已成功禁用!', + 'msg_disable_failed': '禁用失败:{output}', + 'msg_reboot_confirm_title': '确认重启', + 'file_apk': 'APK文件', + 'file_all': '所有文件', + 'dialog_select_apk': '选择APK文件', + 'dialog_select_apk_folder': '选择包含APK文件的文件夹', + 'unknown_error': '未知错误', + 'status_debug': '调试模式', + 'debug_password_prompt': '请输入调试密码:', + 'debug_password_verifying': '正在校验调试密码...', + 'debug_wrong_password': '密码错误', + 'debug_verify_failed': '调试密码校验失败: {message}', + 'debug_need_enable': '请先按 Ctrl+Shift+D 进入调试模式', + 'debug_need_vin': '调试解压测试需要 VIN。请先连接设备刷新,或在调试模式中手动设置 VIN。', + 'log_debug_on': '🔧 调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志', + 'log_debug_off': '调试模式已关闭', + 'log_lang_switched': '语言已切换为中文', + 'log_cache_invalid': '已解压缓存无效: {reason}', + 'log_cache_reuse_invalid': '缓存资源无效,已清理: {reason}', + 'log_package_missing': '未找到资源包文件: {path}', + 'log_adb_missing': '未找到adb命令,请将ADB文件放入本目录', + 'log_device_connected': '设备已连接', + 'log_device_disconnected': '设备已断开连接', + 'log_current_vin': '当前VIN: {vin}', + 'log_vin_unavailable': '无法读取VIN', + 'log_refresh_failed': '刷新设备状态失败: {error}', + 'log_auth_skip_debug': '调试模式:跳过授权', + 'log_auth_checking': '正在验证授权状态...', + 'log_auth_ok': '授权验证通过', + 'log_auth_failed': '授权验证失败', + 'log_vehicle_name': '车型名称: {name}', + 'log_adb_required': '请先连接 ADB 并获取 VIN', + 'log_data_prepare_failed_detail': '资源准备失败: {error}', + 'log_extract_password_missing': '错误:解压密码未设置', + 'log_7za_missing': '错误:未找到 7za.exe ({path})', + 'log_extracted_resource_invalid': '解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。', + 'log_extract_done': '资源准备完成', + 'log_extract_exception': '资源准备失败: {error}', + 'err_extract_wrong_password': '解压密码错误,请重新确认 package.bin 密码', + 'err_extract_data': '资源包数据错误,可能是密码错误或 package.bin 损坏', + 'err_extract_corrupt': '资源包损坏或不完整,请检查 package.bin', + 'err_extract_failed': '解压失败: {error}', + 'err_extract_failed_code': '解压失败 (返回码 {code}),请检查密码是否正确', + 'log_root_failed': '获取失败', + 'log_permission_failed': '获取失败', + 'log_permission_running': '正在获取权限中', + 'log_reboot_failed': '重启失败', + 'log_permission_ok': '获取成功', + 'log_flash_readonly': '请先点击「获取权限」获取权限后再试', + 'log_flash_complete_count': '刷入完成,共 {count} 个语言包', + 'log_flash_effect_after_reboot': '语言包已刷入完成,重启设备后生效,您可在适当时候重启', + 'log_flash_partial': '部分刷入成功({success}/{total})', + 'log_install_start': '开始安装 {count} 个APK...', + 'log_install_done_all': '安装完成:全部 {count} 个成功', + 'log_install_done_partial': '安装完成:{success}/{total} 成功', + 'log_install_failed': '安装失败', + 'log_install_exception': '安装过程异常: {error}', + 'log_install_success_item': '✓ {name}', + 'log_install_failed_item': '✗ {name}', + 'log_menu_key_ready': '辅助组件已安装', + 'log_menu_key_install_failed': '辅助组件安装失败', + 'log_menu_key_permission_failed': '辅助组件权限设置失败', + 'log_quick_lang_setting': '正在设置系统语言为: {language} ({locale})', + 'log_quick_lang_success': '✓ 语言已设置为 {language}', + 'log_quick_lang_failed': '✗ 语言设置失败: {output}', + 'log_rebooting': '设备正在重启...', + 'log_disable_cancelled': '已取消禁用升级操作', + 'log_disable_success': '系统升级已禁用', + 'log_disable_failed': '禁用系统升级失败', + 'log_preclean_start': '正在清理预置应用...', + 'log_preclean_done': '预置应用清理完成', + 'log_preclean_partial': '预置应用部分清理失败', + 'log_preclean_item_done': '清理项已完成: {package}', + 'log_preclean_item_failed': '清理项执行失败: {package}', + 'log_package_key_failed': 'package-key 获取失败', + 'log_package_extract_success': '资源准备完成', + 'log_package_extract_failed': 'package.bin 解压测试失败', + 'log_pwd_success': '密码查询成功', + 'log_pwd_failed': '密码查询失败: {message}', + 'log_pwd_request_failed': '密码查询请求失败: {error}', + 'err_no_usable_apk': '未找到可用 APK', + 'err_zero_apk': '发现 0KB APK: {files}', + 'err_push_failed': 'push失败: {error}', + 'err_cp_failed': 'cp失败: {error}', + }, + 'en': { + 'title': 'Mazda EZ60 Flash Tool_OS-1.0', + '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', + 'btn_query_pwd': 'Query Pwd', + 'btn_debug_extract': 'Extract', + 'device_label': 'Device:', + 'vin_label': 'VIN:', + 'auth_label': 'Auth:', + 'log_title': '📋 Log', + 'status_ready': 'Ready', + 'status_connected': 'Connected', + 'status_disconnected': 'Disconnected', + 'status_detecting': 'Detecting', + 'vin_none': 'None', + 'auth_none': 'Unknown', + 'auth_yes': 'Authorized', + 'auth_no': 'Unauthorized', + 'btn_refresh': '🔄 Check', + 'hint_factory': '🔧 Turn off WiFi & 4G, enter factory mode with dial code', + 'warn_no_device': 'Device not connected', + 'warn_connect_first': 'Please connect device and click Check button!', + 'warn_no_vin': 'Please refresh device status and get VIN', + 'err_auth_fail': 'Authorization failed', + 'err_device_not_auth': 'Device not authorized', + 'err_resource_fail': 'Resource preparation failed!', + 'err_no_resource_dir': 'Resource directory not found', + 'info_flash_start': 'Starting language pack flashing...', + 'info_flash_done': 'Flashing complete, reboot device to take effect', + 'info_flash_fail': 'Flashing failed', + 'warn_no_apk': 'No APK files found', + 'info_installing': 'Installing...', + 'info_install_done': 'Install complete', + 'info_log_cleared': 'Log cleared', + 'confirm_reboot': 'Are you sure you want to reboot?', + 'info_rebooting': 'Device rebooting...', + 'warn_device_disconnected': 'Device disconnected', + 'info_device_connected': 'Device connected', + 'info_checking_auth': 'Verifying authorization...', + 'info_auth_pass': '✅ Authorization passed!', + 'info_auth_fail': '❌ Authorization failed', + 'info_preparing': 'Preparing resources...', + 'err_no_package': 'Error: package.bin not found', + 'err_no_password': 'Error: password not set', + 'err_no_7za': 'Error: 7za.exe not found', + 'err_extract_fail': 'Extraction failed', + 'info_extracting': 'Preparing resources...', + 'info_extract_done': 'Resource preparation complete', + 'err_extract_user': 'Resource preparation failed, check network and retry', + 'warn_no_app_dir': 'Warning: app/priv-app directory not found', + 'progress_resource_loading': 'Preparing resources...', + 'progress_resource_done': 'Resources ready', + 'progress_extracting_percent': 'Preparing resources {percent}%', + 'progress_flashing': 'Flashing', + 'progress_flash_done': 'Flash complete', + 'progress_aborted': 'Aborted', + 'progress_installing': 'Installing', + 'progress_installing_name': 'Installing ({name})', + 'progress_done': 'Done', + 'lang_zh': '中', + 'lang_en': 'EN', + 'switch_lang': 'Language', + 'pwd_query_label': 'Factory password:', + 'vin_placeholder': 'Enter VIN', + 'pwd_success': 'Password: *#{password}#*', + 'pwd_failed': 'Failed: {message}', + 'pwd_request_failed': 'Request failed', + 'tip_1': '1. Keep the vehicle and PC powered during language installation.', + 'tip_2': '2. After permission is obtained, wait for the vehicle to reboot before flashing.', + 'tip_3': '3. Some languages take effect after reboot; reboot after all work is finished.', + 'warn_flash_warning': '⚠️ Warning', + 'warn_flash_msg': 'During flashing, DO NOT:\n ● Reboot vehicle\n ● Close this app\n ● Power off PC\n\nSystem damage may occur!', + 'err_wrong_password': 'Please check password', + 'title_pop_lang': 'Quick Language Setting', + 'quick_lang_header': 'Select target language', + 'quick_lang_hint': 'Click a button to set the system language. Reboot to apply.', + 'quick_lang_system': '⚙️ Open system language settings', + 'msg_warn_title': 'Warning', + 'msg_error_title': 'Error', + 'msg_done_title': 'Done', + 'msg_success_title': 'Success', + 'msg_auth_failed_title': 'Authorization failed', + 'msg_device_unauthorized': 'Device unauthorized', + 'msg_input_vin': 'Enter VIN', + 'msg_resource_prepare_failed': 'Resource preparation failed!', + 'msg_resource_dir_missing': 'Resource directory not found', + 'msg_no_apk_in_folder': 'No APK files found in the selected folder!', + 'msg_confirm_install_title': 'Confirm install', + 'msg_confirm_install_many': '{count} APK files selected.\n\nStart installation?', + 'msg_confirm_install_folder': '{count} APK files found.\n\nStart batch installation?', + 'msg_install_success_many': '{count} APKs installed successfully!', + 'msg_install_partial': 'Success: {success}\nFailed: {failed}', + 'msg_install_all_failed': 'All APK installations failed!', + 'msg_install_exception': 'Installation error: {error}', + 'msg_quick_lang_success': 'System language set to {language}.\n\n⚠️ Reboot the device to apply.', + 'msg_quick_lang_failed': 'Language setting failed!\n\n{output}', + 'msg_disable_confirm_title': 'Confirm Disable OTA', + 'msg_disable_confirm': '⚠️ Warning: after disabling OTA, the system will no longer receive updates.\n\nDisable the OTA app?', + 'msg_disable_success': 'System upgrade has been disabled!', + 'msg_disable_failed': 'Disable failed: {output}', + 'msg_reboot_confirm_title': 'Confirm reboot', + 'file_apk': 'APK files', + 'file_all': 'All files', + 'dialog_select_apk': 'Select APK file', + 'dialog_select_apk_folder': 'Select a folder containing APK files', + 'unknown_error': 'unknown error', + 'status_debug': 'Debug mode', + 'debug_password_prompt': 'Enter debug password:', + 'debug_password_verifying': 'Verifying debug password...', + 'debug_wrong_password': 'Wrong password', + 'debug_verify_failed': 'Debug password verification failed: {message}', + 'debug_need_enable': 'Press Ctrl+Shift+D first.', + 'debug_need_vin': 'Extract test needs a VIN. Refresh a connected device or set VIN in debug mode.', + 'log_debug_on': '🔧 Debug mode enabled - authorization and device checks are skipped, detailed ADB logs are shown', + 'log_debug_off': 'Debug mode disabled', + 'log_lang_switched': 'Language switched to English', + 'log_cache_invalid': 'Extract cache invalid: {reason}', + 'log_cache_reuse_invalid': 'Cached resources invalid and cleaned: {reason}', + 'log_package_missing': 'Resource package not found: {path}', + 'log_adb_missing': 'adb not found. Put ADB files in this directory.', + 'log_device_connected': 'Device connected', + 'log_device_disconnected': 'Device disconnected', + 'log_current_vin': 'Current VIN: {vin}', + 'log_vin_unavailable': 'Unable to read VIN', + 'log_refresh_failed': 'Refresh device status failed: {error}', + 'log_auth_skip_debug': 'Debug mode: skip authorization', + 'log_auth_checking': 'Checking authorization...', + 'log_auth_ok': 'Authorization passed', + 'log_auth_failed': 'Authorization failed', + 'log_vehicle_name': 'Vehicle name: {name}', + 'log_adb_required': 'Connect ADB and get VIN first', + 'log_data_prepare_failed_detail': 'Resource preparation failed: {error}', + 'log_extract_password_missing': 'Extraction password is not set', + 'log_7za_missing': '7za.exe not found: {path}', + 'log_extracted_resource_invalid': 'Extracted resources are invalid: {reason}. Flashing stopped. Check the password or package.', + 'log_extract_done': 'Resources ready', + 'log_extract_exception': 'Resource preparation failed: {error}', + 'err_extract_wrong_password': 'Incorrect extraction password. Check the package.bin password.', + 'err_extract_data': 'Package data error. The password may be wrong or package.bin may be damaged.', + 'err_extract_corrupt': 'Package is damaged or incomplete. Check package.bin.', + 'err_extract_failed': 'Extraction failed: {error}', + 'err_extract_failed_code': 'Extraction failed (exit code {code}). Check whether the password is correct.', + 'log_root_failed': 'Permission failed', + 'log_permission_failed': 'Permission failed', + 'log_permission_running': 'Getting permission', + 'log_reboot_failed': 'Reboot failed', + 'log_permission_ok': 'Permission granted', + 'log_flash_readonly': 'Click Get Root first, then try again', + 'log_flash_complete_count': 'Flashing complete, {count} language packages', + 'log_flash_effect_after_reboot': 'Language package flashed. Reboot the device when convenient.', + 'log_flash_partial': 'Partially flashed ({success}/{total})', + 'log_install_start': 'Installing {count} APKs...', + 'log_install_done_all': 'Installation complete: all {count} succeeded', + 'log_install_done_partial': 'Installation complete: {success}/{total} succeeded', + 'log_install_failed': 'Installation failed', + 'log_install_exception': 'Installation error: {error}', + 'log_install_success_item': '✓ {name}', + 'log_install_failed_item': '✗ {name}', + 'log_menu_key_ready': 'Helper component installed', + 'log_menu_key_install_failed': 'Helper component installation failed', + 'log_menu_key_permission_failed': 'Helper component permission setup failed', + 'log_quick_lang_setting': 'Setting system language to: {language} ({locale})', + 'log_quick_lang_success': '✓ Language set to {language}', + 'log_quick_lang_failed': '✗ Language setting failed: {output}', + 'log_rebooting': 'Device rebooting...', + 'log_disable_cancelled': 'Disable OTA cancelled', + 'log_disable_success': 'System upgrade disabled', + 'log_disable_failed': 'Failed to disable system upgrade', + 'log_preclean_start': 'Cleaning preinstalled apps...', + 'log_preclean_done': 'Preinstalled apps cleaned', + 'log_preclean_partial': 'Some preinstalled apps failed to clean', + 'log_preclean_item_done': 'Cleanup item completed: {package}', + 'log_preclean_item_failed': 'Cleanup item failed: {package}', + 'log_package_key_failed': 'package-key fetch failed', + 'log_package_extract_success': 'Resources ready', + 'log_package_extract_failed': 'package.bin extract test failed', + 'log_pwd_success': 'Password query succeeded', + 'log_pwd_failed': 'Password query failed: {message}', + 'log_pwd_request_failed': 'Password query request failed: {error}', + 'err_no_usable_apk': 'No usable APK found', + 'err_zero_apk': '0KB APK found: {files}', + 'err_push_failed': 'push failed: {error}', + 'err_cp_failed': 'cp failed: {error}', + } + } + + # 从 exe/py 所在目录查找资源文件 + self.base_dir = get_app_dir() + self.adb = find_tool('adb.exe', 'adb') + self.sz = find_tool('7za.exe') + self.package_file = self._find_package_file() + self.extract_password = None + self.apps_dir = None + self.priv_apps_dir = None + self.menu_key_apk = None + self.temp_dir = None + self.api_url = "https://api.changan.softwindy.cn/api/authorizations/auth-check" + self.debug_password_api_url = "https://api.changan.softwindy.cn/api/authorizations/verify-debug-mode-password" + self.vin = None + self.vehicle_name = "" + self.device_connected = False + self._refreshing = False # 防止并发刷新 + self.debug_mode = False # 调试模式 + self.mazda_disable_packages = [ + "com.carinno.p1", + "com.wtcl.electronicdirections", + "com.ximalaya.ting.android.car", + "com.tinnove.netease.music", + "com.migu.miguplay.car", + "cn.cmvideo.car.play", + "com.tinnove.carshow", + "com.tinnove.changba", + "com.qiyi.video.iv", + "com.changan.appmarket", + "com.incall.apps.softmanager", + ] + atexit.register(self.cleanup_cache_on_exit) + + # 设置样式 + self.setup_styles() + self.setup_ui() + self.root.after(200, self.set_window_icon) + self.center_window() + + # 检查环境 + self.check_environment() + + # 启动设备状态监控 + self.start_device_monitor() + + def set_window_icon(self): + """Set the Tk window/taskbar icon at runtime; PyInstaller --icon only sets the exe file icon.""" + try: + icon_path = find_resource("app.ico") + if icon_path.exists(): + self.root.iconbitmap(str(icon_path)) + self._set_windows_hwnd_icon(icon_path) + except Exception: + pass + + def _set_windows_hwnd_icon(self, icon_path): + if sys.platform != 'win32': + return + try: + import ctypes + user32 = ctypes.windll.user32 + hwnd = self.root.winfo_id() + image_icon = 1 + lr_loadfromfile = 0x00000010 + wm_seticon = 0x0080 + icon_small = 0 + icon_big = 1 + path = str(icon_path) + small = user32.LoadImageW(None, path, image_icon, 16, 16, lr_loadfromfile) + big = user32.LoadImageW(None, path, image_icon, 32, 32, lr_loadfromfile) + if small: + user32.SendMessageW(hwnd, wm_seticon, icon_small, small) + if big: + user32.SendMessageW(hwnd, wm_seticon, icon_big, big) + except Exception: + pass + + def setup_styles(self): + """设置自定义样式""" + style = ttk.Style() + style.theme_use('clam') + + # 配置主颜色 + style.configure('TFrame', background=self.colors['bg_dark']) + style.configure('TLabel', background=self.colors['bg_dark'], foreground=self.colors['text']) + style.configure('TLabelframe', background=self.colors['bg_dark'], foreground=self.colors['text']) + style.configure('TLabelframe.Label', background=self.colors['bg_dark'], foreground=self.colors['accent']) + + # 配置进度条 + style.configure('TProgressbar', + background=self.colors['accent'], + troughcolor=self.colors['bg_light'], + borderwidth=0) + + def setup_ui(self): + """设置UI界面""" + # 配置根窗口 + self.root.title(self.t('title')) + 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=45) + title_frame.pack(fill=tk.X, pady=(0, 10)) + title_frame.pack_propagate(False) + + # 标题 + title_inner = tk.Frame(title_frame, bg=self.colors['bg_dark']) + title_inner.place(relx=0.5, rely=0.5, anchor='center') + self.title_icon_label = tk.Label(title_inner, + text="🚗", + font=('Microsoft YaHei', 17, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.title_icon_label.grid(row=0, column=0, padx=(0, 8), sticky='e') + self.title_label = tk.Label(title_inner, + text=self.t('title'), + font=('Microsoft YaHei', 18, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.title_label.grid(row=0, column=1, sticky='w') + + self.btn_lang_switch = tk.Button(title_frame, text="EN", + command=self.toggle_lang, + font=('Microsoft YaHei', 9, 'bold'), + fg='white', + bg=self.colors['accent'], + activebackground=self.colors['accent_hover'], + activeforeground='white', + relief=tk.FLAT, + cursor='hand2', + width=8, + height=1) + self.btn_lang_switch.place(relx=1.0, rely=0.5, x=-2, anchor='e') + + # 工程密码查询区域 + 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) + + self.pwd_query_label = tk.Label(pwd_query_frame, text=self.t('pwd_query_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.pwd_query_label.pack(side=tk.LEFT, padx=(10, 5), pady=5) + + self.vin_input = tk.Entry(pwd_query_frame, + font=('Consolas', 9), + bg='#2d2d3d', + fg='#636e72', + insertbackground='white', + relief=tk.FLAT, + width=20) + self.vin_input.insert(0, self.t('vin_placeholder')) + self.vin_input.bind("", 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=self.t('btn_query_pwd'), + command=self.query_password_by_vin, + font=('Microsoft YaHei', 8), + fg='white', + bg=self.colors['accent'], + activebackground=self.colors['accent_hover'], + activeforeground='white', + 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) + + # 按钮区域(两排,每排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) + + self.debug_button_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + self.btn_debug_extract = tk.Button(self.debug_button_frame, text=self.t('btn_debug_extract'), + command=self.debug_test_package_extract, + bg=self.colors['info'], + **btn_params) + self.btn_debug_extract.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') + + self.device_label = tk.Label(status_indicator_frame, text="设备:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.device_label.pack(side=tk.LEFT, padx=(5, 3)) + + self.device_status_label = tk.Label(status_indicator_frame, text=self.t('status_detecting'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + 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) + self.vin_label_title = tk.Label(vin_frame, text="VIN码:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.vin_label_title.pack(side=tk.LEFT) + self.vin_label = tk.Label(vin_frame, text=self.t('vin_none'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + 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) + self.auth_label_title = tk.Label(auth_frame, text="授权:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.auth_label_title.pack(side=tk.LEFT) + self.auth_label = tk.Label(auth_frame, text=self.t('auth_none'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + anchor='w', width=4) + self.auth_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 刷新按钮 + self.btn_refresh = 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') + self.btn_refresh.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 = [self.t('tip_1'), self.t('tip_2'), self.t('tip_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)) + label = tk.Label(tip_row, text=tip, + font=('Microsoft YaHei', 9), + fg=self.colors['warning'], + bg=self.colors['bg_light'], + wraplength=600, + justify=tk.LEFT) + label.pack(side=tk.LEFT) + setattr(self, f'tip_label_{i + 1}', label) + + # 解压进度条框架 + 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) + + self.log_title_label = tk.Label(log_title_frame, text="📋 运行日志", + font=('Microsoft YaHei', 10, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.log_title_label.pack(side=tk.LEFT, padx=10) + + self.btn_clear = tk.Button(log_title_frame, text="🗑 清空日志", + 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.root.bind('', self._debug_test_extract) + self.root.protocol("WM_DELETE_WINDOW", self.on_close) + + # 绑定悬停效果 + 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, + self.btn_debug_extract] + + 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, lambda: func(*args, **kwargs)) + + def _adb_cmd(self): + return subprocess.list2cmdline([self.adb]) + + def _find_package_file(self): + return find_resource("package.bin") + + def t(self, key): + """获取翻译文本""" + return self.T.get(self.lang, self.T['zh']).get(key, key) + + def tf(self, key, **kwargs): + try: + return self.t(key).format(**kwargs) + except Exception: + return self.t(key) + + def is_placeholder_vin(self, value): + return value in ( + self.T['zh'].get('vin_placeholder'), + self.T['en'].get('vin_placeholder'), + ) + + def toggle_lang(self): + """切换语言""" + self.lang = 'en' if self.lang == 'zh' else 'zh' + self.btn_lang_switch.config(text=self.t('lang_en') if self.lang == 'zh' else self.t('lang_zh')) + self._refresh_ui_texts() + self.log(self.t('log_lang_switched'), "INFO") + + def _refresh_ui_texts(self): + """刷新所有UI文本""" + t = self.t + self.root.title(t('title')) + widgets = [ + (getattr(self, 'title_label', None), 'title', 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_debug_extract', None), 'btn_debug_extract', None), + (getattr(self, 'btn_query_pwd', None), 'btn_query_pwd', None), + (getattr(self, 'btn_clear', None), 'btn_clear_log', None), + (getattr(self, 'pwd_query_label', None), 'pwd_query_label', None), + (getattr(self, 'log_title_label', None), 'log_title', None), + (getattr(self, 'device_label', None), 'device_label', None), + (getattr(self, 'vin_label_title', None), 'vin_label', None), + (getattr(self, 'auth_label_title', None), 'auth_label', None), + (getattr(self, 'btn_refresh', None), 'btn_refresh', None), + (getattr(self, 'hint_label', None), 'hint_factory', None), + (getattr(self, 'tip_label_1', None), 'tip_1', None), + (getattr(self, 'tip_label_2', None), 'tip_2', None), + (getattr(self, 'tip_label_3', None), 'tip_3', None), + ] + for w, key, _ in widgets: + if w: + w.config(text=t(key)) + if getattr(self, 'status_text', None): + self.status_text.config(text=t('status_debug') if self.debug_mode else t('status_ready')) + self.btn_lang_switch.config(text=t('lang_en') if self.lang == 'zh' else t('lang_zh')) + if getattr(self, 'vin_input', None) and self.is_placeholder_vin(self.vin_input.get()): + self.vin_input.delete(0, tk.END) + self.vin_input.insert(0, t('vin_placeholder')) + self._update_device_status_impl( + self.device_connected, + self.vin, + getattr(self, '_last_authorized', False) + ) + + def set_debug_buttons_visible(self, visible): + if not hasattr(self, 'debug_button_frame'): + return + if visible: + self.debug_button_frame.pack(pady=(0, 8)) + else: + self.debug_button_frame.pack_forget() + + 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(self.t('info_log_cleared'), "INFO") + + def _show_progress_impl(self, show=True, is_push=False): + """显示/隐藏进度条的实际实现(必须在主线程调用)""" + if is_push: + if show: + self.push_progress_label.pack() + self.push_progress.pack(fill=tk.X, pady=(2, 0)) + self.push_progress['value'] = 0 + else: + self.push_progress_label.pack_forget() + self.push_progress.pack_forget() + else: + if show: + self.progress_label.pack() + self.progress.pack(fill=tk.X, pady=(2, 0)) + self.progress['value'] = 0 + else: + self.progress_label.pack_forget() + self.progress.pack_forget() + + def show_progress(self, show=True, is_push=False): + """显示/隐藏进度条(线程安全)""" + self.run_on_ui_thread(self._show_progress_impl, show, is_push) + + def _update_progress_impl(self, value, max_value=100, label="", is_push=False): + """更新进度条的实际实现(必须在主线程调用)""" + if is_push: + percent = (value / max_value) * 100 + self.push_progress['value'] = percent + self.push_progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)") + else: + percent = (value / max_value) * 100 + self.progress['value'] = percent + self.progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)") + self.root.update_idletasks() + + def update_progress(self, value, max_value=100, label="", is_push=False): + """更新进度条(线程安全)""" + self.run_on_ui_thread(self._update_progress_impl, value, max_value, label, is_push) + + def update_device_status(self, connected, vin=None, authorized=False): + """更新设备状态显示(线程安全:立即设状态变量,UI走主线程)""" + self.device_connected = connected + if vin is not None: + self.vin = vin + self.run_on_ui_thread(self._update_device_status_impl, connected, vin, authorized) + + def _update_device_status_impl(self, connected, vin, authorized): + """设备状态UI更新的实际实现(必须在主线程调用)""" + self._last_authorized = authorized + t = self.t + if connected: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['success']) + self.device_status_label.config(text=t('status_connected'), fg=self.colors['success']) + if vin: + self.vin_label.config(text=vin, fg=self.colors['success']) + if authorized: + self.auth_label.config(text=t('auth_yes'), fg=self.colors['success']) + else: + self.auth_label.config(text=t('auth_no'), fg=self.colors['error']) + else: + self.vin_label.config(text=t('vin_none'), fg=self.colors['error']) + self.auth_label.config(text=t('auth_none'), fg=self.colors['error']) + else: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['error']) + self.device_status_label.config(text=t('status_disconnected'), fg=self.colors['error']) + self.vin_label.config(text=t('vin_none'), fg=self.colors['error']) + self.auth_label.config(text=t('auth_none'), fg=self.colors['error']) + + def check_device_connection(self): + """检查设备是否连接""" + if self.debug_mode: + return True + if not self.device_connected: + messagebox.showwarning(self.t('warn_no_device'), self.t('warn_connect_first')) + 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(self.t('log_device_disconnected'), "WARNING") + + time.sleep(5) + except: + time.sleep(5) + + threading.Thread(target=monitor, daemon=True).start() + + def get_root_permission(self): + """获取 root/remount 权限,不触发重启。""" + if not self.check_device_connection(): + return + + def get_root(): + self.show_progress(True, is_push=False) + self.log(self.t('log_permission_running'), "INFO") + + ok_root, out_root = self.run_adb_command('adb -d root') + if not ok_root: + self.log(self.t('log_root_failed'), "ERROR") + self.show_progress(False, is_push=False) + return + + time.sleep(1) + + ok_remount, out_remount = self.run_adb_password_command('adb -d remount', timeout=60) + if not ok_remount: + self.log(self.t('log_permission_failed'), "ERROR") + self.show_progress(False, is_push=False) + return + + self.log(self.t('log_permission_ok'), "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 + if has_app or has_priv: + ok, reason = self._validate_extracted_apks() + if not ok: + self.log(self.tf('log_cache_invalid', reason=reason), "ERROR") + self._clear_extracted_cache() + return False + return has_app or has_priv + + def _validate_extracted_apks(self): + apks = [] + if self.apps_dir and self.apps_dir.exists(): + apks.extend(self.apps_dir.glob("*.apk")) + if self.priv_apps_dir and self.priv_apps_dir.exists(): + apks.extend(self.priv_apps_dir.glob("*.apk")) + if not apks: + return False, self.t('err_no_usable_apk') + + zero_apks = [apk.name for apk in apks if apk.stat().st_size <= 0] + if zero_apks: + preview = ", ".join(zero_apks[:5]) + suffix = "..." if len(zero_apks) > 5 else "" + return False, self.tf('err_zero_apk', files=f"{preview}{suffix}") + return True, "" + + def _clear_extracted_cache(self): + if self.temp_dir and self.temp_dir.exists(): + shutil.rmtree(self.temp_dir, ignore_errors=True) + time.sleep(0.5) + self.apps_dir = None + self.priv_apps_dir = None + self.menu_key_apk = None + self.temp_dir = None + + def _cache_dir_path(self): + local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) + return Path(local_appdata) / ".cache" / "system" / ".android" / self.CACHE_DIR_NAME + + def _scan_root_level_apks(self): + if not self.apps_dir: + return + root_dir = self.apps_dir.parent + menu_key_candidate = root_dir / "MenuKey-release.apk" + if menu_key_candidate.exists(): + self.menu_key_apk = menu_key_candidate + + def cleanup_cache_on_exit(self): + self._clear_extracted_cache() + + def on_close(self): + self.cleanup_cache_on_exit() + self.root.destroy() + + def _format_extract_error(self, err_msg, return_code): + text = (err_msg or "").lower() + if any(marker in text for marker in ( + "wrong password", + "incorrect password", + "password is incorrect", + "data error in encrypted file", + "can not open encrypted archive", + )): + return self.t('err_extract_wrong_password') + if "data error" in text: + return self.t('err_extract_data') + if "headers error" in text or "unexpected end" in text: + return self.t('err_extract_corrupt') + if err_msg.strip(): + return self.tf('err_extract_failed', error=err_msg.strip()[:300]) + return self.tf('err_extract_failed_code', code=return_code) + + def _decode_7z_output(self, output): + for enc in ('gbk', 'utf-8'): + try: + return output.decode(enc) + except UnicodeDecodeError: + continue + return output.decode('utf-8', errors='replace') + + def _seven_zip_supports_progress_stream(self): + try: + result = subprocess.run( + [self.sz], + capture_output=True, + text=True, + errors='ignore', + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + return '-bs{o|e|p}' in (result.stdout + result.stderr) + except Exception: + return False + + def _extract_with_7za_progress(self): + self.update_progress(0, 100, self.t('progress_resource_loading')) + cmd = [ + self.sz, 'x', str(self.package_file), + f'-p{self.extract_password}', + f'-o{self.temp_dir}', '-y' + ] + use_progress_switch = self._seven_zip_supports_progress_stream() + if use_progress_switch: + cmd.extend(['-bsp1', '-bso0', '-bse1']) + + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.DEVNULL, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0, + bufsize=0 + ) + + output = bytearray() + last_percent = -1 + while True: + chunk = proc.stdout.read(1) if proc.stdout else b'' + if not chunk: + if proc.poll() is not None: + break + time.sleep(0.05) + continue + + output.extend(chunk) + if len(output) > 60000: + del output[:-60000] + + matches = re.findall(rb'(\d{1,3})%', bytes(output[-512:])) + if matches: + percent = min(100, int(matches[-1])) + if percent != last_percent: + last_percent = percent + self.update_progress( + percent, + 100, + self.tf('progress_extracting_percent', percent=percent) + ) + + return_code = proc.wait() + decoded_output = self._decode_7z_output(bytes(output)) + if return_code == 0: + self.update_progress(100, 100, self.t('progress_resource_done')) + return True, decoded_output + if use_progress_switch and "incorrect command line" in decoded_output.lower(): + return self._extract_with_7za_basic() + return False, decoded_output + + def _extract_with_7za_basic(self): + cmd = [ + self.sz, 'x', str(self.package_file), + f'-p{self.extract_password}', + f'-o{self.temp_dir}', '-y' + ] + result = subprocess.run( + cmd, + capture_output=True, + stdin=subprocess.DEVNULL, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + decoded_output = self._decode_7z_output(result.stdout + result.stderr) + if result.returncode == 0: + self.update_progress(100, 100, self.t('progress_resource_done')) + return True, decoded_output + return False, decoded_output + + def extract_package_silent(self): + """Extract package.bin silently with progress.""" + if not self.package_file.exists(): + self.log(self.tf('log_package_missing', path=self.package_file), "ERROR") + return False + + if not self.extract_password: + self.log(self.t('log_extract_password_missing'), "ERROR") + return False + + if not os.path.exists(self.sz): + self.log(self.tf('log_7za_missing', path=self.sz), "ERROR") + return False + + try: + hidden_path = self._cache_dir_path().parent + hidden_path.mkdir(parents=True, exist_ok=True) + + self.temp_dir = self._cache_dir_path() + + if self.temp_dir.exists(): + shutil.rmtree(self.temp_dir, ignore_errors=True) + time.sleep(0.5) + + self.temp_dir.mkdir(parents=True, exist_ok=True) + + if sys.platform == 'win32': + subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True) + subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True) + + self.log(self.t('info_extracting'), "INFO") + + ok, err_msg = self._extract_with_7za_progress() + if not ok: + self.log(self._format_extract_error(err_msg, 1), "ERROR") + self._clear_extracted_cache() + return False + + self.apps_dir = None + self.priv_apps_dir = None + self.menu_key_apk = 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] + self._scan_root_level_apks() + + 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(self.t('warn_no_app_dir'), "WARNING") + self._clear_extracted_cache() + return False + + ok, reason = self._validate_extracted_apks() + if not ok: + self.log(self.tf('log_extracted_resource_invalid', reason=reason), "ERROR") + self._clear_extracted_cache() + return False + self.log(self.t('log_extract_done'), "SUCCESS") + return True + + except Exception as e: + if getattr(self, 'debug_mode', False): + self.log(self.tf('log_extract_exception', error=str(e)), "ERROR") + import traceback + self.log(traceback.format_exc(), "ERROR") + else: + self.log(self.t('err_extract_user'), "ERROR") + self._clear_extracted_cache() + 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(self.tf('log_package_missing', path=self.package_file), "WARNING") + else: + self._try_reuse_extracted() + else: + self.log(self.t('log_adb_missing'), "ERROR") + except FileNotFoundError: + self.log(self.t('log_adb_missing'), "ERROR") + + def _try_reuse_extracted(self): + """检查磁盘上是否已有解压好的资源,有则直接复用""" + cache_dir = self._cache_dir_path() + 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] + self._scan_root_level_apks() + if has_priv: + self.priv_apps_dir = priv_candidates[0] + self.temp_dir = cache_dir + ok, reason = self._validate_extracted_apks() + if not ok: + self.log(self.tf('log_cache_reuse_invalid', reason=reason), "WARNING") + self._clear_extracted_cache() + return + # self.log("已复用缓存的资源文件", "INFO") + + def refresh_device_status(self): + """Refresh device status.""" + 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().splitlines() + devices = [line for line in lines[1:] if line.strip() and 'device' in line and 'offline' not in line] + + if devices: + if not was_connected: + self.log(self.t('log_device_connected'), "SUCCESS") + + vin = '' + vin_ok, vin_output = self.run_adb_shell('settings get system ca.car.vin') + if vin_ok: + vin = vin_output.strip() + if not vin or vin == 'null': + vin = '' + if vin: + self.log(self.tf('log_current_vin', vin=vin), "INFO") + authorized = self.check_authorization(vin) + self.update_device_status(True, vin, authorized) + else: + self.log(self.t('log_vin_unavailable'), "WARNING") + self.update_device_status(True, None, False) + else: + if was_connected: + self.log(self.t('log_device_disconnected'), "WARNING") + self.update_device_status(False) + except Exception as e: + self.log(self.tf('log_refresh_failed', error=str(e)), "ERROR") + finally: + self._refreshing = False + + threading.Thread(target=refresh, daemon=True).start() + + def check_authorization(self, vin): + """Check authorization.""" + if getattr(self, 'debug_mode', False): + self.log(self.t('log_auth_skip_debug'), "WARNING") + return True + self.log(self.t('log_auth_checking'), "INFO") + try: + authorized, vehicle_name, _ = self.query_authorization_info(vin) + if authorized: + self.log(self.t('log_auth_ok'), "SUCCESS") + if vehicle_name: + self.vehicle_name = vehicle_name + self.log(self.tf('log_vehicle_name', name=vehicle_name), "INFO") + return True + else: + self.log(self.t('log_auth_failed'), "ERROR") + return False + + except Exception: + self.log(self.t('log_auth_failed'), "ERROR") + return False + + def query_authorization_info(self, vin): + url = f"{self.api_url}?{urlencode({'vin': vin})}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + payload = data.get('data', {}) if isinstance(data, dict) else {} + vehicle_name = payload.get('vehicleName') or payload.get('vehicle_name') or "" + vehicle_name = str(vehicle_name).strip() + if data.get('authorized') is True and vehicle_name: + self.vehicle_name = vehicle_name + return data.get('authorized') is True, vehicle_name, data + + def fetch_package_password(self): + """Fetch package password from server.""" + if not self.vin: + self.log(self.t('log_adb_required'), "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, 'vehicleName': self.PACKAGE_KEY_VEHICLE_NAME})}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + if data.get('success') and 'data' in data and 'password' in data['data']: + self.extract_password = data['data']['password'] + return True + else: + self.log(self.tf('log_data_prepare_failed_detail', error=data.get('message', self.t('unknown_error'))), "ERROR") + return False + + except Exception as e: + self.log(self.tf('log_data_prepare_failed_detail', error=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: + output = result.stdout.strip() + err = result.stderr.strip() + if err: + output = f"{output}\n{err}".strip() + return True, output + else: + return False, result.stderr.strip() + except Exception as e: + return False, str(e) + + def run_adb_password_command(self, command, timeout=30): + """执行需要设备验证密码的 adb 命令,并自动输入 adb36987。""" + command = command.replace('adb', self._adb_cmd(), 1) + if self.debug_mode: + self.log(f"CMD: {command}", "CMD") + try: + proc = subprocess.Popen( + command, + shell=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding='utf-8', + errors='replace' + ) + stdout, stderr = proc.communicate(input='adb36987\n', timeout=timeout) + combined = (stdout or "") + if stderr: + combined += ("\n" if combined else "") + stderr + + output_lines = [] + for line in combined.splitlines(): + stripped = line.strip() + lower = stripped.lower() + if 'please input verify password' in lower: + continue + if stripped == 'verify success!': + continue + output_lines.append(line) + output = "\n".join(output_lines).strip() + + if self.debug_mode: + self.log(f"CMD RET: {proc.returncode}", "CMD") + if output: + self.log(f"CMD OUTPUT:\n{output[:1000]}", "CMD") + if proc.returncode == 0: + return True, output + return False, output + except subprocess.TimeoutExpired: + proc.kill() + proc.communicate() + return False, "command timeout" + except Exception as e: + return False, str(e) + + def run_adb_shell(self, shell_command): + """执行 adb shell 命令,自动静默输入设备密码 adb36987。""" + command = f'{self._adb_cmd()} -d shell {shell_command}' + if self.debug_mode: + self.log(f"CMD: adb shell {shell_command}", "CMD") + try: + proc = subprocess.Popen( + command, + shell=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding='utf-8', + errors='replace' + ) + stdout, stderr = proc.communicate(input='adb36987\n', timeout=30) + combined = (stdout or "") + if stderr: + combined += ("\n" if combined else "") + stderr + + output_lines = [] + for line in combined.splitlines(): + stripped = line.strip() + lower = stripped.lower() + if 'please input verify password' in lower: + continue + if stripped == 'verify success!': + continue + output_lines.append(line) + output = "\n".join(output_lines).strip() + + if self.debug_mode: + self.log(f"CMD RET: {proc.returncode}", "CMD") + if output: + self.log(f"CMD OUTPUT:\n{output[:1000]}", "CMD") + if proc.returncode == 0: + return True, output + return False, output + except subprocess.TimeoutExpired: + proc.kill() + proc.communicate() + return False, "command timeout" + except Exception as e: + return False, str(e) + + def grant_menu_key_overlay_permission(self): + package_name = "com.magicianguo.virtualkey" + commands = [ + f"appops set --user 0 {package_name} SYSTEM_ALERT_WINDOW allow", + f"appops set --user 10 {package_name} SYSTEM_ALERT_WINDOW allow", + ] + failed = [] + for command in commands: + ok, output = self.run_adb_shell(command) + if not ok: + failed.append(output or command) + + if failed: + fallback_ok, fallback_output = self.run_adb_shell( + f"appops set {package_name} SYSTEM_ALERT_WINDOW allow" + ) + if not fallback_ok: + if self.debug_mode: + self.log(f"MenuKey overlay permission failed: {'; '.join(failed)}; {fallback_output}", "ERROR") + return False + + return True + + def install_apk_file_with_current_model(self, apk_path): + """Use the same APK install path as the Install App button.""" + apk_path = Path(apk_path) + remote_path = f"/data/local/tmp/{apk_path.name}" + push_ok, push_err = self.run_adb_command(f'adb -d push "{apk_path}" {remote_path}') + if not push_ok: + return False, push_err + + install_ok, install_err = self.run_adb_shell(f'pm install -r -d "{remote_path}"') + self.run_adb_shell(f'rm -f "{remote_path}"') + return install_ok, install_err + + def install_menu_key_before_flash(self): + """Install root-level MenuKey-release.apk and grant overlay permission.""" + menu_key_apk = self.menu_key_apk + if not menu_key_apk and self.apps_dir and self.apps_dir.exists(): + candidate = self.apps_dir.parent / "MenuKey-release.apk" + if candidate.exists(): + menu_key_apk = candidate + self.menu_key_apk = candidate + + if not menu_key_apk or not menu_key_apk.exists(): + if self.debug_mode: + self.log("MenuKey-release.apk not found, skip install", "CMD") + return True + + try: + self.run_adb_shell('setprop vecentek.model 1') + + install_ok, install_err = self.install_apk_file_with_current_model(menu_key_apk) + if not install_ok: + if self.debug_mode: + self.log(f"MenuKey install failed: {install_err}", "ERROR") + self.log(self.t('log_menu_key_install_failed'), "ERROR") + return False + finally: + self.run_adb_shell('setprop vecentek.model 0') + + if not self.grant_menu_key_overlay_permission(): + self.log(self.t('log_menu_key_permission_failed'), "WARNING") + else: + self.log(self.t('log_menu_key_ready'), "SUCCESS") + + return True + + 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, self.tf('err_push_failed', error=err) + + self.run_adb_shell(f'mkdir -p {target_dir}') + ok, err = self.run_adb_shell(f'cp {temp_apk_path} {target_apk_path}') + self.run_adb_shell(f'rm -f {temp_apk_path}') + if not ok: + return False, self.tf('err_cp_failed', error=err) + + return True, "" + + def disable_system_upgrade_for_flash(self): + success, output = self.run_adb_shell('pm disable-user --user 0 com.incall.apps.softmanager') + if success: + self.log(self.t('log_disable_success'), "SUCCESS") + return True + self.log(self.t('log_disable_failed'), "ERROR") + return False + + def ensure_root_ready_for_flash(self): + """刷入前确认 adbd 已经处于 root 状态。""" + ok, output = self.run_adb_command('adb -d root') + if ok and 'adbd is already running as root' in (output or '').lower(): + return True + self.log(self.t('log_flash_readonly'), "ERROR") + return False + + def clean_preinstalled_apps_for_flash(self): + """刷入语言包前清理 Mazda 预置应用。""" + self.log(self.t('log_preclean_start'), "INFO") + success_count = 0 + for package_name in self.mazda_disable_packages: + ok, output = self.run_adb_shell(f'pm disable-user {package_name}') + if ok: + success_count += 1 + if self.debug_mode: + self.log(self.tf('log_preclean_item_done', package=package_name), "SUCCESS") + elif self.debug_mode: + self.log(self.tf('log_preclean_item_failed', package=package_name), "ERROR") + + if success_count == len(self.mazda_disable_packages): + self.log(self.t('log_preclean_done'), "SUCCESS") + return True + + self.log(self.t('log_preclean_partial'), "WARNING") + return success_count > 0 + + def push_all_apks(self): + """推送APK到系统分区(支持app和priv-app)""" + if not self.check_device_connection(): + return + if not self.vin and not self.debug_mode: + messagebox.showwarning(self.t('msg_warn_title'), self.t('warn_no_vin')) + return + + messagebox.showwarning(self.t('warn_flash_warning'), self.t('warn_flash_msg')) + + def do_push_all(): + if not self.check_authorization(self.vin): + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized'))) + return + if not self.ensure_root_ready_for_flash(): + self.run_on_ui_thread(lambda: messagebox.showwarning(self.t('msg_warn_title'), self.t('log_flash_readonly'))) + return + if not self.clean_preinstalled_apps_for_flash(): + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_error_title'), self.t('log_preclean_partial'))) + return + if not self.disable_system_upgrade_for_flash(): + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_disable_failed'))) + return + if not self.extract_password: + if not self.fetch_package_password(): + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_prepare_failed'))) + 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(self.t('msg_error_title'), self.t('msg_resource_prepare_failed'))) + return + self.show_progress(False, is_push=False) + + if (not self.apps_dir or not self.apps_dir.exists()) and \ + (not self.priv_apps_dir or not self.priv_apps_dir.exists()): + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_dir_missing'))) + return + + self.show_progress(True, is_push=True) + self.run_adb_shell('mkdir -p /data/local/tmp') + self.install_menu_key_before_flash() + + 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(self.t('warn_no_apk'), "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(self.t('warn_no_apk'), "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(self.t('log_flash_readonly'), "ERROR") + aborted = True + break + self.update_progress(i, total, self.t('progress_flashing'), is_push=True) + + self.update_progress(total, total, self.t('progress_flash_done') if not aborted else self.t('progress_aborted'), is_push=True) + + if success_count == total: + self.log(self.tf('log_flash_complete_count', count=total), "SUCCESS") + self.log(self.t('log_flash_effect_after_reboot'), "WARNING") + elif success_count > 0: + self.log(self.tf('log_flash_partial', success=success_count, total=total), "WARNING") + if not aborted: + self.log(self.t('log_flash_effect_after_reboot'), "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=self.t('dialog_select_apk_folder')) + if not apk_dir: + return + + apk_files = list(Path(apk_dir).glob("*.apk")) + if not apk_files: + messagebox.showerror(self.t('msg_error_title'), self.t('msg_no_apk_in_folder')) + return + + result = messagebox.askyesno( + self.t('msg_confirm_install_title'), + self.tf('msg_confirm_install_folder', count=len(apk_files)) + ) + if not result: + return + + def install(): + self.show_progress(True, is_push=True) + total = len(apk_files) + self.log(self.tf('log_install_start', count=total), "INFO") + success_count = 0 + try: + self.run_adb_shell('setprop vecentek.model 1') + + for i, apk_path in enumerate(apk_files, 1): + self.update_progress(i, total, self.t('progress_installing'), is_push=True) + temp_apk_path = f"/data/local/tmp/{apk_path.name}" + push_ok, _ = self.run_adb_command(f'adb -d push "{apk_path}" {temp_apk_path}') + success = False + if push_ok: + success, _ = self.run_adb_shell(f'pm install -r -d "{temp_apk_path}"') + self.run_adb_shell(f'rm -f "{temp_apk_path}"') + if success: + success_count += 1 + + self.update_progress(total, total, self.t('progress_done'), is_push=True) + + if success_count == total: + self.log(self.tf('log_install_done_all', count=total), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('info_install_done'), self.tf('msg_install_success_many', count=total)) + elif success_count > 0: + self.log(self.tf('log_install_done_partial', success=success_count, total=total), "WARNING") + self.run_on_ui_thread(messagebox.showwarning, self.t('info_install_done'), self.tf('msg_install_partial', success=success_count, failed=total - success_count)) + else: + self.log(self.t('log_install_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('log_install_failed'), self.t('msg_install_all_failed')) + except Exception as e: + self.log(self.tf('log_install_exception', error=str(e)), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('log_install_failed'), self.tf('msg_install_exception', error=str(e))) + finally: + self.run_adb_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=self.t('dialog_select_apk'), + filetypes=[(self.t('file_apk'), "*.apk"), (self.t('file_all'), "*.*")] + ) + + if not file_path: + return + + def install(): + self.show_progress(True, is_push=True) + self.update_progress(50, 100, self.t('progress_installing'), is_push=True) + try: + self.run_adb_shell('setprop vecentek.model 1') + temp_apk_path = f"/data/local/tmp/{Path(file_path).name}" + push_ok, _ = self.run_adb_command(f'adb -d push "{file_path}" {temp_apk_path}') + success = False + if push_ok: + success, _ = self.run_adb_shell(f'pm install -r -d "{temp_apk_path}"') + self.run_adb_shell(f'rm -f "{temp_apk_path}"') + self.update_progress(100, 100, self.t('progress_done'), is_push=True) + if success: + self.log(self.t('info_install_done'), "SUCCESS") + else: + self.log(self.t('log_install_failed'), "ERROR") + except Exception as e: + self.log(self.tf('log_install_exception', error=str(e)), "ERROR") + finally: + self.run_adb_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_shell('am start -a android.settings.LOCALE_SETTINGS') + + def open_language_quick_set(self): + """打开快捷语言设置弹窗""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 创建弹窗 + popup = tk.Toplevel(self.root) + popup.title(self.t('title_pop_lang')) + popup.geometry("520x320") + popup.configure(bg=self.colors['bg_dark']) + popup.resizable(False, False) + + # 居中显示 + popup.update_idletasks() + x = self.root.winfo_x() + (self.root.winfo_width() - 520) // 2 + y = self.root.winfo_y() + (self.root.winfo_height() - 320) // 2 + popup.geometry(f"+{x}+{y}") + popup.transient(self.root) + popup.grab_set() + + # 标题 + header = tk.Label(popup, text=self.t('quick_lang_header'), + font=('Microsoft YaHei', 13, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + header.pack(pady=(15, 10)) + + hint = tk.Label(popup, text=self.t('quick_lang_hint'), + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + hint.pack(pady=(0, 12)) + + # 语言列表:(显示名, locale_code) + 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=self.t('quick_lang_system'), + command=lambda: self._open_sys_and_close(popup), + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + sys_btn.pack(pady=(0, 10)) + + def _quick_set_language(self, locale_code, language_name, popup): + """执行快捷语言设置""" + popup.destroy() + + def do_set(): + self.log(self.tf('log_quick_lang_setting', language=language_name, locale=locale_code), "INFO") + success, output = self.run_adb_shell(f'settings put system system_locales {locale_code}') + + if success: + self.log(self.tf('log_quick_lang_success', language=language_name), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('msg_success_title'), + self.tf('msg_quick_lang_success', language=language_name) + ) + else: + self.log(self.tf('log_quick_lang_failed', output=output), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.tf('msg_quick_lang_failed', output=output)) + + threading.Thread(target=do_set, daemon=True).start() + + def _open_sys_and_close(self, popup): + """关闭弹窗并打开系统语言设置""" + popup.destroy() + self.open_language_settings() + + def open_timezone_settings(self): + """打开时区设置""" + if not self.check_device_connection(): + return + self.run_adb_shell('am start -a android.settings.TIMEZONE_SETTINGS') + + def open_android_settings(self): + """打开安卓原生设置""" + if not self.check_device_connection(): + return + self.run_adb_shell('am start -a android.settings.SETTINGS') + + def reboot_device(self): + """重启设备""" + if not self.check_device_connection(): + return + if messagebox.askyesno(self.t('msg_reboot_confirm_title'), self.t('confirm_reboot')): + def do_restart_framework(): + self.log(self.t('log_rebooting'), "INFO") + root_ok, root_output = self.run_adb_password_command('adb -d root', timeout=30) + if not root_ok: + if self.debug_mode: + self.log(f"adb root failed: {root_output}", "ERROR") + self.log(self.t('log_reboot_failed'), "ERROR") + return + + time.sleep(2) + stop_ok, stop_output = self.run_adb_shell('stop') + start_ok, start_output = self.run_adb_shell('start') + if stop_ok and start_ok: + self.update_device_status(False) + else: + if self.debug_mode: + if not stop_ok: + self.log(f"adb shell stop failed: {stop_output}", "ERROR") + if not start_ok: + self.log(f"adb shell start failed: {start_output}", "ERROR") + self.log(self.t('log_reboot_failed'), "ERROR") + + threading.Thread(target=do_restart_framework, daemon=True).start() + + def on_disable_upgrade(self): + """禁用系统升级""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 弹窗确认 + result = messagebox.askyesno( + self.t('msg_disable_confirm_title'), + self.t('msg_disable_confirm') + ) + + if not result: + self.log(self.t('log_disable_cancelled'), "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(self.t('log_disable_success'), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('msg_success_title'), self.t('msg_disable_success')) + else: + self.log(self.t('log_disable_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.tf('msg_disable_failed', output=output)) + self.show_progress(False, is_push=False) + + threading.Thread(target=disable, daemon=True).start() + + def _on_vin_input_focus_in(self, event): + """输入框获得焦点时清除占位符""" + if self.is_placeholder_vin(self.vin_input.get()): + self.vin_input.delete(0, tk.END) + self.vin_input.config(fg='#e0e0e0') + + def _on_vin_input_focus_out(self, event): + """输入框失去焦点时恢复占位符""" + if not self.vin_input.get(): + self.vin_input.insert(0, self.t('vin_placeholder')) + self.vin_input.config(fg='#636e72') + + def query_password_by_vin(self): + """通过VIN查询工程密码""" + vin = self.vin_input.get().strip() + if not vin or self.is_placeholder_vin(vin): + messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_input_vin')) + return + + def do_query(): + try: + api_url = "https://api.changan.softwindy.cn/api/authorizations/generate-password-by-vin" + url = f"{api_url}?{urlencode({'vin': vin})}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + def update_ui(): + if data.get('success'): + pwd = data.get('data', {}).get('devicePassword', 'unknown') + self.pwd_result_label.config( + text=self.tf('pwd_success', password=pwd), + fg=self.colors['success'] + ) + self.log(self.t('log_pwd_success'), "SUCCESS") + else: + msg = data.get('message', 'failed') + self.pwd_result_label.config( + text=self.tf('pwd_failed', message=msg), + fg=self.colors['error'] + ) + self.log(self.tf('log_pwd_failed', message=msg), "ERROR") + + self.run_on_ui_thread(update_ui) + + except Exception as e: + def update_ui_error(): + self.pwd_result_label.config( + text=self.t('pwd_request_failed'), + fg=self.colors['error'] + ) + self.log(self.tf('log_pwd_request_failed', error=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(self.t('log_debug_off'), "WARNING") + self.status_text.config(text=self.t('status_ready')) + self.set_debug_buttons_visible(False) + self.refresh_device_status() + return + + pwd = simpledialog.askstring(self.t('status_debug'), self.t('debug_password_prompt'), show='*', parent=self.root) + if not pwd: + return + + self.log(self.t('debug_password_verifying'), "INFO") + + def verify(): + valid, message = self.verify_debug_mode_password(pwd) + if valid: + def enable_debug(): + self.debug_mode = True + self.update_device_status(True, "", True) + self.log(self.t('log_debug_on'), "WARNING") + self.status_text.config(text=self.t('status_debug')) + self.set_debug_buttons_visible(True) + self.run_on_ui_thread(enable_debug) + else: + def show_failed(): + msg = message or self.t('debug_wrong_password') + self.log(self.tf('debug_verify_failed', message=msg), "WARNING") + messagebox.showwarning(self.t('msg_error_title'), msg) + self.run_on_ui_thread(show_failed) + + threading.Thread(target=verify, daemon=True).start() + + def verify_debug_mode_password(self, password): + try: + payload = json.dumps({"password": password}).encode('utf-8') + req = Request( + self.debug_password_api_url, + data=payload, + method='POST', + headers={ + 'Content-Type': 'application/json', + 'User-Agent': 'Mozilla/5.0', + } + ) + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + if data.get('success') is True and data.get('valid') is True: + return True, data.get('message', '') + return False, data.get('message') or self.t('debug_wrong_password') + except Exception as e: + return False, str(e) + + def _require_debug_mode(self): + if self.debug_mode: + return True + messagebox.showwarning(self.t('status_debug'), self.t('debug_need_enable')) + return False + + def install_apps(self): + """安装App — 支持单选或多选APK文件""" + if not self.check_device_connection(): + return + if not self.vin and not self.debug_mode: + messagebox.showwarning(self.t('msg_warn_title'), self.t('warn_no_vin')) + return + + file_paths = filedialog.askopenfilenames( + title=self.t('dialog_select_apk'), + filetypes=[(self.t('file_apk'), "*.apk"), (self.t('file_all'), "*.*")] + ) + if not file_paths: + return + + count = len(file_paths) + result = messagebox.askyesno( + self.t('msg_confirm_install_title'), + self.tf('msg_confirm_install_many', count=count) + ) + if not result: + return + + def install(): + if not self.check_authorization(self.vin): + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_auth_failed_title'), + self.t('msg_device_unauthorized') + ) + return + + self.show_progress(True, is_push=True) + self.log(self.tf('log_install_start', count=count), "INFO") + success_count = 0 + try: + self.run_adb_shell('setprop vecentek.model 1') + + for i, file_path in enumerate(file_paths, 1): + apk_name = Path(file_path).name + self.update_progress(i, count, self.tf('progress_installing_name', name=apk_name), is_push=True) + success, _ = self.install_apk_file_with_current_model(file_path) + if success: + self.log(self.tf('log_install_success_item', name=apk_name), "SUCCESS") + success_count += 1 + else: + self.log(self.tf('log_install_failed_item', name=apk_name), "ERROR") + + self.update_progress(count, count, self.t('progress_done'), is_push=True) + + if success_count == count: + self.log(self.tf('log_install_done_all', count=count), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('info_install_done'), self.tf('msg_install_success_many', count=count)) + elif success_count > 0: + self.log(self.tf('log_install_done_partial', success=success_count, total=count), "WARNING") + self.run_on_ui_thread(messagebox.showwarning, self.t('info_install_done'), self.tf('msg_install_partial', success=success_count, failed=count - success_count)) + else: + self.log(self.t('log_install_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('log_install_failed'), self.t('msg_install_all_failed')) + except Exception as e: + self.log(self.tf('log_install_exception', error=str(e)), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('log_install_failed'), self.tf('msg_install_exception', error=str(e))) + finally: + self.run_adb_shell('setprop vecentek.model 0') + self.show_progress(False, is_push=True) + + threading.Thread(target=install, daemon=True).start() + + def _debug_test_extract(self, event=None): + self.debug_test_package_extract() + + def debug_test_package_extract(self): + """Debug-only package extraction test using package-key.""" + if not self._require_debug_mode(): + return + + if not self.vin: + vin = simpledialog.askstring(self.t('status_debug'), 'VIN:', parent=self.root) + if vin: + self.vin = vin.strip().upper() + if not self.vin: + messagebox.showwarning(self.t('status_debug'), self.t('debug_need_vin')) + return + + def do_extract(): + old_password = self.extract_password + try: + self.log(self.t('info_extracting'), "INFO") + self.extract_password = None + if not self.fetch_package_password(): + self.log(self.t('log_package_key_failed'), "ERROR") + return + self.show_progress(True, is_push=False) + if self.extract_package_silent(): + self.log(self.t('log_package_extract_success'), "SUCCESS") + else: + self.log(self.t('log_package_extract_failed'), "ERROR") + finally: + self.extract_password = old_password + self.show_progress(False, is_push=False) + + threading.Thread(target=do_extract, daemon=True).start() + + def run(self): + """运行程序""" + self.root.mainloop() + +def main(): + """主函数""" + if sys.version_info < (3, 6): + print("错误:需要Python 3.6或更高版本") + sys.exit(1) + + try: + app = ADKAPKGUI() + app.run() + except Exception as e: + print(f"启动失败: {e}") + import traceback + traceback.print_exc() + messagebox.showerror("错误", f"程序启动失败: {e}") + +if __name__ == "__main__": + main() + diff --git a/Mazda-EZ60/Mazda_EZ60-Language-Install_v1.0_direct_push.py b/Mazda-EZ60/Mazda_EZ60-Language-Install_v1.0_direct_push.py new file mode 100644 index 0000000..93d6177 --- /dev/null +++ b/Mazda-EZ60/Mazda_EZ60-Language-Install_v1.0_direct_push.py @@ -0,0 +1,2443 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import subprocess +import json +import re +import threading +import tkinter as tk +import atexit +from tkinter import ttk, scrolledtext, filedialog, messagebox, simpledialog +from pathlib import Path +from urllib.request import urlopen, Request +from urllib.error import URLError, HTTPError +from urllib.parse import urlencode +from datetime import datetime +import zipfile +try: + import pyzipper +except ImportError: + pyzipper = None +import shutil +import time + + +def get_app_dir(): + return Path(sys.executable).resolve().parent if getattr(sys, 'frozen', False) else Path(__file__).resolve().parent + + +def resource_candidates(file_name): + base_dir = get_app_dir() + candidates = [] + if getattr(sys, 'frozen', False): + candidates.append(Path(getattr(sys, '_MEIPASS', base_dir)) / file_name) + candidates.extend([ + base_dir / file_name, + base_dir / 'tools' / file_name, + base_dir / 'shared' / file_name, + base_dir.parent / 'tools' / file_name, + base_dir.parent / 'shared' / file_name, + base_dir.parent / file_name, + ]) + unique = [] + for candidate in candidates: + if candidate not in unique: + unique.append(candidate) + return unique + + +def find_resource(file_name): + candidates = resource_candidates(file_name) + for candidate in candidates: + if candidate.exists(): + return candidate + return candidates[0] + + +def find_tool(file_name, fallback=None): + path = find_resource(file_name) + if path.exists(): + return str(path) + return fallback or str(path) + + +def set_windows_app_user_model_id(): + if sys.platform != 'win32': + return + try: + import ctypes + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + "yibin.keyi.mazda.ez60.language.installer" + ) + except Exception: + pass + + +class ADKAPKGUI: + CACHE_DIR_NAME = "apps_cache_Mazda_EZ60_1_0" + PACKAGE_KEY_VEHICLE_NAME = "EZ60_1.0" + + def __init__(self): + set_windows_app_user_model_id() + self.root = tk.Tk() + self.root.title("长安语言安装工具") + self.root.geometry("650x640") + self.root.resizable(True, True) + self.set_window_icon() + + # 固定颜色 + 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.lang = 'zh' + self.T = { + 'zh': { + 'title': '马自达EZ60刷机工具_OS-1.0', + 'btn_root': '🔓 获取权限', + 'btn_push': '📦 刷入语言包', + 'btn_install': '📱 安装App', + 'btn_language': '🌐 语言设置', + 'btn_timezone': '⏰ 时区设置', + 'btn_settings': '⚙️ 安卓设置', + 'btn_reboot': '🔄 重启设备', + 'btn_disable_upgrade': '❌ 禁用升级', + 'btn_clear_log': '🗑 清空日志', + 'btn_query_pwd': '查询密码', + 'btn_debug_extract': '解压测试', + 'device_label': '设备:', + 'vin_label': 'VIN码:', + 'auth_label': '授权:', + 'log_title': '📋 运行日志', + 'status_ready': '就绪', + 'status_connected': '已连接', + 'status_disconnected': '未连接', + 'status_detecting': '未检测', + 'vin_none': '未获取', + 'auth_none': '未验证', + 'auth_yes': '已授权', + 'auth_no': '未授权', + 'btn_refresh': '🔄 检查', + 'hint_factory': '🔧 关闭车辆WI-FI和4G网络,拨号获取的密码进入工程模式', + 'warn_no_device': '设备未连接', + 'warn_connect_first': '请先连接设备并点击「检查」按钮刷新状态!', + 'warn_no_vin': '请先刷新设备状态并获取VIN码', + 'err_auth_fail': '授权失败', + 'err_device_not_auth': '设备未授权', + 'err_resource_fail': '资源准备失败!', + 'err_no_resource_dir': '资源目录未找到', + 'info_flash_start': '开始刷入语言包...', + 'info_flash_done': '语言包刷入完成,重启设备后生效', + 'info_flash_fail': '语言包刷入失败', + 'warn_no_apk': '未找到语言包文件', + 'info_installing': '安装中...', + 'info_install_done': '安装完成', + 'info_log_cleared': '日志已清空', + 'confirm_reboot': '确定要重启设备吗?', + 'info_rebooting': '设备正在重启...', + 'warn_device_disconnected': '设备已断开连接', + 'info_device_connected': '设备已连接', + 'info_checking_auth': '正在验证授权状态...', + 'info_auth_pass': '✅ 授权验证通过!', + 'info_auth_fail': '❌ 授权验证失败', + 'info_preparing': '正在准备资源...', + 'err_no_package': '错误:未找到资源包', + 'err_no_password': '错误:解压密码未设置', + 'err_no_7za': '错误:未找到 7za.exe', + 'err_extract_fail': '解压失败', + 'info_extracting': '资源准备中...', + 'info_extract_done': '资源准备完成', + 'err_extract_user': '资源准备失败,请检查网络连接后重试', + 'warn_no_app_dir': '警告:未找到 app/priv-app 目录', + 'progress_resource_loading': '资源准备中...', + 'progress_resource_done': '资源准备完成', + 'progress_extracting_percent': '资源准备中 {percent}%', + 'progress_flashing': '正在刷入', + 'progress_flash_done': '刷入完成', + 'progress_aborted': '已终止', + 'progress_installing': '安装中', + 'progress_installing_name': '安装中 ({name})', + 'progress_done': '完成', + 'lang_zh': '中', + 'lang_en': 'EN', + 'switch_lang': '语言 / Language', + 'pwd_query_label': '工程密码查询:', + 'vin_placeholder': '请输入VIN', + 'pwd_success': '密码: *#{password}#*', + 'pwd_failed': '失败: {message}', + 'pwd_request_failed': '请求失败', + 'tip_1': '1. 安装语言过程中请保持车辆和电脑的电量充足,不可中途停止。', + 'tip_2': '2. 获取权限以后,车辆自动重启以后再进入语言刷入。', + 'tip_3': '3. 部分语言需要重启后生效,可以一切工作完成以后再重启。', + 'warn_flash_warning': '⚠️ 重要提示', + 'warn_flash_msg': '刷入过程中请勿:\n ● 重启车机\n ● 退出本程序\n ● 关闭电脑\n\n否则可能导致车机系统损坏!', + 'err_wrong_password': '请检查密码是否正确', + 'title_pop_lang': '快捷语言设置', + 'quick_lang_header': '选择目标语言', + 'quick_lang_hint': '点击按钮即可将系统语言切换为对应语言,重启后生效', + 'quick_lang_system': '⚙️ 打开系统语言设置(手动选择)', + 'msg_warn_title': '警告', + 'msg_error_title': '错误', + 'msg_done_title': '完成', + 'msg_success_title': '成功', + 'msg_auth_failed_title': '授权失败', + 'msg_device_unauthorized': '设备未授权', + 'msg_input_vin': '请输入VIN码', + 'msg_resource_prepare_failed': '资源准备失败!', + 'msg_resource_dir_missing': '资源目录未找到', + 'msg_no_apk_in_folder': '所选文件夹中没有APK文件!', + 'msg_confirm_install_title': '确认安装', + 'msg_confirm_install_many': '已选择 {count} 个APK文件\n\n是否开始安装?', + 'msg_confirm_install_folder': '找到 {count} 个APK文件\n\n是否开始批量安装?', + 'msg_install_success_many': '成功安装 {count} 个APK!', + 'msg_install_partial': '成功: {success}\n失败: {failed}', + 'msg_install_all_failed': '所有APK安装失败!', + 'msg_install_exception': '安装过程异常:{error}', + 'msg_quick_lang_success': '系统语言已设置为 {language}\n\n⚠️ 请重启设备使其生效。', + 'msg_quick_lang_failed': '语言设置失败!\n\n{output}', + 'msg_disable_confirm_title': '确认禁用升级', + 'msg_disable_confirm': '⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n是否确定要禁用系统升级应用?', + 'msg_disable_success': '系统升级已成功禁用!', + 'msg_disable_failed': '禁用失败:{output}', + 'msg_reboot_confirm_title': '确认重启', + 'file_apk': 'APK文件', + 'file_all': '所有文件', + 'dialog_select_apk': '选择APK文件', + 'dialog_select_apk_folder': '选择包含APK文件的文件夹', + 'unknown_error': '未知错误', + 'status_debug': '调试模式', + 'debug_password_prompt': '请输入调试密码:', + 'debug_password_verifying': '正在校验调试密码...', + 'debug_wrong_password': '密码错误', + 'debug_verify_failed': '调试密码校验失败: {message}', + 'debug_need_enable': '请先按 Ctrl+Shift+D 进入调试模式', + 'debug_need_vin': '调试解压测试需要 VIN。请先连接设备刷新,或在调试模式中手动设置 VIN。', + 'log_debug_on': '🔧 调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志', + 'log_debug_off': '调试模式已关闭', + 'log_lang_switched': '语言已切换为中文', + 'log_cache_invalid': '已解压缓存无效: {reason}', + 'log_cache_reuse_invalid': '缓存资源无效,已清理: {reason}', + 'log_package_missing': '未找到资源包文件: {path}', + 'log_adb_missing': '未找到adb命令,请将ADB文件放入本目录', + 'log_device_connected': '设备已连接', + 'log_device_disconnected': '设备已断开连接', + 'log_current_vin': '当前VIN: {vin}', + 'log_vin_unavailable': '无法读取VIN', + 'log_refresh_failed': '刷新设备状态失败: {error}', + 'log_auth_skip_debug': '调试模式:跳过授权', + 'log_auth_checking': '正在验证授权状态...', + 'log_auth_ok': '授权验证通过', + 'log_auth_failed': '授权验证失败', + 'log_vehicle_name': '车型名称: {name}', + 'log_adb_required': '请先连接 ADB 并获取 VIN', + 'log_data_prepare_failed_detail': '资源准备失败: {error}', + 'log_extract_password_missing': '错误:解压密码未设置', + 'log_7za_missing': '错误:未找到 7za.exe ({path})', + 'log_extracted_resource_invalid': '解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。', + 'log_extract_done': '资源准备完成', + 'log_extract_exception': '资源准备失败: {error}', + 'err_extract_wrong_password': '解压密码错误,请重新确认 package.bin 密码', + 'err_extract_data': '资源包数据错误,可能是密码错误或 package.bin 损坏', + 'err_extract_corrupt': '资源包损坏或不完整,请检查 package.bin', + 'err_extract_failed': '解压失败: {error}', + 'err_extract_failed_code': '解压失败 (返回码 {code}),请检查密码是否正确', + 'log_root_failed': '获取失败', + 'log_permission_failed': '获取失败', + 'log_permission_running': '正在获取权限中', + 'log_reboot_failed': '重启失败', + 'log_permission_ok': '获取成功', + 'log_flash_readonly': '请先点击「获取权限」获取权限后再试', + 'log_flash_complete_count': '刷入完成,共 {count} 个语言包', + 'log_flash_effect_after_reboot': '语言包已刷入完成,重启设备后生效,您可在适当时候重启', + 'log_flash_partial': '部分刷入成功({success}/{total})', + 'log_flash_failed_item_detail': '刷入失败: {name} - {error}', + 'log_install_start': '开始安装 {count} 个APK...', + 'log_install_done_all': '安装完成:全部 {count} 个成功', + 'log_install_done_partial': '安装完成:{success}/{total} 成功', + 'log_install_failed': '安装失败', + 'log_install_exception': '安装过程异常: {error}', + 'log_install_success_item': '✓ {name}', + 'log_install_failed_item': '✗ {name}', + 'log_menu_key_ready': '辅助组件已安装', + 'log_menu_key_install_failed': '辅助组件安装失败', + 'log_menu_key_permission_failed': '辅助组件权限设置失败', + 'log_quick_lang_setting': '正在设置系统语言为: {language} ({locale})', + 'log_quick_lang_success': '✓ 语言已设置为 {language}', + 'log_quick_lang_failed': '✗ 语言设置失败: {output}', + 'log_rebooting': '设备正在重启...', + 'log_disable_cancelled': '已取消禁用升级操作', + 'log_disable_success': '系统升级已禁用', + 'log_disable_failed': '禁用系统升级失败', + 'log_preclean_start': '正在清理预置应用...', + 'log_preclean_done': '预置应用清理完成', + 'log_preclean_partial': '预置应用部分清理失败', + 'log_preclean_item_done': '清理项已完成: {package}', + 'log_preclean_item_failed': '清理项执行失败: {package}', + 'log_package_key_failed': 'package-key 获取失败', + 'log_package_extract_success': '资源准备完成', + 'log_package_extract_failed': 'package.bin 解压测试失败', + 'log_pwd_success': '密码查询成功', + 'log_pwd_failed': '密码查询失败: {message}', + 'log_pwd_request_failed': '密码查询请求失败: {error}', + 'err_no_usable_apk': '未找到可用 APK', + 'err_zero_apk': '发现 0KB APK: {files}', + 'err_push_failed': 'push失败: {error}', + 'err_cp_failed': 'cp失败: {error}', + }, + 'en': { + 'title': 'Mazda EZ60 Flash Tool_OS-1.0', + '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', + 'btn_query_pwd': 'Query Pwd', + 'btn_debug_extract': 'Extract', + 'device_label': 'Device:', + 'vin_label': 'VIN:', + 'auth_label': 'Auth:', + 'log_title': '📋 Log', + 'status_ready': 'Ready', + 'status_connected': 'Connected', + 'status_disconnected': 'Disconnected', + 'status_detecting': 'Detecting', + 'vin_none': 'None', + 'auth_none': 'Unknown', + 'auth_yes': 'Authorized', + 'auth_no': 'Unauthorized', + 'btn_refresh': '🔄 Check', + 'hint_factory': '🔧 Turn off WiFi & 4G, enter factory mode with dial code', + 'warn_no_device': 'Device not connected', + 'warn_connect_first': 'Please connect device and click Check button!', + 'warn_no_vin': 'Please refresh device status and get VIN', + 'err_auth_fail': 'Authorization failed', + 'err_device_not_auth': 'Device not authorized', + 'err_resource_fail': 'Resource preparation failed!', + 'err_no_resource_dir': 'Resource directory not found', + 'info_flash_start': 'Starting language pack flashing...', + 'info_flash_done': 'Flashing complete, reboot device to take effect', + 'info_flash_fail': 'Flashing failed', + 'warn_no_apk': 'No APK files found', + 'info_installing': 'Installing...', + 'info_install_done': 'Install complete', + 'info_log_cleared': 'Log cleared', + 'confirm_reboot': 'Are you sure you want to reboot?', + 'info_rebooting': 'Device rebooting...', + 'warn_device_disconnected': 'Device disconnected', + 'info_device_connected': 'Device connected', + 'info_checking_auth': 'Verifying authorization...', + 'info_auth_pass': '✅ Authorization passed!', + 'info_auth_fail': '❌ Authorization failed', + 'info_preparing': 'Preparing resources...', + 'err_no_package': 'Error: package.bin not found', + 'err_no_password': 'Error: password not set', + 'err_no_7za': 'Error: 7za.exe not found', + 'err_extract_fail': 'Extraction failed', + 'info_extracting': 'Preparing resources...', + 'info_extract_done': 'Resource preparation complete', + 'err_extract_user': 'Resource preparation failed, check network and retry', + 'warn_no_app_dir': 'Warning: app/priv-app directory not found', + 'progress_resource_loading': 'Preparing resources...', + 'progress_resource_done': 'Resources ready', + 'progress_extracting_percent': 'Preparing resources {percent}%', + 'progress_flashing': 'Flashing', + 'progress_flash_done': 'Flash complete', + 'progress_aborted': 'Aborted', + 'progress_installing': 'Installing', + 'progress_installing_name': 'Installing ({name})', + 'progress_done': 'Done', + 'lang_zh': '中', + 'lang_en': 'EN', + 'switch_lang': 'Language', + 'pwd_query_label': 'Factory password:', + 'vin_placeholder': 'Enter VIN', + 'pwd_success': 'Password: *#{password}#*', + 'pwd_failed': 'Failed: {message}', + 'pwd_request_failed': 'Request failed', + 'tip_1': '1. Keep the vehicle and PC powered during language installation.', + 'tip_2': '2. After permission is obtained, wait for the vehicle to reboot before flashing.', + 'tip_3': '3. Some languages take effect after reboot; reboot after all work is finished.', + 'warn_flash_warning': '⚠️ Warning', + 'warn_flash_msg': 'During flashing, DO NOT:\n ● Reboot vehicle\n ● Close this app\n ● Power off PC\n\nSystem damage may occur!', + 'err_wrong_password': 'Please check password', + 'title_pop_lang': 'Quick Language Setting', + 'quick_lang_header': 'Select target language', + 'quick_lang_hint': 'Click a button to set the system language. Reboot to apply.', + 'quick_lang_system': '⚙️ Open system language settings', + 'msg_warn_title': 'Warning', + 'msg_error_title': 'Error', + 'msg_done_title': 'Done', + 'msg_success_title': 'Success', + 'msg_auth_failed_title': 'Authorization failed', + 'msg_device_unauthorized': 'Device unauthorized', + 'msg_input_vin': 'Enter VIN', + 'msg_resource_prepare_failed': 'Resource preparation failed!', + 'msg_resource_dir_missing': 'Resource directory not found', + 'msg_no_apk_in_folder': 'No APK files found in the selected folder!', + 'msg_confirm_install_title': 'Confirm install', + 'msg_confirm_install_many': '{count} APK files selected.\n\nStart installation?', + 'msg_confirm_install_folder': '{count} APK files found.\n\nStart batch installation?', + 'msg_install_success_many': '{count} APKs installed successfully!', + 'msg_install_partial': 'Success: {success}\nFailed: {failed}', + 'msg_install_all_failed': 'All APK installations failed!', + 'msg_install_exception': 'Installation error: {error}', + 'msg_quick_lang_success': 'System language set to {language}.\n\n⚠️ Reboot the device to apply.', + 'msg_quick_lang_failed': 'Language setting failed!\n\n{output}', + 'msg_disable_confirm_title': 'Confirm Disable OTA', + 'msg_disable_confirm': '⚠️ Warning: after disabling OTA, the system will no longer receive updates.\n\nDisable the OTA app?', + 'msg_disable_success': 'System upgrade has been disabled!', + 'msg_disable_failed': 'Disable failed: {output}', + 'msg_reboot_confirm_title': 'Confirm reboot', + 'file_apk': 'APK files', + 'file_all': 'All files', + 'dialog_select_apk': 'Select APK file', + 'dialog_select_apk_folder': 'Select a folder containing APK files', + 'unknown_error': 'unknown error', + 'status_debug': 'Debug mode', + 'debug_password_prompt': 'Enter debug password:', + 'debug_password_verifying': 'Verifying debug password...', + 'debug_wrong_password': 'Wrong password', + 'debug_verify_failed': 'Debug password verification failed: {message}', + 'debug_need_enable': 'Press Ctrl+Shift+D first.', + 'debug_need_vin': 'Extract test needs a VIN. Refresh a connected device or set VIN in debug mode.', + 'log_debug_on': '🔧 Debug mode enabled - authorization and device checks are skipped, detailed ADB logs are shown', + 'log_debug_off': 'Debug mode disabled', + 'log_lang_switched': 'Language switched to English', + 'log_cache_invalid': 'Extract cache invalid: {reason}', + 'log_cache_reuse_invalid': 'Cached resources invalid and cleaned: {reason}', + 'log_package_missing': 'Resource package not found: {path}', + 'log_adb_missing': 'adb not found. Put ADB files in this directory.', + 'log_device_connected': 'Device connected', + 'log_device_disconnected': 'Device disconnected', + 'log_current_vin': 'Current VIN: {vin}', + 'log_vin_unavailable': 'Unable to read VIN', + 'log_refresh_failed': 'Refresh device status failed: {error}', + 'log_auth_skip_debug': 'Debug mode: skip authorization', + 'log_auth_checking': 'Checking authorization...', + 'log_auth_ok': 'Authorization passed', + 'log_auth_failed': 'Authorization failed', + 'log_vehicle_name': 'Vehicle name: {name}', + 'log_adb_required': 'Connect ADB and get VIN first', + 'log_data_prepare_failed_detail': 'Resource preparation failed: {error}', + 'log_extract_password_missing': 'Extraction password is not set', + 'log_7za_missing': '7za.exe not found: {path}', + 'log_extracted_resource_invalid': 'Extracted resources are invalid: {reason}. Flashing stopped. Check the password or package.', + 'log_extract_done': 'Resources ready', + 'log_extract_exception': 'Resource preparation failed: {error}', + 'err_extract_wrong_password': 'Incorrect extraction password. Check the package.bin password.', + 'err_extract_data': 'Package data error. The password may be wrong or package.bin may be damaged.', + 'err_extract_corrupt': 'Package is damaged or incomplete. Check package.bin.', + 'err_extract_failed': 'Extraction failed: {error}', + 'err_extract_failed_code': 'Extraction failed (exit code {code}). Check whether the password is correct.', + 'log_root_failed': 'Permission failed', + 'log_permission_failed': 'Permission failed', + 'log_permission_running': 'Getting permission', + 'log_reboot_failed': 'Reboot failed', + 'log_permission_ok': 'Permission granted', + 'log_flash_readonly': 'Click Get Root first, then try again', + 'log_flash_complete_count': 'Flashing complete, {count} language packages', + 'log_flash_effect_after_reboot': 'Language package flashed. Reboot the device when convenient.', + 'log_flash_partial': 'Partially flashed ({success}/{total})', + 'log_flash_failed_item_detail': 'Flash failed: {name} - {error}', + 'log_install_start': 'Installing {count} APKs...', + 'log_install_done_all': 'Installation complete: all {count} succeeded', + 'log_install_done_partial': 'Installation complete: {success}/{total} succeeded', + 'log_install_failed': 'Installation failed', + 'log_install_exception': 'Installation error: {error}', + 'log_install_success_item': '✓ {name}', + 'log_install_failed_item': '✗ {name}', + 'log_menu_key_ready': 'Helper component installed', + 'log_menu_key_install_failed': 'Helper component installation failed', + 'log_menu_key_permission_failed': 'Helper component permission setup failed', + 'log_quick_lang_setting': 'Setting system language to: {language} ({locale})', + 'log_quick_lang_success': '✓ Language set to {language}', + 'log_quick_lang_failed': '✗ Language setting failed: {output}', + 'log_rebooting': 'Device rebooting...', + 'log_disable_cancelled': 'Disable OTA cancelled', + 'log_disable_success': 'System upgrade disabled', + 'log_disable_failed': 'Failed to disable system upgrade', + 'log_preclean_start': 'Cleaning preinstalled apps...', + 'log_preclean_done': 'Preinstalled apps cleaned', + 'log_preclean_partial': 'Some preinstalled apps failed to clean', + 'log_preclean_item_done': 'Cleanup item completed: {package}', + 'log_preclean_item_failed': 'Cleanup item failed: {package}', + 'log_package_key_failed': 'package-key fetch failed', + 'log_package_extract_success': 'Resources ready', + 'log_package_extract_failed': 'package.bin extract test failed', + 'log_pwd_success': 'Password query succeeded', + 'log_pwd_failed': 'Password query failed: {message}', + 'log_pwd_request_failed': 'Password query request failed: {error}', + 'err_no_usable_apk': 'No usable APK found', + 'err_zero_apk': '0KB APK found: {files}', + 'err_push_failed': 'push failed: {error}', + 'err_cp_failed': 'cp failed: {error}', + } + } + + # 从 exe/py 所在目录查找资源文件 + self.base_dir = get_app_dir() + self.adb = find_tool('adb.exe', 'adb') + self.sz = find_tool('7za.exe') + self.package_file = self._find_package_file() + self.extract_password = None + self.apps_dir = None + self.priv_apps_dir = None + self.menu_key_apk = None + self.temp_dir = None + self.api_url = "https://api.changan.softwindy.cn/api/authorizations/auth-check" + self.debug_password_api_url = "https://api.changan.softwindy.cn/api/authorizations/verify-debug-mode-password" + self.vin = None + self.vehicle_name = "" + self.device_connected = False + self._refreshing = False # 防止并发刷新 + self.debug_mode = False # 调试模式 + self.mazda_disable_packages = [ + "com.carinno.p1", + "com.wtcl.electronicdirections", + "com.ximalaya.ting.android.car", + "com.tinnove.netease.music", + "com.migu.miguplay.car", + "cn.cmvideo.car.play", + "com.tinnove.carshow", + "com.tinnove.changba", + "com.qiyi.video.iv", + "com.changan.appmarket", + "com.incall.apps.softmanager", + ] + atexit.register(self.cleanup_cache_on_exit) + + # 设置样式 + self.setup_styles() + self.setup_ui() + self.root.after(200, self.set_window_icon) + self.center_window() + + # 检查环境 + self.check_environment() + + # 启动设备状态监控 + self.start_device_monitor() + + def set_window_icon(self): + """Set the Tk window/taskbar icon at runtime; PyInstaller --icon only sets the exe file icon.""" + try: + icon_path = find_resource("app.ico") + if icon_path.exists(): + self.root.iconbitmap(str(icon_path)) + self._set_windows_hwnd_icon(icon_path) + except Exception: + pass + + def _set_windows_hwnd_icon(self, icon_path): + if sys.platform != 'win32': + return + try: + import ctypes + user32 = ctypes.windll.user32 + hwnd = self.root.winfo_id() + image_icon = 1 + lr_loadfromfile = 0x00000010 + wm_seticon = 0x0080 + icon_small = 0 + icon_big = 1 + path = str(icon_path) + small = user32.LoadImageW(None, path, image_icon, 16, 16, lr_loadfromfile) + big = user32.LoadImageW(None, path, image_icon, 32, 32, lr_loadfromfile) + if small: + user32.SendMessageW(hwnd, wm_seticon, icon_small, small) + if big: + user32.SendMessageW(hwnd, wm_seticon, icon_big, big) + except Exception: + pass + + def setup_styles(self): + """设置自定义样式""" + style = ttk.Style() + style.theme_use('clam') + + # 配置主颜色 + style.configure('TFrame', background=self.colors['bg_dark']) + style.configure('TLabel', background=self.colors['bg_dark'], foreground=self.colors['text']) + style.configure('TLabelframe', background=self.colors['bg_dark'], foreground=self.colors['text']) + style.configure('TLabelframe.Label', background=self.colors['bg_dark'], foreground=self.colors['accent']) + + # 配置进度条 + style.configure('TProgressbar', + background=self.colors['accent'], + troughcolor=self.colors['bg_light'], + borderwidth=0) + + def setup_ui(self): + """设置UI界面""" + # 配置根窗口 + self.root.title(self.t('title')) + 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=45) + title_frame.pack(fill=tk.X, pady=(0, 10)) + title_frame.pack_propagate(False) + + # 标题 + title_inner = tk.Frame(title_frame, bg=self.colors['bg_dark']) + title_inner.place(relx=0.5, rely=0.5, anchor='center') + self.title_icon_label = tk.Label(title_inner, + text="🚗", + font=('Microsoft YaHei', 17, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.title_icon_label.grid(row=0, column=0, padx=(0, 8), sticky='e') + self.title_label = tk.Label(title_inner, + text=self.t('title'), + font=('Microsoft YaHei', 18, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.title_label.grid(row=0, column=1, sticky='w') + + self.btn_lang_switch = tk.Button(title_frame, text="EN", + command=self.toggle_lang, + font=('Microsoft YaHei', 9, 'bold'), + fg='white', + bg=self.colors['accent'], + activebackground=self.colors['accent_hover'], + activeforeground='white', + relief=tk.FLAT, + cursor='hand2', + width=8, + height=1) + self.btn_lang_switch.place(relx=1.0, rely=0.5, x=-2, anchor='e') + + # 工程密码查询区域 + 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) + + self.pwd_query_label = tk.Label(pwd_query_frame, text=self.t('pwd_query_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.pwd_query_label.pack(side=tk.LEFT, padx=(10, 5), pady=5) + + self.vin_input = tk.Entry(pwd_query_frame, + font=('Consolas', 9), + bg='#2d2d3d', + fg='#636e72', + insertbackground='white', + relief=tk.FLAT, + width=20) + self.vin_input.insert(0, self.t('vin_placeholder')) + self.vin_input.bind("", 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=self.t('btn_query_pwd'), + command=self.query_password_by_vin, + font=('Microsoft YaHei', 8), + fg='white', + bg=self.colors['accent'], + activebackground=self.colors['accent_hover'], + activeforeground='white', + 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) + + # 按钮区域(两排,每排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) + + self.debug_button_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + self.btn_debug_extract = tk.Button(self.debug_button_frame, text=self.t('btn_debug_extract'), + command=self.debug_test_package_extract, + bg=self.colors['info'], + **btn_params) + self.btn_debug_extract.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') + + self.device_label = tk.Label(status_indicator_frame, text="设备:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.device_label.pack(side=tk.LEFT, padx=(5, 3)) + + self.device_status_label = tk.Label(status_indicator_frame, text=self.t('status_detecting'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + 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) + self.vin_label_title = tk.Label(vin_frame, text="VIN码:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.vin_label_title.pack(side=tk.LEFT) + self.vin_label = tk.Label(vin_frame, text=self.t('vin_none'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + 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) + self.auth_label_title = tk.Label(auth_frame, text="授权:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.auth_label_title.pack(side=tk.LEFT) + self.auth_label = tk.Label(auth_frame, text=self.t('auth_none'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + anchor='w', width=4) + self.auth_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 刷新按钮 + self.btn_refresh = 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') + self.btn_refresh.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 = [self.t('tip_1'), self.t('tip_2'), self.t('tip_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)) + label = tk.Label(tip_row, text=tip, + font=('Microsoft YaHei', 9), + fg=self.colors['warning'], + bg=self.colors['bg_light'], + wraplength=600, + justify=tk.LEFT) + label.pack(side=tk.LEFT) + setattr(self, f'tip_label_{i + 1}', label) + + # 解压进度条框架 + 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) + + self.log_title_label = tk.Label(log_title_frame, text="📋 运行日志", + font=('Microsoft YaHei', 10, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.log_title_label.pack(side=tk.LEFT, padx=10) + + self.btn_clear = tk.Button(log_title_frame, text="🗑 清空日志", + 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.root.bind('', self._debug_test_extract) + self.root.protocol("WM_DELETE_WINDOW", self.on_close) + + # 绑定悬停效果 + 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, + self.btn_debug_extract] + + 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, lambda: func(*args, **kwargs)) + + def _adb_cmd(self): + return subprocess.list2cmdline([self.adb]) + + def _find_package_file(self): + return find_resource("package.bin") + + def t(self, key): + """获取翻译文本""" + return self.T.get(self.lang, self.T['zh']).get(key, key) + + def tf(self, key, **kwargs): + try: + return self.t(key).format(**kwargs) + except Exception: + return self.t(key) + + def is_placeholder_vin(self, value): + return value in ( + self.T['zh'].get('vin_placeholder'), + self.T['en'].get('vin_placeholder'), + ) + + def toggle_lang(self): + """切换语言""" + self.lang = 'en' if self.lang == 'zh' else 'zh' + self.btn_lang_switch.config(text=self.t('lang_en') if self.lang == 'zh' else self.t('lang_zh')) + self._refresh_ui_texts() + self.log(self.t('log_lang_switched'), "INFO") + + def _refresh_ui_texts(self): + """刷新所有UI文本""" + t = self.t + self.root.title(t('title')) + widgets = [ + (getattr(self, 'title_label', None), 'title', 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_debug_extract', None), 'btn_debug_extract', None), + (getattr(self, 'btn_query_pwd', None), 'btn_query_pwd', None), + (getattr(self, 'btn_clear', None), 'btn_clear_log', None), + (getattr(self, 'pwd_query_label', None), 'pwd_query_label', None), + (getattr(self, 'log_title_label', None), 'log_title', None), + (getattr(self, 'device_label', None), 'device_label', None), + (getattr(self, 'vin_label_title', None), 'vin_label', None), + (getattr(self, 'auth_label_title', None), 'auth_label', None), + (getattr(self, 'btn_refresh', None), 'btn_refresh', None), + (getattr(self, 'hint_label', None), 'hint_factory', None), + (getattr(self, 'tip_label_1', None), 'tip_1', None), + (getattr(self, 'tip_label_2', None), 'tip_2', None), + (getattr(self, 'tip_label_3', None), 'tip_3', None), + ] + for w, key, _ in widgets: + if w: + w.config(text=t(key)) + if getattr(self, 'status_text', None): + self.status_text.config(text=t('status_debug') if self.debug_mode else t('status_ready')) + self.btn_lang_switch.config(text=t('lang_en') if self.lang == 'zh' else t('lang_zh')) + if getattr(self, 'vin_input', None) and self.is_placeholder_vin(self.vin_input.get()): + self.vin_input.delete(0, tk.END) + self.vin_input.insert(0, t('vin_placeholder')) + self._update_device_status_impl( + self.device_connected, + self.vin, + getattr(self, '_last_authorized', False) + ) + + def set_debug_buttons_visible(self, visible): + if not hasattr(self, 'debug_button_frame'): + return + if visible: + self.debug_button_frame.pack(pady=(0, 8)) + else: + self.debug_button_frame.pack_forget() + + 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(self.t('info_log_cleared'), "INFO") + + def _show_progress_impl(self, show=True, is_push=False): + """显示/隐藏进度条的实际实现(必须在主线程调用)""" + if is_push: + if show: + self.push_progress_label.pack() + self.push_progress.pack(fill=tk.X, pady=(2, 0)) + self.push_progress['value'] = 0 + else: + self.push_progress_label.pack_forget() + self.push_progress.pack_forget() + else: + if show: + self.progress_label.pack() + self.progress.pack(fill=tk.X, pady=(2, 0)) + self.progress['value'] = 0 + else: + self.progress_label.pack_forget() + self.progress.pack_forget() + + def show_progress(self, show=True, is_push=False): + """显示/隐藏进度条(线程安全)""" + self.run_on_ui_thread(self._show_progress_impl, show, is_push) + + def _update_progress_impl(self, value, max_value=100, label="", is_push=False): + """更新进度条的实际实现(必须在主线程调用)""" + if is_push: + percent = (value / max_value) * 100 + self.push_progress['value'] = percent + self.push_progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)") + else: + percent = (value / max_value) * 100 + self.progress['value'] = percent + self.progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)") + self.root.update_idletasks() + + def update_progress(self, value, max_value=100, label="", is_push=False): + """更新进度条(线程安全)""" + self.run_on_ui_thread(self._update_progress_impl, value, max_value, label, is_push) + + def update_device_status(self, connected, vin=None, authorized=False): + """更新设备状态显示(线程安全:立即设状态变量,UI走主线程)""" + self.device_connected = connected + if vin is not None: + self.vin = vin + self.run_on_ui_thread(self._update_device_status_impl, connected, vin, authorized) + + def _update_device_status_impl(self, connected, vin, authorized): + """设备状态UI更新的实际实现(必须在主线程调用)""" + self._last_authorized = authorized + t = self.t + if connected: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['success']) + self.device_status_label.config(text=t('status_connected'), fg=self.colors['success']) + if vin: + self.vin_label.config(text=vin, fg=self.colors['success']) + if authorized: + self.auth_label.config(text=t('auth_yes'), fg=self.colors['success']) + else: + self.auth_label.config(text=t('auth_no'), fg=self.colors['error']) + else: + self.vin_label.config(text=t('vin_none'), fg=self.colors['error']) + self.auth_label.config(text=t('auth_none'), fg=self.colors['error']) + else: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['error']) + self.device_status_label.config(text=t('status_disconnected'), fg=self.colors['error']) + self.vin_label.config(text=t('vin_none'), fg=self.colors['error']) + self.auth_label.config(text=t('auth_none'), fg=self.colors['error']) + + def check_device_connection(self): + """检查设备是否连接""" + if self.debug_mode: + return True + if not self.device_connected: + messagebox.showwarning(self.t('warn_no_device'), self.t('warn_connect_first')) + 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(self.t('log_device_disconnected'), "WARNING") + + time.sleep(5) + except: + time.sleep(5) + + threading.Thread(target=monitor, daemon=True).start() + + def get_root_permission(self): + """获取 root/remount 权限,不触发重启。""" + if not self.check_device_connection(): + return + + def get_root(): + self.show_progress(True, is_push=False) + self.log(self.t('log_permission_running'), "INFO") + + ok_root, out_root = self.run_adb_command('adb -d root') + if not ok_root: + self.log(self.t('log_root_failed'), "ERROR") + self.show_progress(False, is_push=False) + return + + time.sleep(1) + + ok_remount, out_remount = self.run_adb_password_command('adb -d remount', timeout=60) + if not ok_remount: + self.log(self.t('log_permission_failed'), "ERROR") + self.show_progress(False, is_push=False) + return + + self.log(self.t('log_permission_ok'), "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 + if has_app or has_priv: + ok, reason = self._validate_extracted_apks() + if not ok: + self.log(self.tf('log_cache_invalid', reason=reason), "ERROR") + self._clear_extracted_cache() + return False + return has_app or has_priv + + def _validate_extracted_apks(self): + apks = [] + if self.apps_dir and self.apps_dir.exists(): + apks.extend(self.apps_dir.glob("*.apk")) + if self.priv_apps_dir and self.priv_apps_dir.exists(): + apks.extend(self.priv_apps_dir.glob("*.apk")) + if not apks: + return False, self.t('err_no_usable_apk') + + zero_apks = [apk.name for apk in apks if apk.stat().st_size <= 0] + if zero_apks: + preview = ", ".join(zero_apks[:5]) + suffix = "..." if len(zero_apks) > 5 else "" + return False, self.tf('err_zero_apk', files=f"{preview}{suffix}") + return True, "" + + def _clear_extracted_cache(self): + if self.temp_dir and self.temp_dir.exists(): + shutil.rmtree(self.temp_dir, ignore_errors=True) + time.sleep(0.5) + self.apps_dir = None + self.priv_apps_dir = None + self.menu_key_apk = None + self.temp_dir = None + + def _cache_dir_path(self): + local_appdata = os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local')) + return Path(local_appdata) / ".cache" / "system" / ".android" / self.CACHE_DIR_NAME + + def _scan_root_level_apks(self): + if not self.apps_dir: + return + root_dir = self.apps_dir.parent + menu_key_candidate = root_dir / "MenuKey-release.apk" + if menu_key_candidate.exists(): + self.menu_key_apk = menu_key_candidate + + def cleanup_cache_on_exit(self): + self._clear_extracted_cache() + + def on_close(self): + self.cleanup_cache_on_exit() + self.root.destroy() + + def _format_extract_error(self, err_msg, return_code): + text = (err_msg or "").lower() + if any(marker in text for marker in ( + "wrong password", + "incorrect password", + "password is incorrect", + "data error in encrypted file", + "can not open encrypted archive", + )): + return self.t('err_extract_wrong_password') + if "data error" in text: + return self.t('err_extract_data') + if "headers error" in text or "unexpected end" in text: + return self.t('err_extract_corrupt') + if err_msg.strip(): + return self.tf('err_extract_failed', error=err_msg.strip()[:300]) + return self.tf('err_extract_failed_code', code=return_code) + + def _decode_7z_output(self, output): + for enc in ('gbk', 'utf-8'): + try: + return output.decode(enc) + except UnicodeDecodeError: + continue + return output.decode('utf-8', errors='replace') + + def _seven_zip_supports_progress_stream(self): + try: + result = subprocess.run( + [self.sz], + capture_output=True, + text=True, + errors='ignore', + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + return '-bs{o|e|p}' in (result.stdout + result.stderr) + except Exception: + return False + + def _extract_with_7za_progress(self): + self.update_progress(0, 100, self.t('progress_resource_loading')) + cmd = [ + self.sz, 'x', str(self.package_file), + f'-p{self.extract_password}', + f'-o{self.temp_dir}', '-y' + ] + use_progress_switch = self._seven_zip_supports_progress_stream() + if use_progress_switch: + cmd.extend(['-bsp1', '-bso0', '-bse1']) + + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.DEVNULL, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0, + bufsize=0 + ) + + output = bytearray() + last_percent = -1 + while True: + chunk = proc.stdout.read(1) if proc.stdout else b'' + if not chunk: + if proc.poll() is not None: + break + time.sleep(0.05) + continue + + output.extend(chunk) + if len(output) > 60000: + del output[:-60000] + + matches = re.findall(rb'(\d{1,3})%', bytes(output[-512:])) + if matches: + percent = min(100, int(matches[-1])) + if percent != last_percent: + last_percent = percent + self.update_progress( + percent, + 100, + self.tf('progress_extracting_percent', percent=percent) + ) + + return_code = proc.wait() + decoded_output = self._decode_7z_output(bytes(output)) + if return_code == 0: + self.update_progress(100, 100, self.t('progress_resource_done')) + return True, decoded_output + if use_progress_switch and "incorrect command line" in decoded_output.lower(): + return self._extract_with_7za_basic() + return False, decoded_output + + def _extract_with_7za_basic(self): + cmd = [ + self.sz, 'x', str(self.package_file), + f'-p{self.extract_password}', + f'-o{self.temp_dir}', '-y' + ] + result = subprocess.run( + cmd, + capture_output=True, + stdin=subprocess.DEVNULL, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + decoded_output = self._decode_7z_output(result.stdout + result.stderr) + if result.returncode == 0: + self.update_progress(100, 100, self.t('progress_resource_done')) + return True, decoded_output + return False, decoded_output + + def extract_package_silent(self): + """Extract package.bin silently with progress.""" + if not self.package_file.exists(): + self.log(self.tf('log_package_missing', path=self.package_file), "ERROR") + return False + + if not self.extract_password: + self.log(self.t('log_extract_password_missing'), "ERROR") + return False + + if not os.path.exists(self.sz): + self.log(self.tf('log_7za_missing', path=self.sz), "ERROR") + return False + + try: + hidden_path = self._cache_dir_path().parent + hidden_path.mkdir(parents=True, exist_ok=True) + + self.temp_dir = self._cache_dir_path() + + if self.temp_dir.exists(): + shutil.rmtree(self.temp_dir, ignore_errors=True) + time.sleep(0.5) + + self.temp_dir.mkdir(parents=True, exist_ok=True) + + if sys.platform == 'win32': + subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True) + subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True) + + self.log(self.t('info_extracting'), "INFO") + + ok, err_msg = self._extract_with_7za_progress() + if not ok: + self.log(self._format_extract_error(err_msg, 1), "ERROR") + self._clear_extracted_cache() + return False + + self.apps_dir = None + self.priv_apps_dir = None + self.menu_key_apk = 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] + self._scan_root_level_apks() + + 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(self.t('warn_no_app_dir'), "WARNING") + self._clear_extracted_cache() + return False + + ok, reason = self._validate_extracted_apks() + if not ok: + self.log(self.tf('log_extracted_resource_invalid', reason=reason), "ERROR") + self._clear_extracted_cache() + return False + self.log(self.t('log_extract_done'), "SUCCESS") + return True + + except Exception as e: + if getattr(self, 'debug_mode', False): + self.log(self.tf('log_extract_exception', error=str(e)), "ERROR") + import traceback + self.log(traceback.format_exc(), "ERROR") + else: + self.log(self.t('err_extract_user'), "ERROR") + self._clear_extracted_cache() + 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(self.tf('log_package_missing', path=self.package_file), "WARNING") + else: + self._try_reuse_extracted() + else: + self.log(self.t('log_adb_missing'), "ERROR") + except FileNotFoundError: + self.log(self.t('log_adb_missing'), "ERROR") + + def _try_reuse_extracted(self): + """检查磁盘上是否已有解压好的资源,有则直接复用""" + cache_dir = self._cache_dir_path() + 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] + self._scan_root_level_apks() + if has_priv: + self.priv_apps_dir = priv_candidates[0] + self.temp_dir = cache_dir + ok, reason = self._validate_extracted_apks() + if not ok: + self.log(self.tf('log_cache_reuse_invalid', reason=reason), "WARNING") + self._clear_extracted_cache() + return + # self.log("已复用缓存的资源文件", "INFO") + + def refresh_device_status(self): + """Refresh device status.""" + 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().splitlines() + devices = [line for line in lines[1:] if line.strip() and 'device' in line and 'offline' not in line] + + if devices: + if not was_connected: + self.log(self.t('log_device_connected'), "SUCCESS") + + vin = '' + vin_ok, vin_output = self.run_adb_shell('settings get system ca.car.vin') + if vin_ok: + vin = vin_output.strip() + if not vin or vin == 'null': + vin = '' + if vin: + self.log(self.tf('log_current_vin', vin=vin), "INFO") + authorized = self.check_authorization(vin) + self.update_device_status(True, vin, authorized) + else: + self.log(self.t('log_vin_unavailable'), "WARNING") + self.update_device_status(True, None, False) + else: + if was_connected: + self.log(self.t('log_device_disconnected'), "WARNING") + self.update_device_status(False) + except Exception as e: + self.log(self.tf('log_refresh_failed', error=str(e)), "ERROR") + finally: + self._refreshing = False + + threading.Thread(target=refresh, daemon=True).start() + + def check_authorization(self, vin): + """Check authorization.""" + if getattr(self, 'debug_mode', False): + self.log(self.t('log_auth_skip_debug'), "WARNING") + return True + self.log(self.t('log_auth_checking'), "INFO") + try: + authorized, vehicle_name, _ = self.query_authorization_info(vin) + if authorized: + self.log(self.t('log_auth_ok'), "SUCCESS") + if vehicle_name: + self.vehicle_name = vehicle_name + self.log(self.tf('log_vehicle_name', name=vehicle_name), "INFO") + return True + else: + self.log(self.t('log_auth_failed'), "ERROR") + return False + + except Exception: + self.log(self.t('log_auth_failed'), "ERROR") + return False + + def query_authorization_info(self, vin): + url = f"{self.api_url}?{urlencode({'vin': vin})}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + payload = data.get('data', {}) if isinstance(data, dict) else {} + vehicle_name = payload.get('vehicleName') or payload.get('vehicle_name') or "" + vehicle_name = str(vehicle_name).strip() + if data.get('authorized') is True and vehicle_name: + self.vehicle_name = vehicle_name + return data.get('authorized') is True, vehicle_name, data + + def fetch_package_password(self): + """Fetch package password from server.""" + if not self.vin: + self.log(self.t('log_adb_required'), "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, 'vehicleName': self.PACKAGE_KEY_VEHICLE_NAME})}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + if data.get('success') and 'data' in data and 'password' in data['data']: + self.extract_password = data['data']['password'] + return True + else: + self.log(self.tf('log_data_prepare_failed_detail', error=data.get('message', self.t('unknown_error'))), "ERROR") + return False + + except Exception as e: + self.log(self.tf('log_data_prepare_failed_detail', error=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: + output = result.stdout.strip() + err = result.stderr.strip() + if err: + output = f"{output}\n{err}".strip() + return True, output + else: + return False, result.stderr.strip() + except Exception as e: + return False, str(e) + + def run_adb_password_command(self, command, timeout=30): + """执行需要设备验证密码的 adb 命令,并自动输入 adb36987。""" + command = command.replace('adb', self._adb_cmd(), 1) + if self.debug_mode: + self.log(f"CMD: {command}", "CMD") + try: + proc = subprocess.Popen( + command, + shell=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding='utf-8', + errors='replace' + ) + stdout, stderr = proc.communicate(input='adb36987\n', timeout=timeout) + combined = (stdout or "") + if stderr: + combined += ("\n" if combined else "") + stderr + + output_lines = [] + for line in combined.splitlines(): + stripped = line.strip() + lower = stripped.lower() + if 'please input verify password' in lower: + continue + if stripped == 'verify success!': + continue + output_lines.append(line) + output = "\n".join(output_lines).strip() + + if self.debug_mode: + self.log(f"CMD RET: {proc.returncode}", "CMD") + if output: + self.log(f"CMD OUTPUT:\n{output[:1000]}", "CMD") + if proc.returncode == 0: + return True, output + return False, output + except subprocess.TimeoutExpired: + proc.kill() + proc.communicate() + return False, "command timeout" + except Exception as e: + return False, str(e) + + def run_adb_shell(self, shell_command): + """执行 adb shell 命令,自动静默输入设备密码 adb36987。""" + command = f'{self._adb_cmd()} -d shell {shell_command}' + if self.debug_mode: + self.log(f"CMD: adb shell {shell_command}", "CMD") + try: + proc = subprocess.Popen( + command, + shell=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding='utf-8', + errors='replace' + ) + stdout, stderr = proc.communicate(input='adb36987\n', timeout=30) + combined = (stdout or "") + if stderr: + combined += ("\n" if combined else "") + stderr + + output_lines = [] + for line in combined.splitlines(): + stripped = line.strip() + lower = stripped.lower() + if 'please input verify password' in lower: + continue + if stripped == 'verify success!': + continue + output_lines.append(line) + output = "\n".join(output_lines).strip() + + if self.debug_mode: + self.log(f"CMD RET: {proc.returncode}", "CMD") + if output: + self.log(f"CMD OUTPUT:\n{output[:1000]}", "CMD") + if proc.returncode == 0: + return True, output + return False, output + except subprocess.TimeoutExpired: + proc.kill() + proc.communicate() + return False, "command timeout" + except Exception as e: + return False, str(e) + + def grant_menu_key_overlay_permission(self): + package_name = "com.magicianguo.virtualkey" + commands = [ + f"appops set --user 0 {package_name} SYSTEM_ALERT_WINDOW allow", + f"appops set --user 10 {package_name} SYSTEM_ALERT_WINDOW allow", + ] + failed = [] + for command in commands: + ok, output = self.run_adb_shell(command) + if not ok: + failed.append(output or command) + + if failed: + fallback_ok, fallback_output = self.run_adb_shell( + f"appops set {package_name} SYSTEM_ALERT_WINDOW allow" + ) + if not fallback_ok: + if self.debug_mode: + self.log(f"MenuKey overlay permission failed: {'; '.join(failed)}; {fallback_output}", "ERROR") + return False + + return True + + def install_apk_file_with_current_model(self, apk_path): + """Use the same APK install path as the Install App button.""" + apk_path = Path(apk_path) + remote_path = f"/data/local/tmp/{apk_path.name}" + push_ok, push_err = self.run_adb_command(f'adb -d push "{apk_path}" {remote_path}') + if not push_ok: + return False, push_err + + install_ok, install_err = self.run_adb_shell(f'pm install -r -d "{remote_path}"') + self.run_adb_shell(f'rm -f "{remote_path}"') + return install_ok, install_err + + def install_menu_key_before_flash(self): + """Install root-level MenuKey-release.apk and grant overlay permission.""" + menu_key_apk = self.menu_key_apk + if not menu_key_apk and self.apps_dir and self.apps_dir.exists(): + candidate = self.apps_dir.parent / "MenuKey-release.apk" + if candidate.exists(): + menu_key_apk = candidate + self.menu_key_apk = candidate + + if not menu_key_apk or not menu_key_apk.exists(): + if self.debug_mode: + self.log("MenuKey-release.apk not found, skip install", "CMD") + return True + + try: + self.run_adb_shell('setprop vecentek.model 1') + + install_ok, install_err = self.install_apk_file_with_current_model(menu_key_apk) + if not install_ok: + if self.debug_mode: + self.log(f"MenuKey install failed: {install_err}", "ERROR") + self.log(self.t('log_menu_key_install_failed'), "ERROR") + return False + finally: + self.run_adb_shell('setprop vecentek.model 0') + + if not self.grant_menu_key_overlay_permission(): + self.log(self.t('log_menu_key_permission_failed'), "WARNING") + else: + self.log(self.t('log_menu_key_ready'), "SUCCESS") + + return True + + def push_single_apk(self, apk_path, apk_name, target_type="app"): + """Directly push one APK into /system/app, returning (success, error).""" + target_dir = f"/system/app/{apk_name}" + target_apk_path = f"{target_dir}/{apk_name}.apk" + + ok, err = self.run_adb_shell(f'mkdir -p "{target_dir}"') + if not ok: + return False, err or f"mkdir failed: {target_dir}" + + ok, err = self.run_adb_command(f'adb -d push "{apk_path}" "{target_apk_path}"') + if not ok: + return False, err or f"push failed: {target_apk_path}" + + ok, err = self.run_adb_shell(f'chmod 755 "{target_dir}"') + if not ok: + return False, err or f"chmod 755 failed: {target_dir}" + + ok, err = self.run_adb_shell(f'chmod 644 "{target_apk_path}"') + if not ok: + return False, err or f"chmod 644 failed: {target_apk_path}" + + return True, "" + + def disable_system_upgrade_for_flash(self): + success, output = self.run_adb_shell('pm disable-user --user 0 com.incall.apps.softmanager') + if success: + self.log(self.t('log_disable_success'), "SUCCESS") + return True + self.log(self.t('log_disable_failed'), "ERROR") + return False + + def ensure_root_ready_for_flash(self): + """刷入前确认 adbd 已经处于 root 状态。""" + ok, output = self.run_adb_command('adb -d root') + if ok and 'adbd is already running as root' in (output or '').lower(): + return True + self.log(self.t('log_flash_readonly'), "ERROR") + return False + + def clean_preinstalled_apps_for_flash(self): + """刷入语言包前清理 Mazda 预置应用。""" + self.log(self.t('log_preclean_start'), "INFO") + success_count = 0 + for package_name in self.mazda_disable_packages: + ok, output = self.run_adb_shell(f'pm disable-user {package_name}') + if ok: + success_count += 1 + if self.debug_mode: + self.log(self.tf('log_preclean_item_done', package=package_name), "SUCCESS") + elif self.debug_mode: + self.log(self.tf('log_preclean_item_failed', package=package_name), "ERROR") + + if success_count == len(self.mazda_disable_packages): + self.log(self.t('log_preclean_done'), "SUCCESS") + return True + + self.log(self.t('log_preclean_partial'), "WARNING") + return success_count > 0 + + def push_all_apks(self): + """推送APK到系统分区(支持app和priv-app)""" + if not self.check_device_connection(): + return + if not self.vin and not self.debug_mode: + messagebox.showwarning(self.t('msg_warn_title'), self.t('warn_no_vin')) + return + + messagebox.showwarning(self.t('warn_flash_warning'), self.t('warn_flash_msg')) + + def do_push_all(): + if not self.check_authorization(self.vin): + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized'))) + return + if not self.ensure_root_ready_for_flash(): + self.run_on_ui_thread(lambda: messagebox.showwarning(self.t('msg_warn_title'), self.t('log_flash_readonly'))) + return + if not self.clean_preinstalled_apps_for_flash(): + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_error_title'), self.t('log_preclean_partial'))) + return + if not self.disable_system_upgrade_for_flash(): + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_disable_failed'))) + return + if not self.extract_password: + if not self.fetch_package_password(): + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_prepare_failed'))) + 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(self.t('msg_error_title'), self.t('msg_resource_prepare_failed'))) + return + self.show_progress(False, is_push=False) + + if not self.apps_dir or not self.apps_dir.exists(): + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_dir_missing'))) + return + + self.show_progress(True, is_push=True) + self.install_menu_key_before_flash() + + 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 not all_apks: + # 缓存可能过期,强制重新解压 + self.apps_dir = None + self.temp_dir = None + if not self.fetch_package_password() or not self.extract_package_silent(): + self.log(self.t('warn_no_apk'), "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 not all_apks: + self.log(self.t('warn_no_apk'), "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: + self.log(self.tf('log_flash_failed_item_detail', name=apk_path.name, error=err), "ERROR") + aborted = True + break + self.update_progress(i, total, self.t('progress_flashing'), is_push=True) + + self.update_progress(total, total, self.t('progress_flash_done') if not aborted else self.t('progress_aborted'), is_push=True) + + if success_count == total: + self.log(self.tf('log_flash_complete_count', count=total), "SUCCESS") + self.log(self.t('log_flash_effect_after_reboot'), "WARNING") + elif success_count > 0: + self.log(self.tf('log_flash_partial', success=success_count, total=total), "WARNING") + if not aborted: + self.log(self.t('log_flash_effect_after_reboot'), "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=self.t('dialog_select_apk_folder')) + if not apk_dir: + return + + apk_files = list(Path(apk_dir).glob("*.apk")) + if not apk_files: + messagebox.showerror(self.t('msg_error_title'), self.t('msg_no_apk_in_folder')) + return + + result = messagebox.askyesno( + self.t('msg_confirm_install_title'), + self.tf('msg_confirm_install_folder', count=len(apk_files)) + ) + if not result: + return + + def install(): + self.show_progress(True, is_push=True) + total = len(apk_files) + self.log(self.tf('log_install_start', count=total), "INFO") + success_count = 0 + try: + self.run_adb_shell('setprop vecentek.model 1') + + for i, apk_path in enumerate(apk_files, 1): + self.update_progress(i, total, self.t('progress_installing'), is_push=True) + temp_apk_path = f"/data/local/tmp/{apk_path.name}" + push_ok, _ = self.run_adb_command(f'adb -d push "{apk_path}" {temp_apk_path}') + success = False + if push_ok: + success, _ = self.run_adb_shell(f'pm install -r -d "{temp_apk_path}"') + self.run_adb_shell(f'rm -f "{temp_apk_path}"') + if success: + success_count += 1 + + self.update_progress(total, total, self.t('progress_done'), is_push=True) + + if success_count == total: + self.log(self.tf('log_install_done_all', count=total), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('info_install_done'), self.tf('msg_install_success_many', count=total)) + elif success_count > 0: + self.log(self.tf('log_install_done_partial', success=success_count, total=total), "WARNING") + self.run_on_ui_thread(messagebox.showwarning, self.t('info_install_done'), self.tf('msg_install_partial', success=success_count, failed=total - success_count)) + else: + self.log(self.t('log_install_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('log_install_failed'), self.t('msg_install_all_failed')) + except Exception as e: + self.log(self.tf('log_install_exception', error=str(e)), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('log_install_failed'), self.tf('msg_install_exception', error=str(e))) + finally: + self.run_adb_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=self.t('dialog_select_apk'), + filetypes=[(self.t('file_apk'), "*.apk"), (self.t('file_all'), "*.*")] + ) + + if not file_path: + return + + def install(): + self.show_progress(True, is_push=True) + self.update_progress(50, 100, self.t('progress_installing'), is_push=True) + try: + self.run_adb_shell('setprop vecentek.model 1') + temp_apk_path = f"/data/local/tmp/{Path(file_path).name}" + push_ok, _ = self.run_adb_command(f'adb -d push "{file_path}" {temp_apk_path}') + success = False + if push_ok: + success, _ = self.run_adb_shell(f'pm install -r -d "{temp_apk_path}"') + self.run_adb_shell(f'rm -f "{temp_apk_path}"') + self.update_progress(100, 100, self.t('progress_done'), is_push=True) + if success: + self.log(self.t('info_install_done'), "SUCCESS") + else: + self.log(self.t('log_install_failed'), "ERROR") + except Exception as e: + self.log(self.tf('log_install_exception', error=str(e)), "ERROR") + finally: + self.run_adb_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_shell('am start -a android.settings.LOCALE_SETTINGS') + + def open_language_quick_set(self): + """打开快捷语言设置弹窗""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 创建弹窗 + popup = tk.Toplevel(self.root) + popup.title(self.t('title_pop_lang')) + popup.geometry("520x320") + popup.configure(bg=self.colors['bg_dark']) + popup.resizable(False, False) + + # 居中显示 + popup.update_idletasks() + x = self.root.winfo_x() + (self.root.winfo_width() - 520) // 2 + y = self.root.winfo_y() + (self.root.winfo_height() - 320) // 2 + popup.geometry(f"+{x}+{y}") + popup.transient(self.root) + popup.grab_set() + + # 标题 + header = tk.Label(popup, text=self.t('quick_lang_header'), + font=('Microsoft YaHei', 13, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + header.pack(pady=(15, 10)) + + hint = tk.Label(popup, text=self.t('quick_lang_hint'), + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + hint.pack(pady=(0, 12)) + + # 语言列表:(显示名, locale_code) + 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=self.t('quick_lang_system'), + command=lambda: self._open_sys_and_close(popup), + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + sys_btn.pack(pady=(0, 10)) + + def _quick_set_language(self, locale_code, language_name, popup): + """执行快捷语言设置""" + popup.destroy() + + def do_set(): + self.log(self.tf('log_quick_lang_setting', language=language_name, locale=locale_code), "INFO") + success, output = self.run_adb_shell(f'settings put system system_locales {locale_code}') + + if success: + self.log(self.tf('log_quick_lang_success', language=language_name), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('msg_success_title'), + self.tf('msg_quick_lang_success', language=language_name) + ) + else: + self.log(self.tf('log_quick_lang_failed', output=output), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.tf('msg_quick_lang_failed', output=output)) + + threading.Thread(target=do_set, daemon=True).start() + + def _open_sys_and_close(self, popup): + """关闭弹窗并打开系统语言设置""" + popup.destroy() + self.open_language_settings() + + def open_timezone_settings(self): + """打开时区设置""" + if not self.check_device_connection(): + return + self.run_adb_shell('am start -a android.settings.TIMEZONE_SETTINGS') + + def open_android_settings(self): + """打开安卓原生设置""" + if not self.check_device_connection(): + return + self.run_adb_shell('am start -a android.settings.SETTINGS') + + def reboot_device(self): + """重启设备""" + if not self.check_device_connection(): + return + if messagebox.askyesno(self.t('msg_reboot_confirm_title'), self.t('confirm_reboot')): + def do_restart_framework(): + self.log(self.t('log_rebooting'), "INFO") + root_ok, root_output = self.run_adb_password_command('adb -d root', timeout=30) + if not root_ok: + if self.debug_mode: + self.log(f"adb root failed: {root_output}", "ERROR") + self.log(self.t('log_reboot_failed'), "ERROR") + return + + time.sleep(2) + stop_ok, stop_output = self.run_adb_shell('stop') + start_ok, start_output = self.run_adb_shell('start') + if stop_ok and start_ok: + self.update_device_status(False) + else: + if self.debug_mode: + if not stop_ok: + self.log(f"adb shell stop failed: {stop_output}", "ERROR") + if not start_ok: + self.log(f"adb shell start failed: {start_output}", "ERROR") + self.log(self.t('log_reboot_failed'), "ERROR") + + threading.Thread(target=do_restart_framework, daemon=True).start() + + def on_disable_upgrade(self): + """禁用系统升级""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 弹窗确认 + result = messagebox.askyesno( + self.t('msg_disable_confirm_title'), + self.t('msg_disable_confirm') + ) + + if not result: + self.log(self.t('log_disable_cancelled'), "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(self.t('log_disable_success'), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('msg_success_title'), self.t('msg_disable_success')) + else: + self.log(self.t('log_disable_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.tf('msg_disable_failed', output=output)) + self.show_progress(False, is_push=False) + + threading.Thread(target=disable, daemon=True).start() + + def _on_vin_input_focus_in(self, event): + """输入框获得焦点时清除占位符""" + if self.is_placeholder_vin(self.vin_input.get()): + self.vin_input.delete(0, tk.END) + self.vin_input.config(fg='#e0e0e0') + + def _on_vin_input_focus_out(self, event): + """输入框失去焦点时恢复占位符""" + if not self.vin_input.get(): + self.vin_input.insert(0, self.t('vin_placeholder')) + self.vin_input.config(fg='#636e72') + + def query_password_by_vin(self): + """通过VIN查询工程密码""" + vin = self.vin_input.get().strip() + if not vin or self.is_placeholder_vin(vin): + messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_input_vin')) + return + + def do_query(): + try: + api_url = "https://api.changan.softwindy.cn/api/authorizations/generate-password-by-vin" + url = f"{api_url}?{urlencode({'vin': vin})}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + def update_ui(): + if data.get('success'): + pwd = data.get('data', {}).get('devicePassword', 'unknown') + self.pwd_result_label.config( + text=self.tf('pwd_success', password=pwd), + fg=self.colors['success'] + ) + self.log(self.t('log_pwd_success'), "SUCCESS") + else: + msg = data.get('message', 'failed') + self.pwd_result_label.config( + text=self.tf('pwd_failed', message=msg), + fg=self.colors['error'] + ) + self.log(self.tf('log_pwd_failed', message=msg), "ERROR") + + self.run_on_ui_thread(update_ui) + + except Exception as e: + def update_ui_error(): + self.pwd_result_label.config( + text=self.t('pwd_request_failed'), + fg=self.colors['error'] + ) + self.log(self.tf('log_pwd_request_failed', error=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(self.t('log_debug_off'), "WARNING") + self.status_text.config(text=self.t('status_ready')) + self.set_debug_buttons_visible(False) + self.refresh_device_status() + return + + pwd = simpledialog.askstring(self.t('status_debug'), self.t('debug_password_prompt'), show='*', parent=self.root) + if not pwd: + return + + self.log(self.t('debug_password_verifying'), "INFO") + + def verify(): + valid, message = self.verify_debug_mode_password(pwd) + if valid: + def enable_debug(): + self.debug_mode = True + self.update_device_status(True, "", True) + self.log(self.t('log_debug_on'), "WARNING") + self.status_text.config(text=self.t('status_debug')) + self.set_debug_buttons_visible(True) + self.run_on_ui_thread(enable_debug) + else: + def show_failed(): + msg = message or self.t('debug_wrong_password') + self.log(self.tf('debug_verify_failed', message=msg), "WARNING") + messagebox.showwarning(self.t('msg_error_title'), msg) + self.run_on_ui_thread(show_failed) + + threading.Thread(target=verify, daemon=True).start() + + def verify_debug_mode_password(self, password): + try: + payload = json.dumps({"password": password}).encode('utf-8') + req = Request( + self.debug_password_api_url, + data=payload, + method='POST', + headers={ + 'Content-Type': 'application/json', + 'User-Agent': 'Mozilla/5.0', + } + ) + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + if data.get('success') is True and data.get('valid') is True: + return True, data.get('message', '') + return False, data.get('message') or self.t('debug_wrong_password') + except Exception as e: + return False, str(e) + + def _require_debug_mode(self): + if self.debug_mode: + return True + messagebox.showwarning(self.t('status_debug'), self.t('debug_need_enable')) + return False + + def install_apps(self): + """安装App — 支持单选或多选APK文件""" + if not self.check_device_connection(): + return + if not self.vin and not self.debug_mode: + messagebox.showwarning(self.t('msg_warn_title'), self.t('warn_no_vin')) + return + + file_paths = filedialog.askopenfilenames( + title=self.t('dialog_select_apk'), + filetypes=[(self.t('file_apk'), "*.apk"), (self.t('file_all'), "*.*")] + ) + if not file_paths: + return + + count = len(file_paths) + result = messagebox.askyesno( + self.t('msg_confirm_install_title'), + self.tf('msg_confirm_install_many', count=count) + ) + if not result: + return + + def install(): + if not self.check_authorization(self.vin): + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_auth_failed_title'), + self.t('msg_device_unauthorized') + ) + return + + self.show_progress(True, is_push=True) + self.log(self.tf('log_install_start', count=count), "INFO") + success_count = 0 + try: + self.run_adb_shell('setprop vecentek.model 1') + + for i, file_path in enumerate(file_paths, 1): + apk_name = Path(file_path).name + self.update_progress(i, count, self.tf('progress_installing_name', name=apk_name), is_push=True) + success, _ = self.install_apk_file_with_current_model(file_path) + if success: + self.log(self.tf('log_install_success_item', name=apk_name), "SUCCESS") + success_count += 1 + else: + self.log(self.tf('log_install_failed_item', name=apk_name), "ERROR") + + self.update_progress(count, count, self.t('progress_done'), is_push=True) + + if success_count == count: + self.log(self.tf('log_install_done_all', count=count), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('info_install_done'), self.tf('msg_install_success_many', count=count)) + elif success_count > 0: + self.log(self.tf('log_install_done_partial', success=success_count, total=count), "WARNING") + self.run_on_ui_thread(messagebox.showwarning, self.t('info_install_done'), self.tf('msg_install_partial', success=success_count, failed=count - success_count)) + else: + self.log(self.t('log_install_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('log_install_failed'), self.t('msg_install_all_failed')) + except Exception as e: + self.log(self.tf('log_install_exception', error=str(e)), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('log_install_failed'), self.tf('msg_install_exception', error=str(e))) + finally: + self.run_adb_shell('setprop vecentek.model 0') + self.show_progress(False, is_push=True) + + threading.Thread(target=install, daemon=True).start() + + def _debug_test_extract(self, event=None): + self.debug_test_package_extract() + + def debug_test_package_extract(self): + """Debug-only package extraction test using package-key.""" + if not self._require_debug_mode(): + return + + if not self.vin: + vin = simpledialog.askstring(self.t('status_debug'), 'VIN:', parent=self.root) + if vin: + self.vin = vin.strip().upper() + if not self.vin: + messagebox.showwarning(self.t('status_debug'), self.t('debug_need_vin')) + return + + def do_extract(): + old_password = self.extract_password + try: + self.log(self.t('info_extracting'), "INFO") + self.extract_password = None + if not self.fetch_package_password(): + self.log(self.t('log_package_key_failed'), "ERROR") + return + self.show_progress(True, is_push=False) + if self.extract_package_silent(): + self.log(self.t('log_package_extract_success'), "SUCCESS") + else: + self.log(self.t('log_package_extract_failed'), "ERROR") + finally: + self.extract_password = old_password + self.show_progress(False, is_push=False) + + threading.Thread(target=do_extract, daemon=True).start() + + def run(self): + """运行程序""" + self.root.mainloop() + +def main(): + """主函数""" + if sys.version_info < (3, 6): + print("错误:需要Python 3.6或更高版本") + sys.exit(1) + + try: + app = ADKAPKGUI() + app.run() + except Exception as e: + print(f"启动失败: {e}") + import traceback + traceback.print_exc() + messagebox.showerror("错误", f"程序启动失败: {e}") + +if __name__ == "__main__": + main() + diff --git a/Mazda-EZ60/app.ico b/Mazda-EZ60/app.ico new file mode 100644 index 0000000..4f8e684 Binary files /dev/null and b/Mazda-EZ60/app.ico differ diff --git a/Mazda-EZ60/manual_push_apps_to_system_app.ps1 b/Mazda-EZ60/manual_push_apps_to_system_app.ps1 new file mode 100644 index 0000000..2ee37d9 --- /dev/null +++ b/Mazda-EZ60/manual_push_apps_to_system_app.ps1 @@ -0,0 +1,209 @@ +param( + [string]$AppsDir = "", + [string]$AdbPath = "", + [string]$ShellPassword = "adb36987" +) + +$ErrorActionPreference = "Stop" + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +if (-not $AppsDir) { + $AppsDir = Join-Path $ScriptDir "apps" +} + +function Find-AdbPath { + param( + [string]$BaseDir + ) + + $candidates = @( + (Join-Path $BaseDir "adb.exe"), + (Join-Path $BaseDir "tools\adb.exe"), + (Join-Path $BaseDir "..\adb.exe"), + (Join-Path $BaseDir "..\tools\adb.exe"), + (Join-Path $BaseDir "..\..\tools\adb.exe"), + "D:\Code\language-installer\tools\adb.exe", + "D:\apk_tools\platform-tools\adb.exe" + ) + + foreach ($candidate in $candidates) { + try { + $resolved = Resolve-Path -LiteralPath $candidate -ErrorAction Stop + if ($resolved) { + return $resolved.Path + } + } catch {} + } + + $pathAdb = Get-Command adb.exe -ErrorAction SilentlyContinue + if ($pathAdb) { + return $pathAdb.Source + } + + return "" +} + +if (-not $AdbPath) { + $AdbPath = Find-AdbPath -BaseDir $ScriptDir +} + +if (-not (Test-Path -LiteralPath $AdbPath)) { + throw "adb.exe not found. Put adb.exe next to this script or run with -AdbPath `"D:\path\adb.exe`"" +} +if (-not (Test-Path -LiteralPath $AppsDir)) { + throw "apps directory not found: $AppsDir" +} + +function Quote-ProcessArgument { + param( + [Parameter(Mandatory = $true)] + [string]$Value + ) + + if ($Value -notmatch '[\s"]') { + return $Value + } + + return '"' + $Value.Replace('"', '\"') + '"' +} + +function Invoke-Adb { + param( + [Parameter(Mandatory = $true)] + [string[]]$Arguments + ) + + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $AdbPath + $psi.Arguments = (($Arguments | ForEach-Object { Quote-ProcessArgument $_ }) -join " ") + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + + $proc = [System.Diagnostics.Process]::Start($psi) + $stdout = $proc.StandardOutput.ReadToEnd() + $stderr = $proc.StandardError.ReadToEnd() + $proc.WaitForExit() + + return [pscustomobject]@{ + Code = $proc.ExitCode + Out = ($stdout + $stderr).Trim() + } +} + +function Invoke-AdbShellWithPassword { + param( + [Parameter(Mandatory = $true)] + [string]$Command, + [int]$TimeoutMs = 60000 + ) + + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $AdbPath + $psi.Arguments = "-d shell" + $psi.RedirectStandardInput = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + + $proc = New-Object System.Diagnostics.Process + $proc.StartInfo = $psi + [void]$proc.Start() + + Start-Sleep -Milliseconds 500 + $proc.StandardInput.WriteLine($ShellPassword) + Start-Sleep -Milliseconds 300 + $proc.StandardInput.WriteLine($Command) + $proc.StandardInput.WriteLine("exit") + $proc.StandardInput.Close() + + if (-not $proc.WaitForExit($TimeoutMs)) { + try { $proc.Kill() } catch {} + return [pscustomobject]@{ + Code = -1 + Out = "shell command timeout: $Command" + } + } + + $stdout = $proc.StandardOutput.ReadToEnd() + $stderr = $proc.StandardError.ReadToEnd() + return [pscustomobject]@{ + Code = $proc.ExitCode + Out = ($stdout + $stderr).Trim() + } +} + +Write-Host "ADB: $AdbPath" +Write-Host "Apps: $AppsDir" + +$devices = Invoke-Adb -Arguments @("-d", "devices") +if ($devices.Code -ne 0 -or $devices.Out -notmatch "`tdevice") { + Write-Host "[ERROR] No adb device connected." -ForegroundColor Red + if ($devices.Out) { Write-Host $devices.Out } + exit 1 +} + +$root = Invoke-Adb -Arguments @("-d", "root") +if ($root.Out) { + Write-Host $root.Out +} +Start-Sleep -Seconds 2 + +$apks = Get-ChildItem -LiteralPath $AppsDir -Filter "*.apk" -File | Sort-Object Name +if (-not $apks) { + Write-Host "[ERROR] No APK files found in apps directory." -ForegroundColor Red + exit 1 +} + +$total = $apks.Count +$index = 0 +$success = 0 + +foreach ($apk in $apks) { + $index++ + $apkName = [System.IO.Path]::GetFileNameWithoutExtension($apk.Name) + $targetDir = "/system/app/$apkName" + $targetApk = "$targetDir/$($apk.Name)" + + Write-Host "" + Write-Host "[$index/$total] $($apk.Name)" -ForegroundColor Cyan + + $mkdir = Invoke-AdbShellWithPassword -Command "mkdir -p '$targetDir'" + if ($mkdir.Code -ne 0) { + Write-Host "[ERROR] mkdir failed: $targetDir" -ForegroundColor Red + if ($mkdir.Out) { Write-Host $mkdir.Out } + exit 1 + } + + $push = Invoke-Adb -Arguments @("-d", "push", $apk.FullName, $targetApk) + if ($push.Code -ne 0) { + Write-Host "[ERROR] push failed: $($apk.Name)" -ForegroundColor Red + if ($push.Out) { Write-Host $push.Out } + exit 1 + } + if ($push.Out) { + Write-Host $push.Out + } + + $chmodDir = Invoke-AdbShellWithPassword -Command "chmod 755 '$targetDir'" + if ($chmodDir.Code -ne 0) { + Write-Host "[ERROR] chmod 755 failed: $targetDir" -ForegroundColor Red + if ($chmodDir.Out) { Write-Host $chmodDir.Out } + exit 1 + } + + $chmodApk = Invoke-AdbShellWithPassword -Command "chmod 644 '$targetApk'" + if ($chmodApk.Code -ne 0) { + Write-Host "[ERROR] chmod 644 failed: $targetApk" -ForegroundColor Red + if ($chmodApk.Out) { Write-Host $chmodApk.Out } + exit 1 + } + + $success++ + Write-Host "[OK] $($apk.Name)" -ForegroundColor Green +} + +Write-Host "" +Write-Host "[DONE] Pushed $success/$total APK files." -ForegroundColor Green diff --git a/Mazda-EZ60/pack_mazda_ez60.bat b/Mazda-EZ60/pack_mazda_ez60.bat deleted file mode 100644 index 61f2663..0000000 --- a/Mazda-EZ60/pack_mazda_ez60.bat +++ /dev/null @@ -1,100 +0,0 @@ -@echo off -chcp 65001 >nul -cd /d "%~dp0" -set "ROOT=%~dp0.." -set "TOOLS=%ROOT%\tools" -set "NAME=Mazda-EZ60-Language-Installer" -set "SRC=Mazda-EZ60.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 -if errorlevel 1 ( - echo [WARN] Copy source failed, fallback - goto :NORMAL -) - -"%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 "%TOOLS%\adb.exe" . >nul -copy "%TOOLS%\AdbWinApi.dll" . >nul -copy "%TOOLS%\AdbWinUsbApi.dll" . >nul -copy "%TOOLS%\7za.exe" . >nul -if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul - -echo [5/6] PyInstaller... -"%PY%" -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 -echo [INFO] Normal PyInstaller... -"%PY%" -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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/Mazda-EZ60/pack_mazda_ez60_1.0.bat b/Mazda-EZ60/pack_mazda_ez60_1.0.bat new file mode 100644 index 0000000..20f72df --- /dev/null +++ b/Mazda-EZ60/pack_mazda_ez60_1.0.bat @@ -0,0 +1,159 @@ +@echo off +chcp 65001 >nul +cd /d "%~dp0" +set "ROOT=%~dp0.." +set "TOOLS=%ROOT%\tools" +set "NAME=Mazda_EZ60-Language-Install_v1.0" +set "SRC=Mazda_EZ60-Language-Install_v1.0.py" +set "ICON=%~dp0app.ico" +title %NAME% - Cython Build + +echo ============================================================ +echo %NAME% - Cython Build +echo ============================================================ +echo. + +where python >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Python not found + pause + exit /b 1 +) +for /f "delims=" %%i in ('where python') do set "PY=%%i" +echo Python: %PY% + +if not exist "%SRC%" ( + echo [ERROR] Source not found: %SRC% + pause + exit /b 1 +) +if not exist "%ICON%" ( + echo [ERROR] Icon not found: %ICON% + pause + exit /b 1 +) +if not exist "%TOOLS%\adb.exe" ( + echo [ERROR] adb.exe not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\AdbWinApi.dll" ( + echo [ERROR] AdbWinApi.dll not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\AdbWinUsbApi.dll" ( + echo [ERROR] AdbWinUsbApi.dll not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\7za.exe" ( + echo [ERROR] 7za.exe not found in %TOOLS% + pause + exit /b 1 +) + +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 +) +if errorlevel 1 ( + echo [ERROR] Dependency install failed + pause + exit /b 1 +) + +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 +if exist "%NAME%.spec" del /q "%NAME%.spec" 2>nul + +echo [3/6] Cython compile... +mkdir dist_cy 2>nul +copy "%SRC%" dist_cy\_core.py >nul +if errorlevel 1 ( + echo [ERROR] Copy source failed + pause + exit /b 1 +) + +"%PY%" -c "open('dist_cy/setup_cython.py','w',encoding='utf-8').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')" +if errorlevel 1 ( + echo [ERROR] Failed to create Cython setup script + pause + exit /b 1 +) + +cd dist_cy +"%PY%" setup_cython.py build_ext --inplace +if errorlevel 1 ( + cd .. + echo [ERROR] Cython failed. Build stopped. + pause + exit /b 1 +) + +set "PYD=" +for %%f in (_core*.pyd) do set "PYD=%%f" +if "%PYD%"=="" ( + cd .. + echo [ERROR] No Cython PYD generated. Build stopped. + pause + exit /b 1 +) +echo PYD: %PYD% +copy "%PYD%" _core.pyd >nul +if errorlevel 1 ( + cd .. + echo [ERROR] Failed to copy Cython PYD + pause + exit /b 1 +) + +"%PY%" -c "open('launcher.py','w',encoding='utf-8').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')" +if errorlevel 1 ( + cd .. + echo [ERROR] Failed to create launcher.py + pause + exit /b 1 +) + +echo [4/6] Copy resources... +copy "%TOOLS%\adb.exe" . >nul +copy "%TOOLS%\AdbWinApi.dll" . >nul +copy "%TOOLS%\AdbWinUsbApi.dll" . >nul +copy "%TOOLS%\7za.exe" . >nul +copy "%ICON%" . >nul +if errorlevel 1 ( + cd .. + echo [ERROR] Copy resources failed + pause + exit /b 1 +) + +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-data "app.ico;." --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 1 +) + +echo [6/6] Cleanup... +del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py app.ico 2>nul +rmdir /s /q build 2>nul +cd .. + +echo. +echo Done. +if exist "dist_cy\dist\%NAME%.exe" ( + echo Output: dist_cy\dist\%NAME%.exe +) else ( + echo [ERROR] Output exe was not generated + pause + exit /b 1 +) +pause diff --git a/Mazda-EZ60/pack_mazda_ez60_1.0_direct_push.bat b/Mazda-EZ60/pack_mazda_ez60_1.0_direct_push.bat new file mode 100644 index 0000000..5823a3f --- /dev/null +++ b/Mazda-EZ60/pack_mazda_ez60_1.0_direct_push.bat @@ -0,0 +1,159 @@ +@echo off +chcp 65001 >nul +cd /d "%~dp0" +set "ROOT=%~dp0.." +set "TOOLS=%ROOT%\tools" +set "NAME=Mazda_EZ60-Language-Install_v1.0-direct-push" +set "SRC=Mazda_EZ60-Language-Install_v1.0_direct_push.py" +set "ICON=%~dp0app.ico" +title %NAME% - Cython Build + +echo ============================================================ +echo %NAME% - Cython Build +echo ============================================================ +echo. + +where python >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Python not found + pause + exit /b 1 +) +for /f "delims=" %%i in ('where python') do set "PY=%%i" +echo Python: %PY% + +if not exist "%SRC%" ( + echo [ERROR] Source not found: %SRC% + pause + exit /b 1 +) +if not exist "%ICON%" ( + echo [ERROR] Icon not found: %ICON% + pause + exit /b 1 +) +if not exist "%TOOLS%\adb.exe" ( + echo [ERROR] adb.exe not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\AdbWinApi.dll" ( + echo [ERROR] AdbWinApi.dll not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\AdbWinUsbApi.dll" ( + echo [ERROR] AdbWinUsbApi.dll not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\7za.exe" ( + echo [ERROR] 7za.exe not found in %TOOLS% + pause + exit /b 1 +) + +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 +) +if errorlevel 1 ( + echo [ERROR] Dependency install failed + pause + exit /b 1 +) + +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 +if exist "%NAME%.spec" del /q "%NAME%.spec" 2>nul + +echo [3/6] Cython compile... +mkdir dist_cy 2>nul +copy "%SRC%" dist_cy\_core.py >nul +if errorlevel 1 ( + echo [ERROR] Copy source failed + pause + exit /b 1 +) + +"%PY%" -c "open('dist_cy/setup_cython.py','w',encoding='utf-8').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')" +if errorlevel 1 ( + echo [ERROR] Failed to create Cython setup script + pause + exit /b 1 +) + +cd dist_cy +"%PY%" setup_cython.py build_ext --inplace +if errorlevel 1 ( + cd .. + echo [ERROR] Cython failed. Build stopped. + pause + exit /b 1 +) + +set "PYD=" +for %%f in (_core*.pyd) do set "PYD=%%f" +if "%PYD%"=="" ( + cd .. + echo [ERROR] No Cython PYD generated. Build stopped. + pause + exit /b 1 +) +echo PYD: %PYD% +copy "%PYD%" _core.pyd >nul +if errorlevel 1 ( + cd .. + echo [ERROR] Failed to copy Cython PYD + pause + exit /b 1 +) + +"%PY%" -c "open('launcher.py','w',encoding='utf-8').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')" +if errorlevel 1 ( + cd .. + echo [ERROR] Failed to create launcher.py + pause + exit /b 1 +) + +echo [4/6] Copy resources... +copy "%TOOLS%\adb.exe" . >nul +copy "%TOOLS%\AdbWinApi.dll" . >nul +copy "%TOOLS%\AdbWinUsbApi.dll" . >nul +copy "%TOOLS%\7za.exe" . >nul +copy "%ICON%" . >nul +if errorlevel 1 ( + cd .. + echo [ERROR] Copy resources failed + pause + exit /b 1 +) + +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-data "app.ico;." --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 1 +) + +echo [6/6] Cleanup... +del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py app.ico 2>nul +rmdir /s /q build 2>nul +cd .. + +echo. +echo Done. +if exist "dist_cy\dist\%NAME%.exe" ( + echo Output: dist_cy\dist\%NAME%.exe +) else ( + echo [ERROR] Output exe was not generated + pause + exit /b 1 +) +pause diff --git a/Mazda-EZ60/pack_mazda_ez60_1.2.bat b/Mazda-EZ60/pack_mazda_ez60_1.2.bat new file mode 100644 index 0000000..a0fe4f4 --- /dev/null +++ b/Mazda-EZ60/pack_mazda_ez60_1.2.bat @@ -0,0 +1,194 @@ +@echo off +chcp 65001 >nul +cd /d "%~dp0" +set "ROOT=%~dp0.." +set "TOOLS=%ROOT%\tools" +set "NAME=Mazda-EZ60-Language-Installer_v1.2" +set "SRC=Mazda-EZ60_1.2.py" +set "ICON=%~dp0app.ico" +title %NAME% - Cython Build + +echo ============================================================ +echo %NAME% - Cython Build +echo ============================================================ +echo. + +where python >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Python not found + pause + exit /b 1 +) +for /f "delims=" %%i in ('where python') do set "PY=%%i" +echo Python: %PY% + +if not exist "%SRC%" ( + echo [ERROR] Source not found: %SRC% + pause + exit /b 1 +) +if not exist "%ICON%" ( + echo [ERROR] Icon not found: %ICON% + pause + exit /b 1 +) +if not exist "%TOOLS%\adb.exe" ( + echo [ERROR] adb.exe not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\AdbWinApi.dll" ( + echo [ERROR] AdbWinApi.dll not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\AdbWinUsbApi.dll" ( + echo [ERROR] AdbWinUsbApi.dll not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\7za.exe" ( + echo [ERROR] 7za.exe not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\fastboot.exe" ( + echo [ERROR] fastboot.exe not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\usb_driver\android_winusb.inf" ( + echo [ERROR] usb_driver not found at %TOOLS%\usb_driver + pause + exit /b 1 +) +if not exist "%~dp0runtime.dat" ( + echo [ERROR] runtime.dat not found at %~dp0runtime.dat + pause + exit /b 1 +) + +echo [1/6] Installing deps... +"%PY%" -m pip install pyinstaller cython pyzipper cryptography -q +if errorlevel 1 ( + "%PY%" -m pip install pyinstaller cython pyzipper cryptography -q -i https://pypi.tuna.tsinghua.edu.cn/simple +) +if errorlevel 1 ( + echo [ERROR] Dependency install failed + pause + exit /b 1 +) + +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 +if exist "%NAME%.spec" del /q "%NAME%.spec" 2>nul + +echo [3/6] Cython compile... +mkdir dist_cy 2>nul +copy "%SRC%" dist_cy\_core.py >nul +if errorlevel 1 ( + echo [ERROR] Copy source failed + pause + exit /b 1 +) + +"%PY%" -c "open('dist_cy/setup_cython.py','w',encoding='utf-8').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')" +if errorlevel 1 ( + echo [ERROR] Failed to create Cython setup script + pause + exit /b 1 +) + +cd dist_cy +"%PY%" setup_cython.py build_ext --inplace +if errorlevel 1 ( + cd .. + echo [ERROR] Cython failed. Build stopped. + pause + exit /b 1 +) + +set "PYD=" +for %%f in (_core*.pyd) do set "PYD=%%f" +if "%PYD%"=="" ( + cd .. + echo [ERROR] No Cython PYD generated. Build stopped. + pause + exit /b 1 +) +echo PYD: %PYD% +copy "%PYD%" _core.pyd >nul +if errorlevel 1 ( + cd .. + echo [ERROR] Failed to copy Cython PYD + pause + exit /b 1 +) + +"%PY%" -c "open('launcher.py','w',encoding='utf-8').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')" +if errorlevel 1 ( + cd .. + echo [ERROR] Failed to create launcher.py + pause + exit /b 1 +) + +echo [4/6] Copy resources... +copy "%TOOLS%\adb.exe" . >nul +copy "%TOOLS%\AdbWinApi.dll" . >nul +copy "%TOOLS%\AdbWinUsbApi.dll" . >nul +copy "%TOOLS%\7za.exe" . >nul +copy "%TOOLS%\fastboot.exe" . >nul +copy "%ICON%" . >nul +if errorlevel 1 ( + cd .. + echo [ERROR] Copy resources failed + pause + exit /b 1 +) +if exist "%~dp0EZ60_resource.dat" copy "%~dp0EZ60_resource.dat" . >nul + +echo [5/6] PyInstaller... +set ADD_PERMISSION_RESOURCE= +if exist "EZ60_resource.dat" set ADD_PERMISSION_RESOURCE=--add-data "EZ60_resource.dat;." +"%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-data "fastboot.exe;." --add-data "app.ico;." --add-data "%TOOLS%\usb_driver;usb_driver" %ADD_PERMISSION_RESOURCE% --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 --hidden-import=cryptography --collect-all tkinter --collect-all cryptography --uac-admin launcher.py +if errorlevel 1 ( + cd .. + echo [ERROR] PyInstaller failed + pause + exit /b 1 +) + +echo [6/6] Cleanup... +del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py app.ico fastboot.exe EZ60_resource.dat 2>nul +rmdir /s /q build 2>nul +cd .. + +if not exist "dist_cy\dist\%NAME%.exe" ( + echo [ERROR] Output exe was not generated + pause + exit /b 1 +) + +copy "runtime.dat" "dist_cy\dist\runtime.dat" >nul +if errorlevel 1 ( + echo [ERROR] Copy runtime.dat failed + pause + exit /b 1 +) + +if exist "package_voice-assistant.bin" ( + copy "package_voice-assistant.bin" "dist_cy\dist\package_voice-assistant.bin" >nul + if errorlevel 1 ( + echo [ERROR] Copy package_voice-assistant.bin failed + pause + exit /b 1 + ) +) + +echo. +echo Done. +echo Output: dist_cy\dist\%NAME%.exe +pause diff --git a/Q05-Lidar/Q05-Lidar_Installer.py b/Q05-Lidar/Q05-Lidar_Installer.py index 00e1cf0..c6ed3d2 100644 --- a/Q05-Lidar/Q05-Lidar_Installer.py +++ b/Q05-Lidar/Q05-Lidar_Installer.py @@ -66,17 +66,61 @@ def find_resource(file_name): return candidates[0] +def resource_dir_candidates(dir_name): + base_dir = get_app_dir() + candidates = [] + if getattr(sys, 'frozen', False): + candidates.append(Path(getattr(sys, '_MEIPASS', base_dir)) / dir_name) + candidates.extend([ + base_dir / dir_name, + base_dir / 'tools' / dir_name, + base_dir / 'shared' / dir_name, + base_dir.parent / dir_name, + base_dir.parent / 'tools' / dir_name, + base_dir.parent / 'shared' / dir_name, + ]) + unique = [] + for candidate in candidates: + if candidate not in unique: + unique.append(candidate) + return unique + + +def find_resource_dir(dir_name): + candidates = resource_dir_candidates(dir_name) + for candidate in candidates: + if candidate.exists() and candidate.is_dir(): + return candidate + return candidates[0] + + def find_tool(file_name, fallback=None): path = find_resource(file_name) if path.exists(): return str(path) return fallback or str(path) + + +def set_windows_app_id(): + if sys.platform != 'win32': + return + try: + import ctypes + app_id = 'YibinKeyi.Q05Lidar.Installer.1.0' + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id) + except Exception: + pass + + class ADKAPKGUI: def __init__(self): + set_windows_app_id() self.root = tk.Tk() self.root.title("启源Q05Ultra+激光雷达") self.root.geometry("900x660") self.root.resizable(True, True) + self.set_window_icon() + self.root.after(200, self.set_window_icon) self.default_window_size = (900, 660) self.right_panel_width = 280 @@ -129,6 +173,7 @@ class ADKAPKGUI: 'auth_yes': '已授权', 'auth_no': '未授权', 'btn_refresh': '🔄 检查', + 'btn_install_driver': '🧩 安装驱动', 'hint_factory': '🔧 关闭车辆WI-FI和4G网络,拨号获取的密码进入工程模式', 'hotspot_title': '📶 电脑热点', 'hotspot_start': '🔧 打开热点设置', @@ -163,11 +208,21 @@ class ADKAPKGUI: 'msg_permission_failed': '获取失败', 'msg_confirm_permission_title': '确认获取权限', 'msg_confirm_permission': '此操作将为当前设备获取权限。\n过程中请不要断电或拔线。\n\n是否继续?', + 'msg_driver_missing_title': '驱动环境', + 'msg_driver_missing': '驱动缺失,即将自动安装驱动。', + 'msg_driver_install_confirm': '即将安装驱动环境,需要管理员权限。', + 'msg_driver_install_done': '驱动安装完成。如设备仍无法识别,请重新插拔 USB 线缆。', + 'msg_driver_install_failed': '驱动安装失败: {error}', 'status_debug': '🔧 调试模式', 'log_cleared': '日志已清空', 'log_lang_changed': '语言已切换为中文', 'log_debug_on': '🔧 调试模式已开启', 'log_debug_off': '调试模式已关闭', + 'log_driver_found': '已检测到驱动环境', + 'log_driver_missing': '未检测到驱动环境', + 'log_driver_install_start': '正在安装驱动环境...', + 'log_driver_install_success': '驱动环境安装完成', + 'log_driver_install_failed': '驱动环境安装失败: {error}', 'log_permission_success': '获取成功', 'log_push_success': '语言包刷入完成,重启设备后生效', 'log_push_failed': '语言包刷入失败', @@ -193,7 +248,7 @@ class ADKAPKGUI: 'progress_flashing': '正在刷入', 'progress_flash_done': '刷入完成', 'progress_extracting_percent': '资源准备中 {percent}%', - 'progress_install_module': '安装模块 {module}', + 'progress_install_module': '安装资源', 'progress_push_files': '推送文件 {current}/{total}', 'quick_lang_title': '快捷语言设置', 'quick_lang_header': '选择目标语言', @@ -206,7 +261,7 @@ class ADKAPKGUI: 'reboot_confirm_title': '确认重启', 'reboot_confirm': '确定要重启设备吗?', 'disable_confirm_title': '确认禁用升级', - 'disable_confirm': '⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n是否确定要禁用系统升级应用?\n\n禁用命令:\nadb shell pm disable-user --user 0 com.incall.apps.softmanager', + 'disable_confirm': '⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n是否确定要禁用系统升级应用?', 'disable_success_title': '成功', 'disable_success': '系统升级已成功禁用!', 'disable_failed': '禁用失败:{output}', @@ -227,7 +282,7 @@ class ADKAPKGUI: 'debug_need_vin': '请先刷新设备VIN,或在工程密码输入框填入VIN', 'log_decrypt_success': '解密测试成功: size={size}, sha256={sha}', 'log_package_extract_success': 'Q05_Lidar-package.bin 解压测试成功: APK数量={count}', - 'log_package_extract_modules_success': 'Q05_Lidar-package.bin 解压测试成功: 主模块={main}, 附加模块={extra}', + 'log_package_extract_modules_success': 'Q05_Lidar-package.bin 解压测试成功: 资源数量={count}', 'hotspot_stopped': '未启动', 'log_device_disconnected': '设备已断开连接', 'log_cache_invalid': '已解压缓存无效: {reason}', @@ -248,12 +303,11 @@ class ADKAPKGUI: 'log_apps_dir_missing': '警告:未找到 apps 目录', 'log_extracted_resource_invalid': '解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。', 'log_resource_ready': '资源准备完成 (app: {count})', - 'log_module_resource_ready': '资源准备完成: 主模块={main}, 附加模块={extra}', - 'err_module_prop_missing': '资源包缺少 module.prop', - 'err_system_dir_missing': '资源包缺少 system 目录', - 'err_disable_module_missing': '资源包缺少 disable-wireless-adb-vecentek-magisk.zip', - 'err_module_id_missing': 'module.prop 缺少 id 字段', - 'err_disable_module_invalid': '附加模块压缩包无效或缺少 module.prop', + 'log_module_resource_ready': '资源准备完成', + 'err_module_prop_missing': '资源包缺少资源配置', + 'err_module_id_missing': '资源配置缺少 ID 字段', + 'err_module_zip_invalid': '资源文件无效或缺少配置: {file}', + 'err_duplicate_module_id': '资源包存在重复资源 ID', 'log_resource_prepare_failed_detail': '资源准备失败: {error}', 'log_resource_prepare_network': '资源准备失败,请检查网络连接后重试', 'log_package_file_missing': '未找到资源包文件', @@ -296,7 +350,7 @@ class ADKAPKGUI: 'log_runtime_install_failed': '获取失败', 'log_runtime_install_success': '正在获取权限中', 'log_fastboot_enter_failed': '获取失败', - 'log_fastboot_wait': '正在获取权限中', + 'log_fastboot_wait': '正在获取权限,请不要关闭程序或断开数据连接!', 'log_fastboot_missing': '获取失败', 'log_init_boot_flash_failed': '获取失败', 'log_init_boot_flash_success': '正在获取权限中', @@ -305,16 +359,16 @@ class ADKAPKGUI: 'log_install_failed_detail': 'install失败: {error}', 'log_prepare_resource': '正在准备资源...', 'log_flash_start': '开始刷入语言包...', - 'log_open_magisk': '正在打开 Magisk App,请在弹窗中授予 Shell/root 权限', - 'log_root_checking': '正在检测 ADB Shell root 权限...', - 'log_root_ok': 'ADB Shell root 权限已授予', - 'log_root_failed': '未获得 ADB Shell root 权限,请手动在 Magisk App 授予 Shell 权限', - 'msg_root_failed': '请手动在 Magisk App 授予 Shell 权限后重试。', - 'log_module_install_start': '开始安装 Magisk 模块: {module}', - 'log_module_install_done': 'Magisk 模块安装完成: {module}', - 'log_module_install_failed': 'Magisk 模块安装失败: {module}: {error}', + 'log_open_magisk': '请在弹窗中点击“允许”授予权限', + 'log_root_checking': '正在检测权限...', + 'log_root_ok': '权限已授予', + 'log_root_failed': '权限获取失败,请手动授予权限', + 'msg_root_failed': '请手动授予权限后重试。', + 'log_module_install_start': '开始安装资源', + 'log_module_install_done': '资源安装完成', + 'log_module_install_failed': '资源安装失败: {error}', 'log_push_file_failed': '文件推送失败: {file}: {error}', - 'log_root_cmd_failed': 'Root 命令执行失败: {command}: {error}', + 'log_root_cmd_failed': '权限操作失败: {error}', 'log_no_lang_pkg': '未找到语言包文件', 'log_install_item_ok': '安装成功: {name}.apk', 'log_install_item_failed': '安装失败: {name}.apk', @@ -386,6 +440,7 @@ class ADKAPKGUI: 'auth_yes': 'Authorized', 'auth_no': 'Unauthorized', 'btn_refresh': '🔄 Check', + 'btn_install_driver': '🧩 Driver', 'hint_factory': '🔧 Turn off the vehicle WiFi & 4G, enter factory mode with dial code', 'hotspot_title': '📶 Hotspot', 'hotspot_start': '🔧 Open Hotspot Settings', @@ -422,11 +477,21 @@ class ADKAPKGUI: 'msg_permission_failed': 'Permission failed', 'msg_confirm_permission_title': 'Confirm Unlock', 'msg_confirm_permission': 'This will grant permission for the current device.\nDo not disconnect power or USB during the process.\n\nContinue?', + 'msg_driver_missing_title': 'Driver Environment', + 'msg_driver_missing': 'Driver environment is missing. Driver installation will start automatically.', + 'msg_driver_install_confirm': 'Driver environment installation requires administrator permission.', + 'msg_driver_install_done': 'Driver installation completed. If the device is still not recognized, reconnect USB cable.', + 'msg_driver_install_failed': 'Driver installation failed: {error}', 'status_debug': '🔧 Debug mode', 'log_cleared': 'Log cleared', 'log_lang_changed': 'Language switched to English', 'log_debug_on': '🔧 Debug mode enabled', 'log_debug_off': 'Debug mode disabled', + 'log_driver_found': 'Driver environment detected', + 'log_driver_missing': 'Driver environment not detected', + 'log_driver_install_start': 'Installing driver environment...', + 'log_driver_install_success': 'Driver environment installed', + 'log_driver_install_failed': 'Driver environment installation failed: {error}', 'log_permission_success': 'Permission granted', 'log_push_success': 'Language package flashed. Reboot to apply changes.', 'log_push_failed': 'Language package flash failed', @@ -452,7 +517,7 @@ class ADKAPKGUI: 'progress_flashing': 'Flashing', 'progress_flash_done': 'Flash complete', 'progress_extracting_percent': 'Preparing resources {percent}%', - 'progress_install_module': 'Installing module {module}', + 'progress_install_module': 'Installing resources', 'progress_push_files': 'Pushing files {current}/{total}', 'quick_lang_title': 'Quick Language', 'quick_lang_header': 'Choose target language', @@ -465,7 +530,7 @@ class ADKAPKGUI: 'reboot_confirm_title': 'Confirm Reboot', 'reboot_confirm': 'Reboot the device now?', 'disable_confirm_title': 'Confirm Disable OTA', - 'disable_confirm': '⚠️ Warning: disabling system upgrade prevents system updates.\n\nDisable the system upgrade app?\n\nCommand:\nadb shell pm disable-user --user 0 com.incall.apps.softmanager', + 'disable_confirm': '⚠️ Warning: disabling system upgrade prevents system updates.\n\nDisable the system upgrade app?', 'disable_success_title': 'Success', 'disable_success': 'System upgrade app disabled.', 'disable_failed': 'Disable failed: {output}', @@ -486,7 +551,7 @@ class ADKAPKGUI: 'debug_need_vin': 'Refresh device VIN first, or enter a VIN in the password query field.', 'log_decrypt_success': 'Decrypt test passed: size={size}, sha256={sha}', 'log_package_extract_success': 'Q05_Lidar-package.bin extract test passed: APK count={count}', - 'log_package_extract_modules_success': 'Q05_Lidar-package.bin extract test passed: main={main}, extra={extra}', + 'log_package_extract_modules_success': 'Q05_Lidar-package.bin extract test passed: resource count={count}', 'hotspot_stopped': 'stopped', 'log_device_disconnected': 'Device disconnected', 'log_cache_invalid': 'Extract cache invalid: {reason}', @@ -507,12 +572,11 @@ class ADKAPKGUI: 'log_apps_dir_missing': 'apps directory not found', 'log_extracted_resource_invalid': 'Extracted resources are invalid: {reason}. Flashing stopped. Check the password or package.', 'log_resource_ready': 'Resources ready (app: {count})', - 'log_module_resource_ready': 'Resources ready: main={main}, extra={extra}', - 'err_module_prop_missing': 'module.prop is missing from the package', - 'err_system_dir_missing': 'system directory is missing from the package', - 'err_disable_module_missing': 'disable-wireless-adb-vecentek-magisk.zip is missing from the package', - 'err_module_id_missing': 'module.prop is missing the id field', - 'err_disable_module_invalid': 'Extra module archive is invalid or missing module.prop', + 'log_module_resource_ready': 'Resources ready', + 'err_module_prop_missing': 'Resource config is missing from the package', + 'err_module_id_missing': 'Resource config is missing the id field', + 'err_module_zip_invalid': 'Resource file is invalid or missing config: {file}', + 'err_duplicate_module_id': 'Duplicate resource ID found in package', 'log_resource_prepare_failed_detail': 'Resource preparation failed: {error}', 'log_resource_prepare_network': 'Resource preparation failed. Check the network connection and retry.', 'log_package_file_missing': 'Package file not found', @@ -555,7 +619,7 @@ class ADKAPKGUI: 'log_runtime_install_failed': 'Permission failed', 'log_runtime_install_success': 'Getting permission', 'log_fastboot_enter_failed': 'Permission failed', - 'log_fastboot_wait': 'Getting permission', + 'log_fastboot_wait': 'Getting permission. Do not close the program or disconnect the data cable.', 'log_fastboot_missing': 'Permission failed', 'log_init_boot_flash_failed': 'Permission failed', 'log_init_boot_flash_success': 'Getting permission', @@ -564,16 +628,16 @@ class ADKAPKGUI: 'log_install_failed_detail': 'install failed: {error}', 'log_prepare_resource': 'Preparing resources...', 'log_flash_start': 'Starting language package flash...', - 'log_open_magisk': 'Opening Magisk App. Grant Shell/root permission when prompted.', - 'log_root_checking': 'Checking ADB Shell root permission...', - 'log_root_ok': 'ADB Shell root permission granted', - 'log_root_failed': 'ADB Shell root permission was not granted. Grant Shell permission in Magisk manually.', - 'msg_root_failed': 'Grant Shell permission in Magisk manually, then retry.', - 'log_module_install_start': 'Installing Magisk module: {module}', - 'log_module_install_done': 'Magisk module installed: {module}', - 'log_module_install_failed': 'Magisk module install failed: {module}: {error}', + 'log_open_magisk': 'Tap "允许" to grant permission when prompted.', + 'log_root_checking': 'Checking permission...', + 'log_root_ok': 'Permission granted', + 'log_root_failed': 'Permission grant failed. Grant permission manually.', + 'msg_root_failed': 'Grant permission manually, then retry.', + 'log_module_install_start': 'Installing resources', + 'log_module_install_done': 'Resources installed', + 'log_module_install_failed': 'Resource installation failed: {error}', 'log_push_file_failed': 'File push failed: {file}: {error}', - 'log_root_cmd_failed': 'Root command failed: {command}: {error}', + 'log_root_cmd_failed': 'Permission operation failed: {error}', 'log_no_lang_pkg': 'No language package files found', 'log_install_item_ok': 'Installed: {name}.apk', 'log_install_item_failed': 'Install failed: {name}.apk', @@ -621,6 +685,8 @@ class ADKAPKGUI: self.adb = self._find_tool('adb.exe', 'adb') self.sz = self._find_tool('7za.exe', str(find_resource('7za.exe'))) self.fastboot = self._find_tool('fastboot.exe', 'fastboot') + self.driver_dir = find_resource_dir("usb_driver") + self.driver_inf = self.driver_dir / "android_winusb.inf" self.package_file = find_resource("Q05_Lidar-package.bin") self.runtime_file = self._find_data_file("runtime.dat", prefer_embedded=True) self.resource_file = self._find_data_file("resource.dat", prefer_embedded=True) @@ -629,8 +695,7 @@ class ADKAPKGUI: self.resource_key = None self.apps_dir = None self.temp_dir = None - self.module_dir = None - self.disable_module_dir = None + self.module_zips = [] self.runtime_cache_dir = None self.api_url = "https://api.changan.softwindy.cn/api/authorizations/auth-check" self.boot_challenge_api_url = "https://api.changan.softwindy.cn/api/authorizations/boot-challenge" @@ -642,6 +707,7 @@ class ADKAPKGUI: self.device_connected = False self._refreshing = False # 防止并发刷新 self.debug_mode = False # 调试模式 + self.driver_prompted = False atexit.register(self.cleanup_cache_on_exit) # 设置样式 @@ -656,6 +722,15 @@ class ADKAPKGUI: # 启动设备状态监控 self.start_device_monitor() + def set_window_icon(self): + """Set the Tk window/taskbar icon at runtime; PyInstaller --icon only sets the exe file icon.""" + try: + icon_path = find_resource("app.ico") + if icon_path.exists(): + self.root.iconbitmap(str(icon_path)) + except Exception: + pass + def _find_tool(self, exe_name, fallback): candidates = resource_candidates(exe_name) candidates.extend([ @@ -931,15 +1006,27 @@ class ADKAPKGUI: bg=self.colors['bg_light']) self.auth_label.pack(side=tk.LEFT, padx=(5, 0)) - # 刷新按钮 - self.btn_refresh = tk.Button(status_bar_frame, text=self.t('btn_refresh'), + # 操作按钮:检查靠近设备状态,安装驱动在检查右侧 + status_actions_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) + status_actions_frame.pack(side=tk.RIGHT, padx=10, pady=5) + + self.btn_refresh = tk.Button(status_actions_frame, text=self.t('btn_refresh'), command=lambda: self.refresh_device_status(force=True), font=('Microsoft YaHei', 8), fg=self.colors['accent'], bg=self.colors['bg_light'], relief=tk.FLAT, cursor='hand2') - self.btn_refresh.pack(side=tk.RIGHT, padx=10, pady=5) + self.btn_refresh.pack(side=tk.LEFT, padx=(0, 8)) + + self.btn_install_driver = tk.Button(status_actions_frame, text=self.t('btn_install_driver'), + command=self.install_fastboot_driver, + font=('Microsoft YaHei', 8), + fg=self.colors['warning'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + self.btn_install_driver.pack(side=tk.LEFT) # 解压进度条框架 progress_frame = tk.Frame(left_frame, bg=self.colors['bg_dark']) @@ -1222,6 +1309,7 @@ class ADKAPKGUI: (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, 'btn_install_driver', None), 'btn_install_driver', None), (getattr(self, 'hint_label', None), 'hint_factory', None), (getattr(self, 'pwd_query_label', None), 'pwd_query_label', None), (getattr(self, 'btn_hotspot', None), 'hotspot_start', None), @@ -1246,10 +1334,28 @@ class ADKAPKGUI: self._update_device_status_impl(self.device_connected, self.vin, getattr(self, '_last_authorized', False)) + def _sanitize_user_log_message(self, message): + """Hide low-level device command details from normal operator logs.""" + text = str(message) + replacements = [ + (r'adb(?:\.exe)?(?:\s+-d)?\s+shell(?:\s+root)?', '权限获取'), + (r'/debug_ramdisk/su(?:\s+-c)?', '权限获取'), + (r'\bsu\s+-c\b', '权限获取'), + (r'Shell 权限', '权限'), + (r'\bShell permission\b', 'permission'), + (r'\b[Rr]oot command\b', 'permission operation'), + (r'\b[Rr]oot\b', 'permission'), + ] + for pattern, replacement in replacements: + text = re.sub(pattern, replacement, text) + return text + def _log_impl(self, message, level="INFO"): """日志写入的实际实现(必须在主线程调用)""" if not self.debug_mode and level in ("INFO", "CMD"): return + if not self.debug_mode: + message = self._sanitize_user_log_message(message) timestamp = datetime.now().strftime("%H:%M:%S") log_entry = f"[{timestamp}] [{level}] {message}\n" self.log_text.insert(tk.END, log_entry, level) @@ -1373,11 +1479,11 @@ class ADKAPKGUI: threading.Thread(target=monitor, daemon=True).start() # ============================================================ - # 核心:adb shell 自动密码输入 + # 核心:设备权限命令自动密码输入 # ============================================================ def run_adb_shell(self, shell_command, timeout=15): - """执行 adb shell 命令,自动静默输入设备密码 adb36987。 + """执行设备权限命令,自动静默输入设备密码 adb36987。 静默执行,不显示 adb 原始输出,仅返回结果。""" if self.debug_mode: self.log(f"CMD: adb shell {shell_command}", "CMD") @@ -1461,79 +1567,70 @@ class ADKAPKGUI: ok, output = self.run_root_command("id", timeout=20) if ok and "uid=0" in output: self.log(self.t('log_root_ok'), "STATUS") + self.run_adb_shell('pm uninstall --user 0 com.topjohnwu.magisk', timeout=30) return True self.log(self.t('log_root_failed'), "ERROR") return False - def install_magisk_module_files(self, module_dir, skip_names=None): - module_dir = Path(module_dir) - skip_names = set(skip_names or []) - mod_id = self.read_module_id(module_dir) + def disable_and_uninstall_preinstalled_packages(self): + self.run_adb_shell('pm disable-user com.incall.apps.softmanager', timeout=30) + package_names = [ + "com.carcontrolhome.app", + "com.tinnove.netease.music", + "com.wtcl.electronicdirections", + ] + for package_name in package_names: + self.run_adb_shell(f'pm disable-user {package_name}', timeout=30) + self.run_adb_shell(f'pm uninstall --user 0 {package_name}', timeout=30) + + def install_magisk_module_zip(self, zip_path): + zip_path = Path(zip_path) + mod_id = self.read_module_id_from_zip(zip_path) if not mod_id: return False, self.t('err_module_id_missing') + if not zip_path.exists(): + return False, self.tf('err_extract_failed', error=f"{zip_path} not found") - device_stage = f"/data/local/tmp/q05_lidar_modules/{mod_id}" + device_stage_dir = "/data/local/tmp/q05_lidar_modules" + device_zip = f"{device_stage_dir}/{mod_id}.zip" device_module = f"/data/adb/modules/{mod_id}" - files = [ - p for p in module_dir.rglob("*") - if p.is_file() - and not self._is_skipped_module_path(module_dir, p, skip_names) - ] - if not files: - return False, self.t('err_no_apks') - self.log(self.tf('log_module_install_start', module=mod_id), "STATUS") + self.log(self.t('log_module_install_start'), "STATUS") commands = [ - f"rm -rf {self.shell_quote(device_stage)} {self.shell_quote(device_module)}", - f"mkdir -p {self.shell_quote(device_stage)} {self.shell_quote(device_module)}", - f"chmod -R 777 {self.shell_quote(device_stage)}", + f"rm -rf {self.shell_quote(device_module)}", + f"mkdir -p {self.shell_quote(device_stage_dir)} {self.shell_quote(device_module)}", + f"chmod 777 {self.shell_quote(device_stage_dir)}", ] for command in commands: ok, output = self.run_root_command(command, timeout=60) if not ok: return False, self.tf('log_root_cmd_failed', command=command, error=output) - total = len(files) - for idx, local_file in enumerate(files, 1): - rel = local_file.relative_to(module_dir).as_posix() - remote_file = f"{device_stage}/{rel}" - remote_parent = remote_file.rsplit("/", 1)[0] - ok, output = self.run_root_command(f"mkdir -p {self.shell_quote(remote_parent)}", timeout=30) - if not ok: - return False, self.tf('log_root_cmd_failed', command="mkdir", error=output) - ok, output = self.run_root_command(f"chmod 777 {self.shell_quote(remote_parent)}", timeout=30) - if not ok: - return False, self.tf('log_root_cmd_failed', command="chmod", error=output) - ok, output = self.run_adb_command(f'adb -d push "{local_file}" {remote_file}') - if not ok: - return False, self.tf('log_push_file_failed', file=rel, error=output) - self.update_progress(idx, total, self.tf('progress_push_files', current=idx, total=total), is_push=True) + ok, output = self.run_adb_command(f'adb -d push "{zip_path}" {device_zip}') + if not ok: + return False, self.tf('log_push_file_failed', file=zip_path.name, error=output) install_commands = [ - f"cp -af {self.shell_quote(device_stage)}/. {self.shell_quote(device_module)}/", + f"unzip -oq {self.shell_quote(device_zip)} -x 'META-INF/*' -d {self.shell_quote(device_module)}", f"find {self.shell_quote(device_module)} -type d -exec chmod 755 {{}} \\;", f"find {self.shell_quote(device_module)} -type f -exec chmod 644 {{}} \\;", f"find {self.shell_quote(device_module)} -type f -name '*.sh' -exec chmod 755 {{}} \\;", - f"rm -rf {self.shell_quote(device_stage)}", + f"rm -f {self.shell_quote(device_zip)}", ] for command in install_commands: - ok, output = self.run_root_command(command, timeout=120) + ok, output = self.run_root_command(command, timeout=300) if not ok: return False, self.tf('log_root_cmd_failed', command=command, error=output) - self.log(self.tf('log_module_install_done', module=mod_id), "STATUS") + self.log(self.t('log_module_install_done'), "STATUS") return True, mod_id - def _is_skipped_module_path(self, module_dir, file_path, skip_names): - rel = file_path.relative_to(module_dir) - parts = rel.parts - if parts and (parts[0] == "META-INF" or parts[0].startswith("_")): - return True - return file_path.name in skip_names - def check_package_extracted(self): """检查语言包是否已解压""" - has_module = self.module_dir and self.module_dir.exists() + has_module = bool(getattr(self, 'module_zips', None)) + if not has_module and self.temp_dir and self.temp_dir.exists(): + self.module_zips = self.find_module_zips(self.temp_dir) + has_module = bool(self.module_zips) if has_module: ok, reason = self._validate_extracted_modules() if not ok: @@ -1543,22 +1640,16 @@ class ADKAPKGUI: return has_module def _validate_extracted_modules(self): - if not self.module_dir or not self.module_dir.exists(): + if not getattr(self, 'module_zips', None): return False, self.t('err_module_prop_missing') - if not (self.module_dir / "module.prop").exists(): - return False, self.t('err_module_prop_missing') - if not (self.module_dir / "system").exists(): - return False, self.t('err_system_dir_missing') - if not (self.module_dir / "disable-wireless-adb-vecentek-magisk.zip").exists(): - return False, self.t('err_disable_module_missing') - main_id = self.read_module_id(self.module_dir) - if not main_id: - return False, self.t('err_module_id_missing') - if not self.disable_module_dir or not self.disable_module_dir.exists(): - return False, self.t('err_disable_module_invalid') - extra_id = self.read_module_id(self.disable_module_dir) - if not extra_id: - return False, self.t('err_disable_module_invalid') + module_ids = [] + for zip_path in self.module_zips: + mod_id = self.read_module_id_from_zip(zip_path) + if not mod_id: + return False, self.tf('err_module_zip_invalid', file=zip_path.name) + module_ids.append(mod_id) + if len(set(module_ids)) != len(module_ids): + return False, self.t('err_duplicate_module_id') return True, "" def _clear_extracted_cache(self): @@ -1566,8 +1657,7 @@ class ADKAPKGUI: shutil.rmtree(self.temp_dir, ignore_errors=True) time.sleep(0.5) self.apps_dir = None - self.module_dir = None - self.disable_module_dir = None + self.module_zips = [] def cleanup_cache_on_exit(self): self._clear_extracted_cache() @@ -1650,20 +1740,27 @@ class ADKAPKGUI: output = self._decode_process_output((result.stderr or b'') + (result.stdout or b'')) return result.returncode == 0, output - def _find_main_module_dir(self, root_dir): - candidates = [root_dir] - candidates.extend([p for p in root_dir.iterdir() if p.is_dir()]) - for candidate in candidates: - if (candidate / "module.prop").exists() and (candidate / "system").exists(): - return candidate - return None + def find_module_zips(self, root_dir): + root_dir = Path(root_dir) + candidates = [] + modules_dir = root_dir / "modules" + search_roots = [modules_dir] if modules_dir.exists() else [root_dir] + for search_root in search_roots: + candidates.extend(sorted(search_root.rglob("*.zip"))) + module_zips = [] + for zip_path in candidates: + if self.read_module_id_from_zip(zip_path): + module_zips.append(zip_path) + return module_zips - def read_module_id(self, module_dir): - prop_path = Path(module_dir) / "module.prop" - if not prop_path.exists(): - return "" + def read_module_id_from_zip(self, zip_path): try: - for line in prop_path.read_text(encoding='utf-8', errors='replace').splitlines(): + with zipfile.ZipFile(zip_path, 'r') as zf: + prop_name = self._find_module_prop_in_zip(zf) + if not prop_name: + return "" + raw = zf.read(prop_name) + for line in raw.decode('utf-8', errors='replace').splitlines(): line = line.strip() if line.startswith("id="): return line.split("=", 1)[1].strip() @@ -1671,6 +1768,25 @@ class ADKAPKGUI: return "" return "" + def _find_module_prop_in_zip(self, zf): + names = zf.namelist() + normalized = {} + for name in names: + clean = name.replace("\\", "/").lstrip("./") + normalized[clean] = name + if "module.prop" in normalized: + return normalized["module.prop"] + + candidates = [] + for clean, original in normalized.items(): + parts = [part for part in clean.split("/") if part] + if len(parts) == 2 and parts[-1] == "module.prop": + candidates.append((len(parts), original)) + if candidates: + candidates.sort() + return candidates[0][1] + return "" + def extract_package_silent(self): """静默解压语言包(带进度)—— Q05-Lidar 使用 Magisk 模块结构""" if not self.package_file.exists(): @@ -1720,37 +1836,18 @@ class ADKAPKGUI: return False self.update_progress(1, 1, self.t('progress_resource_done')) - self.module_dir = self._find_main_module_dir(self.temp_dir) - if not self.module_dir: + self.module_zips = self.find_module_zips(self.temp_dir) + if not self.module_zips: self.log(self.t('err_module_prop_missing'), "ERROR") self._clear_extracted_cache() return False - disable_zip = self.module_dir / "disable-wireless-adb-vecentek-magisk.zip" - self.disable_module_dir = self.temp_dir / "_disable_wireless_adb_vecentek" - if self.disable_module_dir.exists(): - shutil.rmtree(self.disable_module_dir, ignore_errors=True) - self.disable_module_dir.mkdir(parents=True, exist_ok=True) - if not disable_zip.exists(): - self.log(self.t('err_disable_module_missing'), "ERROR") - self._clear_extracted_cache() - return False - ok, err_msg = self._run_7za_extract_basic(disable_zip, self.disable_module_dir) - if not ok: - self.log(self.tf('err_extract_failed', error=err_msg[:300]), "ERROR") - self._clear_extracted_cache() - return False - ok, reason = self._validate_extracted_modules() if not ok: self.log(self.tf('log_extracted_resource_invalid', reason=reason), "ERROR") self._clear_extracted_cache() return False - self.log(self.tf( - 'log_module_resource_ready', - main=self.read_module_id(self.module_dir), - extra=self.read_module_id(self.disable_module_dir) - ), "INFO") + self.log(self.t('log_module_resource_ready'), "INFO") return True except Exception as e: @@ -1779,6 +1876,154 @@ class ADKAPKGUI: self.log(self.t('log_adb_missing'), "ERROR") except FileNotFoundError: self.log(self.t('log_adb_missing'), "ERROR") + self.root.after(800, self.check_fastboot_driver_on_startup) + + def _run_command_capture(self, command, timeout=30): + creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + result = subprocess.run( + command, + capture_output=True, + text=True, + timeout=timeout, + creationflags=creationflags + ) + output = (result.stdout or "") + (result.stderr or "") + return result.returncode, output.strip() + + def ask_ok_cancel_on_ui_thread(self, title, message): + result = {"value": False} + done = threading.Event() + + def prompt(): + try: + result["value"] = messagebox.askokcancel(title, message, parent=self.root) + finally: + done.set() + + self.run_on_ui_thread(prompt) + done.wait() + return result["value"] + + def is_fastboot_driver_installed(self): + if sys.platform != 'win32': + return True, "" + try: + code, output = self._run_command_capture(['pnputil', '/enum-drivers'], timeout=40) + if code != 0: + return False, output or "pnputil enum failed" + normalized = output.lower() + installed = ( + 'android_winusb.inf'.lower() in normalized + or 'android bootloader interface' in normalized + or 'android adb interface' in normalized + or 'fastboot' in normalized + ) + return installed, output + except Exception as e: + return False, str(e) + + def _install_driver_with_uac(self, inf_path): + quoted_inf = str(inf_path).replace("'", "''") + ps_command = ( + "$proc = Start-Process -FilePath pnputil " + "-ArgumentList @('/add-driver', '{0}', '/install') " + "-Verb RunAs -WindowStyle Hidden -PassThru; " + "if ($null -eq $proc) {{ exit 1 }}; " + "$proc.WaitForExit(); " + "Write-Output $proc.ExitCode" + ).format(quoted_inf) + creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + result = subprocess.run( + ['powershell', '-NoProfile', '-Command', ps_command], + capture_output=True, + text=True, + creationflags=creationflags + ) + output = ((result.stdout or "") + (result.stderr or "")).strip() + exit_code = None + for line in reversed(output.splitlines()): + text = line.strip() + if text.isdigit(): + exit_code = int(text) + break + if exit_code is None and result.returncode == 0: + exit_code = 0 + return exit_code == 0, output or f"powershell exit={result.returncode}" + + def install_fastboot_driver(self, prompt=True): + def worker(): + try: + if not self.driver_inf.exists(): + error = self.tf('msg_driver_install_failed', error=f"{self.driver_inf} not found") + self.log(self.tf('log_driver_install_failed', error=f"{self.driver_inf} not found"), "ERROR") + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_driver_missing_title'), error)) + return + + if prompt: + confirmed = self.ask_ok_cancel_on_ui_thread( + self.t('msg_driver_missing_title'), + self.t('msg_driver_install_confirm') + ) + if not confirmed: + return + + self.log(self.t('log_driver_install_start'), "STATUS") + ok, output = self._install_driver_with_uac(self.driver_inf) + if not ok: + self.log(self.tf('log_driver_install_failed', error=output or self.t('unknown_error')), "ERROR") + self.run_on_ui_thread( + lambda: messagebox.showerror( + self.t('msg_driver_missing_title'), + self.tf('msg_driver_install_failed', error=output or self.t('unknown_error')), + parent=self.root + ) + ) + return + + self.log(self.t('log_driver_install_success'), "SUCCESS") + self.run_on_ui_thread( + lambda: messagebox.showinfo( + self.t('msg_driver_missing_title'), + self.t('msg_driver_install_done'), + parent=self.root + ) + ) + except Exception as e: + self.log(self.tf('log_driver_install_failed', error=str(e)), "ERROR") + self.run_on_ui_thread( + lambda: messagebox.showerror( + self.t('msg_driver_missing_title'), + self.tf('msg_driver_install_failed', error=str(e)), + parent=self.root + ) + ) + + threading.Thread(target=worker, daemon=True).start() + + def check_fastboot_driver_on_startup(self): + if self.driver_prompted: + return + self.driver_prompted = True + + def worker(): + installed, detail = self.is_fastboot_driver_installed() + if installed: + return + + if self.debug_mode and detail: + self.log(detail[:500], "CMD") + + def notify_and_install(): + messagebox.showinfo( + self.t('msg_driver_missing_title'), + self.t('msg_driver_missing'), + parent=self.root + ) + self.install_fastboot_driver(prompt=False) + + self.run_on_ui_thread(notify_and_install) + + threading.Thread(target=worker, daemon=True).start() def refresh_device_status(self, force=False): """刷新设备状态 —— 逸动版使用 ca.car.vin 获取 VIN""" @@ -2184,6 +2429,10 @@ class ADKAPKGUI: return True return False + def fastboot_output_has_okay(self, output): + text = str(output or "").upper() + return "OKAY" in text and "FAILED" not in text + def wait_for_fastboot(self, timeout=180, interval=5): deadline = time.time() + timeout while time.time() < deadline: @@ -2236,9 +2485,9 @@ class ADKAPKGUI: self.update_progress(4, 7, self.t('progress_reboot_fastboot'), is_push=True) ok, output = self.run_adb_shell('reboot fastboot') if not ok: - self.log(self.tf('log_fastboot_enter_failed', output=output), "ERROR") - return - self.log(self.t('log_fastboot_wait'), "INFO") + if self.debug_mode: + self.log(self.tf('log_fastboot_enter_failed', output=output), "CMD") + self.log(self.t('log_fastboot_wait'), "WARNING") if not self.wait_for_fastboot(): self.log(self.t('log_fastboot_missing'), "ERROR") return @@ -2250,7 +2499,7 @@ class ADKAPKGUI: self.update_progress(6, 7, self.t('progress_flash_init_boot'), is_push=True) ok, output = self.run_fastboot_command(['flash', 'init_boot', str(temp_img)], timeout=120) - if not ok: + if not ok or not self.fastboot_output_has_okay(output): self.log(self.tf('log_init_boot_flash_failed', output=output), "ERROR") return self.log(self.t('log_init_boot_flash_success'), "INFO") @@ -2326,7 +2575,7 @@ class ADKAPKGUI: return self.show_progress(False, is_push=False) - if not self.module_dir or not self.module_dir.exists(): + if not getattr(self, 'module_zips', None): self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_dir_missing'))) return @@ -2337,25 +2586,18 @@ class ADKAPKGUI: if not self.open_magisk_and_check_root(): self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_root_failed'))) return + self.disable_and_uninstall_preinstalled_packages() - main_id = self.read_module_id(self.module_dir) - self.update_progress(0, 2, self.tf('progress_install_module', module=main_id), is_push=True) - ok, result = self.install_magisk_module_files( - self.module_dir, - skip_names={"disable-wireless-adb-vecentek-magisk.zip"} - ) - if not ok: - self.log(self.tf('log_module_install_failed', module=main_id or "main", error=result), "ERROR") - return + total = len(self.module_zips) + for idx, zip_path in enumerate(self.module_zips, 1): + mod_id = self.read_module_id_from_zip(zip_path) + self.update_progress(idx - 1, total, self.t('progress_install_module'), is_push=True) + ok, result = self.install_magisk_module_zip(zip_path) + if not ok: + self.log(self.tf('log_module_install_failed', error=result), "ERROR") + return - extra_id = self.read_module_id(self.disable_module_dir) - self.update_progress(1, 2, self.tf('progress_install_module', module=extra_id), is_push=True) - ok, result = self.install_magisk_module_files(self.disable_module_dir) - if not ok: - self.log(self.tf('log_module_install_failed', module=extra_id or "extra", error=result), "ERROR") - return - - self.update_progress(2, 2, self.t('progress_flash_done'), is_push=True) + self.update_progress(total, total, self.t('progress_flash_done'), is_push=True) self.log(self.t('log_push_success'), "SUCCESS") finally: self.show_progress(False, is_push=True) @@ -2854,8 +3096,7 @@ class ADKAPKGUI: if self.extract_package_silent(): self.log(self.tf( 'log_package_extract_modules_success', - main=self.read_module_id(self.module_dir), - extra=self.read_module_id(self.disable_module_dir) + count=len(self.module_zips) ), "SUCCESS") else: self.log(self.t('log_package_extract_failed'), "ERROR") @@ -3147,6 +3388,7 @@ def main(): sys.exit(1) try: + set_windows_app_id() app = ADKAPKGUI() app.run() except Exception as e: diff --git a/Q05-Lidar/pack_q05_lidar.bat b/Q05-Lidar/pack_q05_lidar.bat index 82cee9d..146e682 100644 --- a/Q05-Lidar/pack_q05_lidar.bat +++ b/Q05-Lidar/pack_q05_lidar.bat @@ -116,11 +116,11 @@ if exist "%APPDIR%resource.dat" copy "%APPDIR%resource.dat" . >nul echo [5/6] PyInstaller... set ADD_RESOURCE= if exist "resource.dat" set ADD_RESOURCE=--add-data "resource.dat;." -"%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-data "fastboot.exe;." %ADD_RESOURCE% --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=cryptography --collect-all tkinter --collect-all cryptography --uac-admin launcher.py -if errorlevel 1 ( - cd .. - echo [ERROR] PyInstaller failed - pause +"%PY%" -m PyInstaller --onefile --windowed --name="%NAME%" --icon="app.ico" --add-data "app.ico;." --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --add-data "fastboot.exe;." --add-data "%APPDIR%usb_driver;usb_driver" %ADD_RESOURCE% --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=cryptography --collect-all tkinter --collect-all cryptography --uac-admin launcher.py +if errorlevel 1 ( + cd .. + echo [ERROR] PyInstaller failed + pause exit /b ) @@ -157,6 +157,12 @@ if errorlevel 1 ( pause exit /b 1 ) +if not exist "%APPDIR%usb_driver\android_winusb.inf" ( + cd .. + echo [ERROR] usb_driver\android_winusb.inf not found at %APPDIR%usb_driver + pause + exit /b 1 +) echo [7/7] Cleanup... del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py adb.exe AdbWinApi.dll AdbWinUsbApi.dll 7za.exe fastboot.exe resource.dat 2>nul diff --git a/Q07/app.py b/Q07/Qiyuan_Q07-multi-lan-installer.py similarity index 62% rename from Q07/app.py rename to Q07/Qiyuan_Q07-multi-lan-installer.py index 1a1578a..9daa829 100644 --- a/Q07/app.py +++ b/Q07/Qiyuan_Q07-multi-lan-installer.py @@ -8,6 +8,7 @@ import json import re import threading import tkinter as tk +import atexit from tkinter import ttk, scrolledtext, filedialog, messagebox, simpledialog from pathlib import Path from urllib.request import urlopen, Request @@ -59,16 +60,32 @@ def find_tool(file_name, fallback=None): path = find_resource(file_name) if path.exists(): return str(path) - return fallback or str(path) + return fallback or str(path) + + +def set_windows_app_user_model_id(): + if sys.platform != 'win32': + return + try: + import ctypes + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + "yibin.keyi.qiyuan.q07.language.installer" + ) + except Exception: + pass + + class ADKAPKGUI: def __init__(self): + set_windows_app_user_model_id() self.root = tk.Tk() self.root.title("长安语言安装工具") self.root.geometry("650x640") self.root.resizable(True, True) + self.set_window_icon() - # 设置颜色主题 - self.colors_dark = { + # 固定颜色 + self.colors = { 'bg_dark': '#1e1e2e', 'bg_light': '#2a2a3e', 'accent': '#6c5ce7', @@ -81,21 +98,6 @@ class ADKAPKGUI: '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' @@ -111,6 +113,7 @@ class ADKAPKGUI: 'btn_reboot': '🔄 重启设备', 'btn_disable_upgrade': '❌ 禁用升级', 'btn_clear_log': '🗑 清空日志', + 'btn_debug_extract': '解压测试', 'device_label': '设备:', 'vin_label': 'VIN码:', 'auth_label': '授权:', @@ -151,21 +154,127 @@ class ADKAPKGUI: 'err_no_password': '错误:解压密码未设置', 'err_no_7za': '错误:未找到 7za.exe', 'err_extract_fail': '解压失败', - 'info_extracting': '正在解压资源包...', + 'info_extracting': '资源准备中...', 'info_extract_done': '资源准备完成', 'err_extract_user': '资源准备失败,请检查网络连接后重试', 'warn_no_app_dir': '警告:未找到 app/priv-app 目录', - 'theme_dark': '🌙 暗色', - 'theme_light': '☀️ 亮色', + 'progress_resource_loading': '资源准备中...', + 'progress_resource_done': '资源准备完成', + 'progress_extracting_percent': '资源准备中 {percent}%', + 'progress_flashing': '正在刷入', + 'progress_flash_done': '刷入完成', + 'progress_aborted': '已终止', + 'progress_installing': '安装中', + 'progress_installing_name': '安装中 ({name})', + 'progress_done': '完成', 'lang_zh': '中', 'lang_en': 'EN', 'switch_lang': '语言 / Language', - 'switch_theme': '切换主题', - 'about_company': '宜宾科宜科技有限公司 - 智能设备管理平台', + 'tip_1': '1. 安装语言过程中请保持车辆和电脑的电量充足,不可中途停止。', + 'tip_2': '2. 获取权限以后,车辆自动重启以后再进入语言刷入。', + 'tip_3': '3. 部分语言需要重启后生效,可以一切工作完成以后再重启。', 'warn_flash_warning': '⚠️ 重要提示', 'warn_flash_msg': '刷入过程中请勿:\n ● 重启车机\n ● 退出本程序\n ● 关闭电脑\n\n否则可能导致车机系统损坏!', 'err_wrong_password': '请检查密码是否正确', 'title_pop_lang': '快捷语言设置', + 'quick_lang_header': '选择目标语言', + 'quick_lang_hint': '点击按钮即可将系统语言切换为对应语言,重启后生效', + 'quick_lang_system': '⚙️ 打开系统语言设置(手动选择)', + 'msg_warn_title': '警告', + 'msg_error_title': '错误', + 'msg_done_title': '完成', + 'msg_success_title': '成功', + 'msg_auth_failed_title': '授权失败', + 'msg_device_unauthorized': '设备未授权', + 'msg_resource_prepare_failed': '资源准备失败!', + 'msg_resource_dir_missing': '资源目录未找到', + 'msg_no_apk_in_folder': '所选文件夹中没有APK文件!', + 'msg_confirm_install_title': '确认安装', + 'msg_confirm_install_many': '已选择 {count} 个APK文件\n\n是否开始安装?', + 'msg_confirm_install_folder': '找到 {count} 个APK文件\n\n是否开始批量安装?', + 'msg_install_success_many': '成功安装 {count} 个APK!', + 'msg_install_partial': '成功: {success}\n失败: {failed}', + 'msg_install_all_failed': '所有APK安装失败!', + 'msg_install_exception': '安装过程异常:{error}', + 'msg_quick_lang_success': '系统语言已设置为 {language}\n\n⚠️ 请重启设备使其生效。', + 'msg_quick_lang_failed': '语言设置失败!\n\n{output}', + 'msg_disable_confirm_title': '确认禁用升级', + 'msg_disable_confirm': '⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n是否确定要禁用系统升级应用?', + 'msg_disable_success': '系统升级已成功禁用!', + 'msg_disable_failed': '禁用失败:{output}', + 'msg_reboot_confirm_title': '确认重启', + 'file_apk': 'APK文件', + 'file_all': '所有文件', + 'dialog_select_apk': '选择APK文件', + 'dialog_select_apk_folder': '选择包含APK文件的文件夹', + 'unknown_error': '未知错误', + 'status_debug': '调试模式', + 'debug_password_prompt': '请输入调试密码:', + 'debug_password_verifying': '正在校验调试密码...', + 'debug_wrong_password': '密码错误', + 'debug_verify_failed': '调试密码校验失败: {message}', + 'debug_need_enable': '请先按 Ctrl+Shift+D 进入调试模式', + 'debug_need_vin': '调试解压测试需要 VIN。请先连接设备刷新,或在调试模式中手动设置 VIN。', + 'log_debug_on': '🔧 调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志', + 'log_debug_off': '调试模式已关闭', + 'log_lang_switched': '语言已切换为中文', + 'log_cache_invalid': '已解压缓存无效: {reason}', + 'log_cache_reuse_invalid': '缓存资源无效,已清理: {reason}', + 'log_package_missing': '未找到资源包文件: {path}', + 'log_adb_missing': '未找到adb命令,请将ADB文件放入本目录', + 'log_device_connected': '设备已连接', + 'log_device_disconnected': '设备已断开连接', + 'log_current_vin': '当前VIN: {vin}', + 'log_vin_unavailable': '无法读取VIN', + 'log_refresh_failed': '刷新设备状态失败: {error}', + 'log_auth_skip_debug': '调试模式:跳过授权', + 'log_auth_checking': '正在验证授权状态...', + 'log_auth_ok': '授权验证通过', + 'log_auth_failed': '授权验证失败', + 'log_vehicle_name': '车型名称: {name}', + 'log_adb_required': '请先连接 ADB 并获取 VIN', + 'log_data_prepare_failed_detail': '资源准备失败: {error}', + 'log_extract_password_missing': '错误:解压密码未设置', + 'log_7za_missing': '错误:未找到 7za.exe ({path})', + 'log_extracted_resource_invalid': '解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。', + 'log_extract_done': '资源准备完成', + 'log_extract_exception': '资源准备失败: {error}', + 'err_extract_wrong_password': '解压密码错误,请重新确认 Q07_package.bin 密码', + 'err_extract_data': '资源包数据错误,可能是密码错误或 Q07_package.bin 损坏', + 'err_extract_corrupt': '资源包损坏或不完整,请检查 Q07_package.bin', + 'err_extract_failed': '解压失败: {error}', + 'err_extract_failed_code': '解压失败 (返回码 {code}),请检查密码是否正确', + 'log_root_failed': '获取 root 失败', + 'log_permission_failed': '获取权限失败', + 'log_permission_reboot_required': '首次获取权限,需要重启设备...', + 'log_permission_rebooting': '设备即将重启,重启后权限生效', + 'log_reboot_failed': '重启失败', + 'log_permission_ok': '已获取权限', + 'log_flash_readonly': '请先点击「获取权限」获取权限后再试', + 'log_flash_complete_count': '刷入完成,共 {count} 个语言包', + 'log_flash_effect_after_reboot': '语言包已刷入完成,重启设备后生效,您可在适当时候重启', + 'log_flash_partial': '部分刷入成功({success}/{total})', + 'log_install_start': '开始安装 {count} 个APK...', + 'log_install_done_all': '安装完成:全部 {count} 个成功', + 'log_install_done_partial': '安装完成:{success}/{total} 成功', + 'log_install_failed': '安装失败', + 'log_install_exception': '安装过程异常: {error}', + 'log_install_success_item': '✓ {name}', + 'log_install_failed_item': '✗ {name}', + 'log_quick_lang_setting': '正在设置系统语言为: {language} ({locale})', + 'log_quick_lang_success': '✓ 语言已设置为 {language}', + 'log_quick_lang_failed': '✗ 语言设置失败: {output}', + 'log_rebooting': '设备正在重启...', + 'log_disable_cancelled': '已取消禁用升级操作', + 'log_disable_success': '系统升级已禁用', + 'log_disable_failed': '禁用系统升级失败', + 'log_package_key_failed': 'package-key 获取失败', + 'log_package_extract_success': '资源准备完成', + 'log_package_extract_failed': 'Q07_package.bin 解压测试失败', + 'err_no_usable_apk': '未找到可用 APK', + 'err_zero_apk': '发现 0KB APK: {files}', + 'err_push_failed': 'push失败: {error}', + 'err_cp_failed': 'cp失败: {error}', }, 'en': { 'title': 'Qiyuan Q07 Multi-Language', @@ -178,6 +287,7 @@ class ADKAPKGUI: 'btn_reboot': '🔄 Reboot', 'btn_disable_upgrade': '❌ Disable OTA', 'btn_clear_log': '🗑 Clear Log', + 'btn_debug_extract': 'Extract', 'device_label': 'Device:', 'vin_label': 'VIN:', 'auth_label': 'Auth:', @@ -214,25 +324,131 @@ class ADKAPKGUI: 'info_auth_pass': '✅ Authorization passed!', 'info_auth_fail': '❌ Authorization failed', 'info_preparing': 'Preparing resources...', - 'err_no_package': 'Error: package.bin not found', + 'err_no_package': 'Error: Q07_package.bin not found', 'err_no_password': 'Error: password not set', 'err_no_7za': 'Error: 7za.exe not found', 'err_extract_fail': 'Extraction failed', - 'info_extracting': 'Extracting package...', + 'info_extracting': 'Preparing resources...', 'info_extract_done': 'Resource preparation complete', 'err_extract_user': 'Resource preparation failed, check network and retry', 'warn_no_app_dir': 'Warning: app/priv-app directory not found', - 'theme_dark': '🌙 Dark', - 'theme_light': '☀️ Light', + 'progress_resource_loading': 'Preparing resources...', + 'progress_resource_done': 'Resources ready', + 'progress_extracting_percent': 'Preparing resources {percent}%', + 'progress_flashing': 'Flashing', + 'progress_flash_done': 'Flash complete', + 'progress_aborted': 'Aborted', + 'progress_installing': 'Installing', + 'progress_installing_name': 'Installing ({name})', + 'progress_done': 'Done', 'lang_zh': '中', 'lang_en': 'EN', 'switch_lang': 'Language', - 'switch_theme': 'Theme', - 'about_company': 'Yibin Keyi Technology - Smart Device Platform', + 'tip_1': '1. Keep the vehicle and PC powered during language installation.', + 'tip_2': '2. After permission is obtained, wait for the vehicle to reboot before flashing.', + 'tip_3': '3. Some languages take effect after reboot; reboot after all work is finished.', 'warn_flash_warning': '⚠️ Warning', 'warn_flash_msg': 'During flashing, DO NOT:\n ● Reboot vehicle\n ● Close this app\n ● Power off PC\n\nSystem damage may occur!', 'err_wrong_password': 'Please check password', 'title_pop_lang': 'Quick Language Setting', + 'quick_lang_header': 'Select target language', + 'quick_lang_hint': 'Click a button to set the system language. Reboot to apply.', + 'quick_lang_system': '⚙️ Open system language settings', + 'msg_warn_title': 'Warning', + 'msg_error_title': 'Error', + 'msg_done_title': 'Done', + 'msg_success_title': 'Success', + 'msg_auth_failed_title': 'Authorization failed', + 'msg_device_unauthorized': 'Device unauthorized', + 'msg_resource_prepare_failed': 'Resource preparation failed!', + 'msg_resource_dir_missing': 'Resource directory not found', + 'msg_no_apk_in_folder': 'No APK files found in the selected folder!', + 'msg_confirm_install_title': 'Confirm install', + 'msg_confirm_install_many': '{count} APK files selected.\n\nStart installation?', + 'msg_confirm_install_folder': '{count} APK files found.\n\nStart batch installation?', + 'msg_install_success_many': '{count} APKs installed successfully!', + 'msg_install_partial': 'Success: {success}\nFailed: {failed}', + 'msg_install_all_failed': 'All APK installations failed!', + 'msg_install_exception': 'Installation error: {error}', + 'msg_quick_lang_success': 'System language set to {language}.\n\n⚠️ Reboot the device to apply.', + 'msg_quick_lang_failed': 'Language setting failed!\n\n{output}', + 'msg_disable_confirm_title': 'Confirm Disable OTA', + 'msg_disable_confirm': '⚠️ Warning: after disabling OTA, the system will no longer receive updates.\n\nDisable the OTA app?', + 'msg_disable_success': 'System upgrade has been disabled!', + 'msg_disable_failed': 'Disable failed: {output}', + 'msg_reboot_confirm_title': 'Confirm reboot', + 'file_apk': 'APK files', + 'file_all': 'All files', + 'dialog_select_apk': 'Select APK file', + 'dialog_select_apk_folder': 'Select a folder containing APK files', + 'unknown_error': 'unknown error', + 'status_debug': 'Debug mode', + 'debug_password_prompt': 'Enter debug password:', + 'debug_password_verifying': 'Verifying debug password...', + 'debug_wrong_password': 'Wrong password', + 'debug_verify_failed': 'Debug password verification failed: {message}', + 'debug_need_enable': 'Press Ctrl+Shift+D first.', + 'debug_need_vin': 'Extract test needs a VIN. Refresh a connected device or set VIN in debug mode.', + 'log_debug_on': '🔧 Debug mode enabled - authorization and device checks are skipped, detailed ADB logs are shown', + 'log_debug_off': 'Debug mode disabled', + 'log_lang_switched': 'Language switched to English', + 'log_cache_invalid': 'Extract cache invalid: {reason}', + 'log_cache_reuse_invalid': 'Cached resources invalid and cleaned: {reason}', + 'log_package_missing': 'Resource package not found: {path}', + 'log_adb_missing': 'adb not found. Put ADB files in this directory.', + 'log_device_connected': 'Device connected', + 'log_device_disconnected': 'Device disconnected', + 'log_current_vin': 'Current VIN: {vin}', + 'log_vin_unavailable': 'Unable to read VIN', + 'log_refresh_failed': 'Refresh device status failed: {error}', + 'log_auth_skip_debug': 'Debug mode: skip authorization', + 'log_auth_checking': 'Checking authorization...', + 'log_auth_ok': 'Authorization passed', + 'log_auth_failed': 'Authorization failed', + 'log_vehicle_name': 'Vehicle name: {name}', + 'log_adb_required': 'Connect ADB and get VIN first', + 'log_data_prepare_failed_detail': 'Resource preparation failed: {error}', + 'log_extract_password_missing': 'Extraction password is not set', + 'log_7za_missing': '7za.exe not found: {path}', + 'log_extracted_resource_invalid': 'Extracted resources are invalid: {reason}. Flashing stopped. Check the password or package.', + 'log_extract_done': 'Resources ready', + 'log_extract_exception': 'Resource preparation failed: {error}', + 'err_extract_wrong_password': 'Incorrect extraction password. Check the Q07_package.bin password.', + 'err_extract_data': 'Package data error. The password may be wrong or Q07_package.bin may be damaged.', + 'err_extract_corrupt': 'Package is damaged or incomplete. Check Q07_package.bin.', + 'err_extract_failed': 'Extraction failed: {error}', + 'err_extract_failed_code': 'Extraction failed (exit code {code}). Check whether the password is correct.', + 'log_root_failed': 'Failed to get root', + 'log_permission_failed': 'Failed to get permission', + 'log_permission_reboot_required': 'First permission attempt requires a reboot...', + 'log_permission_rebooting': 'Device will reboot; permission takes effect after reboot', + 'log_reboot_failed': 'Reboot failed', + 'log_permission_ok': 'Permission granted', + 'log_flash_readonly': 'Click Get Root first, then try again', + 'log_flash_complete_count': 'Flashing complete, {count} language packages', + 'log_flash_effect_after_reboot': 'Language package flashed. Reboot the device when convenient.', + 'log_flash_partial': 'Partially flashed ({success}/{total})', + 'log_install_start': 'Installing {count} APKs...', + 'log_install_done_all': 'Installation complete: all {count} succeeded', + 'log_install_done_partial': 'Installation complete: {success}/{total} succeeded', + 'log_install_failed': 'Installation failed', + 'log_install_exception': 'Installation error: {error}', + 'log_install_success_item': '✓ {name}', + 'log_install_failed_item': '✗ {name}', + 'log_quick_lang_setting': 'Setting system language to: {language} ({locale})', + 'log_quick_lang_success': '✓ Language set to {language}', + 'log_quick_lang_failed': '✗ Language setting failed: {output}', + 'log_rebooting': 'Device rebooting...', + 'log_disable_cancelled': 'Disable OTA cancelled', + 'log_disable_success': 'System upgrade disabled', + 'log_disable_failed': 'Failed to disable system upgrade', + 'log_package_key_failed': 'package-key fetch failed', + 'log_package_extract_success': 'Resources ready', + 'log_package_extract_failed': 'Q07_package.bin extract test failed', + 'err_no_usable_apk': 'No usable APK found', + 'err_zero_apk': '0KB APK found: {files}', + 'err_push_failed': 'push failed: {error}', + 'err_cp_failed': 'cp failed: {error}', } } @@ -240,20 +456,24 @@ class ADKAPKGUI: self.base_dir = get_app_dir() self.adb = find_tool('adb.exe', 'adb') self.sz = find_tool('7za.exe') - self.package_file = find_resource("package.bin") + self.package_file = self._find_package_file() 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.debug_password_api_url = "https://api.changan.softwindy.cn/api/authorizations/verify-debug-mode-password" self.vin = None + self.vehicle_name = "" self.device_connected = False self._refreshing = False # 防止并发刷新 self.debug_mode = False # 调试模式 + atexit.register(self.cleanup_cache_on_exit) # 设置样式 self.setup_styles() self.setup_ui() + self.root.after(200, self.set_window_icon) self.center_window() # 检查环境 @@ -261,6 +481,38 @@ class ADKAPKGUI: # 启动设备状态监控 self.start_device_monitor() + + def set_window_icon(self): + """Set the Tk window/taskbar icon at runtime; PyInstaller --icon only sets the exe file icon.""" + try: + icon_path = find_resource("app.ico") + if icon_path.exists(): + self.root.iconbitmap(str(icon_path)) + self._set_windows_hwnd_icon(icon_path) + except Exception: + pass + + def _set_windows_hwnd_icon(self, icon_path): + if sys.platform != 'win32': + return + try: + import ctypes + user32 = ctypes.windll.user32 + hwnd = self.root.winfo_id() + image_icon = 1 + lr_loadfromfile = 0x00000010 + wm_seticon = 0x0080 + icon_small = 0 + icon_big = 1 + path = str(icon_path) + small = user32.LoadImageW(None, path, image_icon, 16, 16, lr_loadfromfile) + big = user32.LoadImageW(None, path, image_icon, 32, 32, lr_loadfromfile) + if small: + user32.SendMessageW(hwnd, wm_seticon, icon_small, small) + if big: + user32.SendMessageW(hwnd, wm_seticon, icon_big, big) + except Exception: + pass def setup_styles(self): """设置自定义样式""" @@ -282,6 +534,7 @@ class ADKAPKGUI: def setup_ui(self): """设置UI界面""" # 配置根窗口 + self.root.title(self.t('title')) self.root.configure(bg=self.colors['bg_dark']) # 创建主框架 @@ -289,7 +542,7 @@ class ADKAPKGUI: 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 = tk.Frame(main_frame, bg=self.colors['bg_dark'], height=45) title_frame.pack(fill=tk.X, pady=(0, 10)) title_frame.pack_propagate(False) @@ -301,13 +554,6 @@ class ADKAPKGUI: bg=self.colors['bg_dark']) self.title_label.pack() - self.subtitle_label = tk.Label(title_frame, - text="宜宾科宜科技有限公司 - 智能设备管理平台", - font=('Microsoft YaHei', 9), - fg=self.colors['text_secondary'], - bg=self.colors['bg_dark']) - self.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) @@ -377,6 +623,13 @@ class ADKAPKGUI: bg=self.colors['error'], **btn_params) self.btn_exit.pack(side=tk.LEFT, padx=4) + + self.debug_button_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + self.btn_debug_extract = tk.Button(self.debug_button_frame, text=self.t('btn_debug_extract'), + command=self.debug_test_package_extract, + bg=self.colors['info'], + **btn_params) + self.btn_debug_extract.pack(side=tk.LEFT, padx=4) # 设备状态栏(横条) status_bar_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) @@ -397,7 +650,7 @@ class ADKAPKGUI: bg=self.colors['bg_light']) self.device_label.pack(side=tk.LEFT, padx=(5, 3)) - self.device_status_label = tk.Label(status_indicator_frame, text="未检测", + self.device_status_label = tk.Label(status_indicator_frame, text=self.t('status_detecting'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light'], @@ -412,7 +665,7 @@ class ADKAPKGUI: fg=self.colors['text'], bg=self.colors['bg_light']) self.vin_label_title.pack(side=tk.LEFT) - self.vin_label = tk.Label(vin_frame, text="未获取", + self.vin_label = tk.Label(vin_frame, text=self.t('vin_none'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light'], @@ -427,7 +680,7 @@ class ADKAPKGUI: fg=self.colors['text'], bg=self.colors['bg_light']) self.auth_label_title.pack(side=tk.LEFT) - self.auth_label = tk.Label(auth_frame, text="未验证", + self.auth_label = tk.Label(auth_frame, text=self.t('auth_none'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light'], @@ -448,21 +701,19 @@ class ADKAPKGUI: 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. 部分语言需要重启后生效,可以一切工作完成以后再重启。", - ] + tips = [self.t('tip_1'), self.t('tip_2'), self.t('tip_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, + label = 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) + justify=tk.LEFT) + label.pack(side=tk.LEFT) + setattr(self, f'tip_label_{i + 1}', label) # 解压进度条框架 progress_frame = tk.Frame(main_frame, bg=self.colors['bg_dark']) @@ -542,16 +793,7 @@ class ADKAPKGUI: 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'), @@ -564,6 +806,7 @@ class ADKAPKGUI: # 调试模式快捷键 self.root.bind('', self._toggle_debug) self.root.bind('', self._debug_test_extract) + self.root.protocol("WM_DELETE_WINDOW", self.on_close) # 绑定悬停效果 self.bind_hover_effects() @@ -572,7 +815,8 @@ class ADKAPKGUI: """绑定按钮悬停效果""" 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] + self.btn_reboot, self.btn_clear, self.btn_exit, + self.btn_debug_extract] for btn in buttons: original_bg = btn.cget('bg') @@ -615,57 +859,38 @@ class ADKAPKGUI: def _adb_cmd(self): return subprocess.list2cmdline([self.adb]) + def _find_package_file(self): + q07_package = find_resource("Q07_package.bin") + if q07_package.exists(): + return q07_package + legacy_package = find_resource("package.bin") + if legacy_package.exists(): + return legacy_package + return q07_package + def t(self, key): """获取翻译文本""" return self.T.get(self.lang, self.T['zh']).get(key, key) + def tf(self, key, **kwargs): + try: + return self.t(key).format(**kwargs) + except Exception: + return self.t(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']) - # 更新 ttk 样式 - 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') + self.log(self.t('log_lang_switched'), "INFO") def _refresh_ui_texts(self): """刷新所有UI文本""" t = self.t + self.root.title(t('title')) 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), @@ -674,24 +899,37 @@ class ADKAPKGUI: (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_debug_extract', None), 'btn_debug_extract', 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), + (getattr(self, 'tip_label_1', None), 'tip_1', None), + (getattr(self, 'tip_label_2', None), 'tip_2', None), + (getattr(self, 'tip_label_3', None), 'tip_3', 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')) + if getattr(self, 'status_text', None): + self.status_text.config(text=t('status_debug') if self.debug_mode else t('status_ready')) 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)) + self._update_device_status_impl( + self.device_connected, + self.vin, + getattr(self, '_last_authorized', False) + ) + + def set_debug_buttons_visible(self, visible): + if not hasattr(self, 'debug_button_frame'): + return + if visible: + self.debug_button_frame.pack(pady=(0, 8)) + else: + self.debug_button_frame.pack_forget() def _log_impl(self, message, level="INFO"): """日志写入的实际实现(必须在主线程调用)""" @@ -707,7 +945,7 @@ class ADKAPKGUI: def clear_log(self): """清空日志""" self.log_text.delete(1.0, tk.END) - self.log("日志已清空", "INFO") + self.log(self.t('info_log_cleared'), "INFO") def _show_progress_impl(self, show=True, is_push=False): """显示/隐藏进度条的实际实现(必须在主线程调用)""" @@ -782,7 +1020,7 @@ class ADKAPKGUI: if self.debug_mode: return True if not self.device_connected: - messagebox.showwarning("设备未连接", "请先连接设备并点击「检查」按钮刷新状态!") + messagebox.showwarning(self.t('warn_no_device'), self.t('warn_connect_first')) return False return True @@ -801,7 +1039,7 @@ class ADKAPKGUI: elif not devices and self.device_connected: # 设备断开连接 self.update_device_status(False) - self.log("设备已断开连接", "WARNING") + self.log(self.t('log_device_disconnected'), "WARNING") time.sleep(5) except: @@ -820,7 +1058,7 @@ class ADKAPKGUI: # 执行 adb -d root ok_root, out_root = self.run_adb_command('adb -d root') if not ok_root: - self.log("获取 root 失败", "ERROR") + self.log(self.t('log_root_failed'), "ERROR") self.show_progress(False, is_push=False) return @@ -830,22 +1068,22 @@ class ADKAPKGUI: 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.log(self.t('log_permission_failed'), "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") + self.log(self.t('log_permission_reboot_required'), "INFO") ok, _ = self.run_adb_command('adb -d shell reboot') if ok: - self.log("设备即将重启,重启后权限生效", "INFO") + self.log(self.t('log_permission_rebooting'), "INFO") self.update_device_status(False) else: - self.log("重启失败", "ERROR") + self.log(self.t('log_reboot_failed'), "ERROR") else: - self.log("已获取权限", "SUCCESS") + self.log(self.t('log_permission_ok'), "SUCCESS") self.show_progress(False, is_push=False) @@ -858,7 +1096,7 @@ class ADKAPKGUI: if has_app or has_priv: ok, reason = self._validate_extracted_apks() if not ok: - self.log(f"已解压缓存无效: {reason}", "ERROR") + self.log(self.tf('log_cache_invalid', reason=reason), "ERROR") self._clear_extracted_cache() return False return has_app or has_priv @@ -870,13 +1108,13 @@ class ADKAPKGUI: if self.priv_apps_dir and self.priv_apps_dir.exists(): apks.extend(self.priv_apps_dir.glob("*.apk")) if not apks: - return False, "未找到可用 APK" + return False, self.t('err_no_usable_apk') zero_apks = [apk.name for apk in apks if apk.stat().st_size <= 0] if zero_apks: preview = ", ".join(zero_apks[:5]) suffix = "..." if len(zero_apks) > 5 else "" - return False, f"发现 0KB APK: {preview}{suffix}" + return False, self.tf('err_zero_apk', files=f"{preview}{suffix}") return True, "" def _clear_extracted_cache(self): @@ -886,6 +1124,13 @@ class ADKAPKGUI: self.apps_dir = None self.priv_apps_dir = None + def cleanup_cache_on_exit(self): + self._clear_extracted_cache() + + def on_close(self): + self.cleanup_cache_on_exit() + self.root.destroy() + def _format_extract_error(self, err_msg, return_code): text = (err_msg or "").lower() if any(marker in text for marker in ( @@ -895,14 +1140,14 @@ class ADKAPKGUI: "data error in encrypted file", "can not open encrypted archive", )): - return "解压密码错误,请重新确认 package.bin 密码" + return self.t('err_extract_wrong_password') if "data error" in text: - return "资源包数据错误,可能是密码错误或 package.bin 损坏" + return self.t('err_extract_data') if "headers error" in text or "unexpected end" in text: - return "资源包损坏或不完整,请检查 package.bin" + return self.t('err_extract_corrupt') if err_msg.strip(): - return f"解压失败: {err_msg.strip()[:300]}" - return f"解压失败 (返回码 {return_code}),请检查密码是否正确" + return self.tf('err_extract_failed', error=err_msg.strip()[:300]) + return self.tf('err_extract_failed_code', code=return_code) def _decode_7z_output(self, output): for enc in ('gbk', 'utf-8'): @@ -926,13 +1171,14 @@ class ADKAPKGUI: return False def _extract_with_7za_progress(self): - self.update_progress(0, 100, "Loading resources...") + self.update_progress(0, 100, self.t('progress_resource_loading')) 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(): + use_progress_switch = self._seven_zip_supports_progress_stream() + if use_progress_switch: cmd.extend(['-bsp1', '-bso0', '-bse1']) proc = subprocess.Popen( @@ -963,27 +1209,51 @@ class ADKAPKGUI: percent = min(100, int(matches[-1])) if percent != last_percent: last_percent = percent - self.update_progress(percent, 100, "Loading resources...") + self.update_progress( + percent, + 100, + self.tf('progress_extracting_percent', percent=percent) + ) return_code = proc.wait() decoded_output = self._decode_7z_output(bytes(output)) if return_code == 0: - self.update_progress(100, 100, "Resources loaded") + self.update_progress(100, 100, self.t('progress_resource_done')) + return True, decoded_output + if use_progress_switch and "incorrect command line" in decoded_output.lower(): + return self._extract_with_7za_basic() + return False, decoded_output + + def _extract_with_7za_basic(self): + cmd = [ + self.sz, 'x', str(self.package_file), + f'-p{self.extract_password}', + f'-o{self.temp_dir}', '-y' + ] + result = subprocess.run( + cmd, + capture_output=True, + stdin=subprocess.DEVNULL, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + decoded_output = self._decode_7z_output(result.stdout + result.stderr) + if result.returncode == 0: + self.update_progress(100, 100, self.t('progress_resource_done')) return True, decoded_output return False, decoded_output def extract_package_silent(self): - """Extract package.bin silently with progress.""" + """Extract Q07_package.bin silently with progress.""" if not self.package_file.exists(): - self.log(f"Error: package not found ({self.package_file})", "ERROR") + self.log(self.tf('log_package_missing', path=self.package_file), "ERROR") return False if not self.extract_password: - self.log("Error: extract password is not set", "ERROR") + self.log(self.t('log_extract_password_missing'), "ERROR") return False if not os.path.exists(self.sz): - self.log(f"Error: 7za.exe not found ({self.sz})", "ERROR") + self.log(self.tf('log_7za_missing', path=self.sz), "ERROR") return False try: @@ -1003,7 +1273,7 @@ class ADKAPKGUI: subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True) subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True) - self.log("正在准备资源包", "INFO") + self.log(self.t('info_extracting'), "INFO") ok, err_msg = self._extract_with_7za_progress() if not ok: @@ -1023,27 +1293,25 @@ class ADKAPKGUI: 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") + self.log(self.t('warn_no_app_dir'), "WARNING") self._clear_extracted_cache() 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 ok, reason = self._validate_extracted_apks() if not ok: - self.log(f"解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。", "ERROR") + self.log(self.tf('log_extracted_resource_invalid', reason=reason), "ERROR") self._clear_extracted_cache() return False - self.log(f"资源准备完成 (app: {apk_count}, priv-app: {priv_count})", "SUCCESS") + self.log(self.t('log_extract_done'), "SUCCESS") return True except Exception as e: if getattr(self, 'debug_mode', False): - self.log(f"Package preparation failed: {str(e)}", "ERROR") + self.log(self.tf('log_extract_exception', error=str(e)), "ERROR") import traceback self.log(traceback.format_exc(), "ERROR") else: - self.log("资源准备失败,请检查网络连接后重试", "ERROR") + self.log(self.t('err_extract_user'), "ERROR") self._clear_extracted_cache() return False @@ -1054,13 +1322,13 @@ class ADKAPKGUI: if result.returncode == 0: self.refresh_device_status() if not self.package_file.exists(): - self.log("未找到资源包文件", "WARNING") + self.log(self.tf('log_package_missing', path=self.package_file), "WARNING") else: self._try_reuse_extracted() else: - self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") + self.log(self.t('log_adb_missing'), "ERROR") except FileNotFoundError: - self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") + self.log(self.t('log_adb_missing'), "ERROR") def _try_reuse_extracted(self): """检查磁盘上是否已有解压好的资源,有则直接复用""" @@ -1089,7 +1357,7 @@ class ADKAPKGUI: self.temp_dir = cache_dir ok, reason = self._validate_extracted_apks() if not ok: - self.log(f"缓存资源无效,已清理: {reason}", "WARNING") + self.log(self.tf('log_cache_reuse_invalid', reason=reason), "WARNING") self._clear_extracted_cache() return # self.log("已复用缓存的资源文件", "INFO") @@ -1110,7 +1378,7 @@ class ADKAPKGUI: if devices: if not was_connected: - self.log("Device connected", "SUCCESS") + self.log(self.t('log_device_connected'), "SUCCESS") vin = '' for key in ('ca_vin_info', 'VIN'): @@ -1122,18 +1390,18 @@ class ADKAPKGUI: break vin = '' if vin: - self.log(f"Current VIN: {vin}", "INFO") + self.log(self.tf('log_current_vin', vin=vin), "INFO") authorized = self.check_authorization(vin) self.update_device_status(True, vin, authorized) else: - self.log("Unable to read VIN", "WARNING") + self.log(self.t('log_vin_unavailable'), "WARNING") self.update_device_status(True, None, False) else: if was_connected: - self.log("Device disconnected", "WARNING") + self.log(self.t('log_device_disconnected'), "WARNING") self.update_device_status(False) except Exception as e: - self.log(f"Refresh device status failed: {str(e)}", "ERROR") + self.log(self.tf('log_refresh_failed', error=str(e)), "ERROR") finally: self._refreshing = False @@ -1142,38 +1410,58 @@ class ADKAPKGUI: def check_authorization(self, vin): """Check authorization.""" if getattr(self, 'debug_mode', False): - self.log("Debug mode: skip authorization", "WARNING") + self.log(self.t('log_auth_skip_debug'), "WARNING") return True - self.log("Checking authorization...", "INFO") + self.log(self.t('log_auth_checking'), "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("Authorization passed", "SUCCESS") - if 'data' in data and 'vehicleName' in data['data']: - self.log(f"Vehicle name: {data['data']['vehicleName']}", "INFO") + authorized, vehicle_name, _ = self.query_authorization_info(vin) + if authorized: + self.log(self.t('log_auth_ok'), "SUCCESS") + if vehicle_name: + self.vehicle_name = vehicle_name + self.log(self.tf('log_vehicle_name', name=vehicle_name), "INFO") return True else: - self.log("Authorization failed", "ERROR") + self.log(self.t('log_auth_failed'), "ERROR") return False except Exception: - self.log("Authorization failed", "ERROR") + self.log(self.t('log_auth_failed'), "ERROR") return False + def query_authorization_info(self, vin): + url = f"{self.api_url}?{urlencode({'vin': vin})}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + payload = data.get('data', {}) if isinstance(data, dict) else {} + vehicle_name = payload.get('vehicleName') or payload.get('vehicle_name') or "" + vehicle_name = str(vehicle_name).strip() + if data.get('authorized') is True and vehicle_name: + self.vehicle_name = vehicle_name + return data.get('authorized') is True, vehicle_name, data + def fetch_package_password(self): """Fetch package password from server.""" if not self.vin: - self.log("Please connect adb first", "ERROR") + self.log(self.t('log_adb_required'), "ERROR") return False try: + vehicle_name = self.vehicle_name + if not vehicle_name: + authorized, vehicle_name, _ = self.query_authorization_info(self.vin) + if not authorized: + self.log(self.t('log_auth_failed'), "ERROR") + return False + if not vehicle_name: + self.log(self.t('log_auth_failed'), "ERROR") + return False + pwd_api_url = "https://api.changan.softwindy.cn/api/authorizations/package-key" - url = f"{pwd_api_url}?{urlencode({'vin': self.vin})}" + url = f"{pwd_api_url}?{urlencode({'vin': self.vin, 'vehicleName': vehicle_name})}" req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) with urlopen(req, timeout=10) as response: @@ -1183,11 +1471,11 @@ class ADKAPKGUI: self.extract_password = data['data']['password'] return True else: - self.log(f"Data preparation failed: {data.get('message', 'unknown error')}", "ERROR") + self.log(self.tf('log_data_prepare_failed_detail', error=data.get('message', self.t('unknown_error'))), "ERROR") return False except Exception as e: - self.log(f"Data preparation failed: {str(e)}", "ERROR") + self.log(self.tf('log_data_prepare_failed_detail', error=str(e)), "ERROR") return False def run_adb_command(self, command): @@ -1219,13 +1507,13 @@ class ADKAPKGUI: ok, err = self.run_adb_command(f'adb -d push "{apk_path}" {temp_apk_path}') if not ok: - return False, f"push失败: {err}" + return False, self.tf('err_push_failed', error=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 False, self.tf('err_cp_failed', error=err) return True, "" @@ -1233,36 +1521,31 @@ class ADKAPKGUI: """推送APK到系统分区(支持app和priv-app)""" if not self.check_device_connection(): return - if not self.vin: - messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码") + if not self.vin and not self.debug_mode: + messagebox.showwarning(self.t('msg_warn_title'), self.t('warn_no_vin')) return - messagebox.showwarning("⚠️ 重要提示", - "刷入过程中请勿:\n" - " ● 重启车机\n" - " ● 退出本程序\n" - " ● 关闭电脑\n\n" - "否则可能导致车机系统损坏!") + messagebox.showwarning(self.t('warn_flash_warning'), self.t('warn_flash_msg')) def do_push_all(): if not self.check_authorization(self.vin): - self.run_on_ui_thread(lambda: messagebox.showerror("授权失败", "设备未授权")) + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized'))) return if not self.extract_password: if not self.fetch_package_password(): - self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源准备失败!")) + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_prepare_failed'))) 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("错误", "资源准备失败!")) + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_prepare_failed'))) return self.show_progress(False, is_push=False) if (not self.apps_dir or not self.apps_dir.exists()) and \ (not self.priv_apps_dir or not self.priv_apps_dir.exists()): - self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源目录未找到")) + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_dir_missing'))) return self.show_progress(True, is_push=True) @@ -1282,7 +1565,7 @@ class ADKAPKGUI: 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.log(self.t('warn_no_apk'), "WARNING") self.show_progress(False, is_push=True) return # 重新收集 @@ -1294,7 +1577,7 @@ class ADKAPKGUI: for apk in self.priv_apps_dir.glob("*.apk"): all_apks.append((apk, "priv-app")) if not all_apks: - self.log("未找到语言包文件", "WARNING") + self.log(self.t('warn_no_apk'), "WARNING") self.show_progress(False, is_push=True) return @@ -1308,20 +1591,20 @@ class ADKAPKGUI: success_count += 1 else: if "Read-only file system" in err: - self.log("请先点击「获取权限」获取权限后再试", "ERROR") + self.log(self.t('log_flash_readonly'), "ERROR") aborted = True break - self.update_progress(i, total, "正在刷入...", is_push=True) + self.update_progress(i, total, self.t('progress_flashing'), is_push=True) - self.update_progress(total, total, "刷入完成" if not aborted else "已终止", is_push=True) + self.update_progress(total, total, self.t('progress_flash_done') if not aborted else self.t('progress_aborted'), is_push=True) if success_count == total: - self.log(f"刷入完成,共 {total} 个语言包", "SUCCESS") - self.log("语言包已刷入完成,重启设备后生效,您可在适当时候重启", "WARNING") + self.log(self.tf('log_flash_complete_count', count=total), "SUCCESS") + self.log(self.t('log_flash_effect_after_reboot'), "WARNING") elif success_count > 0: - self.log(f"部分刷入成功({success_count}/{total})", "WARNING") + self.log(self.tf('log_flash_partial', success=success_count, total=total), "WARNING") if not aborted: - self.log("语言包已刷入完成,重启设备后生效,您可在适当时候重启", "WARNING") + self.log(self.t('log_flash_effect_after_reboot'), "WARNING") self.show_progress(False, is_push=True) @@ -1332,48 +1615,50 @@ class ADKAPKGUI: if not self.check_device_connection(): return - apk_dir = filedialog.askdirectory(title="选择包含APK文件的文件夹") + apk_dir = filedialog.askdirectory(title=self.t('dialog_select_apk_folder')) if not apk_dir: return apk_files = list(Path(apk_dir).glob("*.apk")) if not apk_files: - messagebox.showerror("错误", "所选文件夹中没有APK文件!") + messagebox.showerror(self.t('msg_error_title'), self.t('msg_no_apk_in_folder')) return - result = messagebox.askyesno("确认安装", - f"找到 {len(apk_files)} 个APK文件\n\n是否开始批量安装?") + result = messagebox.askyesno( + self.t('msg_confirm_install_title'), + self.tf('msg_confirm_install_folder', count=len(apk_files)) + ) if not result: return def install(): self.show_progress(True, is_push=True) total = len(apk_files) - self.log(f"开始批量安装 {total} 个APK...", "INFO") + self.log(self.tf('log_install_start', count=total), "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) + self.update_progress(i, total, self.t('progress_installing'), 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) + self.update_progress(total, total, self.t('progress_done'), is_push=True) if success_count == total: - self.log(f"安装完成:全部 {total} 个成功", "SUCCESS") - self.run_on_ui_thread(messagebox.showinfo, "安装完成", f"成功安装 {total} 个APK!") + self.log(self.tf('log_install_done_all', count=total), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('info_install_done'), self.tf('msg_install_success_many', count=total)) 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}") + self.log(self.tf('log_install_done_partial', success=success_count, total=total), "WARNING") + self.run_on_ui_thread(messagebox.showwarning, self.t('info_install_done'), self.tf('msg_install_partial', success=success_count, failed=total - success_count)) else: - self.log("安装失败", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "安装失败", "所有APK安装失败!") + self.log(self.t('log_install_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('log_install_failed'), self.t('msg_install_all_failed')) except Exception as e: - self.log(f"安装过程异常: {str(e)}", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "安装失败", f"安装过程异常:{str(e)}") + self.log(self.tf('log_install_exception', error=str(e)), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('log_install_failed'), self.tf('msg_install_exception', error=str(e))) finally: self.run_adb_command('adb -d shell setprop vecentek.model 0') self.show_progress(False, is_push=True) @@ -1387,8 +1672,8 @@ class ADKAPKGUI: return file_path = filedialog.askopenfilename( - title="选择APK文件", - filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + title=self.t('dialog_select_apk'), + filetypes=[(self.t('file_apk'), "*.apk"), (self.t('file_all'), "*.*")] ) if not file_path: @@ -1396,17 +1681,17 @@ class ADKAPKGUI: def install(): self.show_progress(True, is_push=True) - self.update_progress(50, 100, f"安装中", is_push=True) + self.update_progress(50, 100, self.t('progress_installing'), 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) + self.update_progress(100, 100, self.t('progress_done'), is_push=True) if success: - self.log("✓ 安装成功", "SUCCESS") + self.log(self.t('info_install_done'), "SUCCESS") else: - self.log("✗ 安装失败", "ERROR") + self.log(self.t('log_install_failed'), "ERROR") except Exception as e: - self.log(f"安装过程异常: {str(e)}", "ERROR") + self.log(self.tf('log_install_exception', error=str(e)), "ERROR") finally: self.run_adb_command('adb -d shell setprop vecentek.model 0') self.show_progress(False, is_push=True) @@ -1427,7 +1712,7 @@ class ADKAPKGUI: # 创建弹窗 popup = tk.Toplevel(self.root) - popup.title("快捷语言设置") + popup.title(self.t('title_pop_lang')) popup.geometry("520x320") popup.configure(bg=self.colors['bg_dark']) popup.resizable(False, False) @@ -1441,13 +1726,13 @@ class ADKAPKGUI: popup.grab_set() # 标题 - header = tk.Label(popup, text="选择目标语言", + header = tk.Label(popup, text=self.t('quick_lang_header'), font=('Microsoft YaHei', 13, 'bold'), fg=self.colors['accent'], bg=self.colors['bg_dark']) header.pack(pady=(15, 10)) - hint = tk.Label(popup, text="点击按钮即可将系统语言切换为对应语言,重启后生效", + hint = tk.Label(popup, text=self.t('quick_lang_hint'), font=('Microsoft YaHei', 9), fg=self.colors['text_secondary'], bg=self.colors['bg_dark']) @@ -1496,7 +1781,7 @@ class ADKAPKGUI: 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="⚙️ 打开系统语言设置(手动选择)", + sys_btn = tk.Button(popup, text=self.t('quick_lang_system'), command=lambda: self._open_sys_and_close(popup), font=('Microsoft YaHei', 9), fg=self.colors['text_secondary'], @@ -1510,21 +1795,21 @@ class ADKAPKGUI: popup.destroy() def do_set(): - self.log(f"正在设置系统语言为: {language_name} ({locale_code})", "INFO") + self.log(self.tf('log_quick_lang_setting', language=language_name, locale=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.log(self.tf('log_quick_lang_success', language=language_name), "SUCCESS") self.run_on_ui_thread( messagebox.showinfo, - "设置成功", - f"系统语言已设置为 {language_name}\n\n⚠️ 请重启设备使其生效。" + self.t('msg_success_title'), + self.tf('msg_quick_lang_success', language=language_name) ) else: - self.log(f"✗ 语言设置失败: {output}", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "设置失败", f"语言设置失败!\n\n{output}") + self.log(self.tf('log_quick_lang_failed', output=output), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.tf('msg_quick_lang_failed', output=output)) threading.Thread(target=do_set, daemon=True).start() @@ -1549,10 +1834,10 @@ class ADKAPKGUI: """重启设备""" if not self.check_device_connection(): return - if messagebox.askyesno("确认重启", "确定要重启设备吗?"): + if messagebox.askyesno(self.t('msg_reboot_confirm_title'), self.t('confirm_reboot')): subprocess.Popen(f'{self._adb_cmd()} -d shell reboot', shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - self.log("设备正在重启...", "INFO") + self.log(self.t('log_rebooting'), "INFO") self.update_device_status(False) def on_disable_upgrade(self): @@ -1563,15 +1848,12 @@ class ADKAPKGUI: # 弹窗确认 result = messagebox.askyesno( - "确认禁用升级", - "⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n" - "是否确定要禁用系统升级应用?\n\n" - "禁用命令:\n" - "adb -d shell pm disable-user --user 0 com.incall.apps.softmanager" + self.t('msg_disable_confirm_title'), + self.t('msg_disable_confirm') ) if not result: - self.log("已取消禁用升级操作", "INFO") + self.log(self.t('log_disable_cancelled'), "INFO") return def disable(): @@ -1579,11 +1861,11 @@ class ADKAPKGUI: 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, "成功", "系统升级已成功禁用!") + self.log(self.t('log_disable_success'), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('msg_success_title'), self.t('msg_disable_success')) else: - self.log("禁用系统升级失败", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "错误", f"禁用失败:{output}") + self.log(self.t('log_disable_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.tf('msg_disable_failed', output=output)) self.show_progress(False, is_push=False) threading.Thread(target=disable, daemon=True).start() @@ -1592,68 +1874,125 @@ class ADKAPKGUI: """切换调试模式(隐藏入口,Ctrl+Shift+D)""" if self.debug_mode: self.debug_mode = False - self.log("调试模式已关闭", "WARNING") + self.log(self.t('log_debug_off'), "WARNING") self.status_text.config(text=self.t('status_ready')) + self.set_debug_buttons_visible(False) 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("错误", "密码错误") + pwd = simpledialog.askstring(self.t('status_debug'), self.t('debug_password_prompt'), show='*', parent=self.root) + if not pwd: + return + + self.log(self.t('debug_password_verifying'), "INFO") + + def verify(): + valid, message = self.verify_debug_mode_password(pwd) + if valid: + def enable_debug(): + self.debug_mode = True + self.update_device_status(True, "", True) + self.log(self.t('log_debug_on'), "WARNING") + self.status_text.config(text=self.t('status_debug')) + self.set_debug_buttons_visible(True) + self.run_on_ui_thread(enable_debug) + else: + def show_failed(): + msg = message or self.t('debug_wrong_password') + self.log(self.tf('debug_verify_failed', message=msg), "WARNING") + messagebox.showwarning(self.t('msg_error_title'), msg) + self.run_on_ui_thread(show_failed) + + threading.Thread(target=verify, daemon=True).start() + + def verify_debug_mode_password(self, password): + try: + payload = json.dumps({"password": password}).encode('utf-8') + req = Request( + self.debug_password_api_url, + data=payload, + method='POST', + headers={ + 'Content-Type': 'application/json', + 'User-Agent': 'Mozilla/5.0', + } + ) + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + if data.get('success') is True and data.get('valid') is True: + return True, data.get('message', '') + return False, data.get('message') or self.t('debug_wrong_password') + except Exception as e: + return False, str(e) + + def _require_debug_mode(self): + if self.debug_mode: + return True + messagebox.showwarning(self.t('status_debug'), self.t('debug_need_enable')) + return False def install_apps(self): """安装App — 支持单选或多选APK文件""" if not self.check_device_connection(): return + if not self.vin and not self.debug_mode: + messagebox.showwarning(self.t('msg_warn_title'), self.t('warn_no_vin')) + return file_paths = filedialog.askopenfilenames( - title="选择APK文件", - filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + title=self.t('dialog_select_apk'), + filetypes=[(self.t('file_apk'), "*.apk"), (self.t('file_all'), "*.*")] ) if not file_paths: return count = len(file_paths) - result = messagebox.askyesno("确认安装", f"已选择 {count} 个APK文件\n\n是否开始安装?") + result = messagebox.askyesno( + self.t('msg_confirm_install_title'), + self.tf('msg_confirm_install_many', count=count) + ) if not result: return def install(): + if not self.check_authorization(self.vin): + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_auth_failed_title'), + self.t('msg_device_unauthorized') + ) + return + self.show_progress(True, is_push=True) - self.log(f"开始安装 {count} 个APK...", "INFO") + self.log(self.tf('log_install_start', count=count), "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) + apk_name = Path(file_path).name + self.update_progress(i, count, self.tf('progress_installing_name', name=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") + self.log(self.tf('log_install_success_item', name=apk_name), "SUCCESS") success_count += 1 else: - self.log(f"✗ {apk_name}.apk", "ERROR") + self.log(self.tf('log_install_failed_item', name=apk_name), "ERROR") - self.update_progress(count, count, "安装完成", is_push=True) + self.update_progress(count, count, self.t('progress_done'), is_push=True) if success_count == count: - self.log(f"安装完成:全部 {count} 个成功", "SUCCESS") - self.run_on_ui_thread(messagebox.showinfo, "安装完成", f"成功安装 {count} 个APK!") + self.log(self.tf('log_install_done_all', count=count), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('info_install_done'), self.tf('msg_install_success_many', count=count)) 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}") + self.log(self.tf('log_install_done_partial', success=success_count, total=count), "WARNING") + self.run_on_ui_thread(messagebox.showwarning, self.t('info_install_done'), self.tf('msg_install_partial', success=success_count, failed=count - success_count)) else: - self.log("安装失败", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "安装失败", "所有APK安装失败!") + self.log(self.t('log_install_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('log_install_failed'), self.t('msg_install_all_failed')) except Exception as e: - self.log(f"安装过程异常: {str(e)}", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "安装失败", f"安装过程异常:{str(e)}") + self.log(self.tf('log_install_exception', error=str(e)), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('log_install_failed'), self.tf('msg_install_exception', error=str(e))) finally: self.run_adb_command('adb -d shell setprop vecentek.model 0') self.show_progress(False, is_push=True) @@ -1661,30 +2000,34 @@ class ADKAPKGUI: threading.Thread(target=install, daemon=True).start() def _debug_test_extract(self, event=None): - """Debug-only package extraction test.""" - if not getattr(self, 'debug_mode', False): - messagebox.showwarning("Debug mode", "Press Ctrl+Shift+D to enable debug mode first") + self.debug_test_package_extract() + + def debug_test_package_extract(self): + """Debug-only package extraction test using package-key.""" + if not self._require_debug_mode(): return - pwd = simpledialog.askstring("Test extraction", "Enter package.bin password:", show='*', parent=self.root) - if not pwd: + if not self.vin: + vin = simpledialog.askstring(self.t('status_debug'), 'VIN:', parent=self.root) + if vin: + self.vin = vin.strip().upper() + if not self.vin: + messagebox.showwarning(self.t('status_debug'), self.t('debug_need_vin')) return def do_extract(): old_password = self.extract_password - self.extract_password = pwd try: + self.log(self.t('info_extracting'), "INFO") + self.extract_password = None + if not self.fetch_package_password(): + self.log(self.t('log_package_key_failed'), "ERROR") + return self.show_progress(True, is_push=False) if self.extract_package_silent(): - self.log("Test extraction succeeded", "SUCCESS") - self.run_on_ui_thread( - messagebox.showinfo, - "Test extraction succeeded", - f"Resources extracted to:\n{self.temp_dir}" - ) + self.log(self.t('log_package_extract_success'), "SUCCESS") else: - self.log("Test extraction failed", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "Test extraction failed", "Check the 7za output in logs") + self.log(self.t('log_package_extract_failed'), "ERROR") finally: self.extract_password = old_password self.show_progress(False, is_push=False) diff --git a/Q07/app.ico b/Q07/app.ico new file mode 100644 index 0000000..1b655d0 Binary files /dev/null and b/Q07/app.ico differ diff --git a/Q07/pack.bat b/Q07/pack.bat deleted file mode 100644 index 819bae4..0000000 --- a/Q07/pack.bat +++ /dev/null @@ -1,95 +0,0 @@ -@echo off -chcp 65001 >nul -cd /d "%~dp0" -set "ROOT=%~dp0.." -set "TOOLS=%ROOT%\tools" -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 "%TOOLS%\adb.exe" . >nul -copy "%TOOLS%\AdbWinApi.dll" . >nul -copy "%TOOLS%\AdbWinUsbApi.dll" . >nul -copy "%TOOLS%\7za.exe" . >nul -if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul - -echo [5/6] PyInstaller... -%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 -echo [INFO] Normal PyInstaller... -%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 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/Q07/pack_q07.bat b/Q07/pack_q07.bat index f0feb85..40d7794 100644 --- a/Q07/pack_q07.bat +++ b/Q07/pack_q07.bat @@ -3,8 +3,9 @@ chcp 65001 >nul cd /d "%~dp0" set "ROOT=%~dp0.." set "TOOLS=%ROOT%\tools" -set NAME=启源Q07刷入工具 -set SRC=app.py +set "ICON=%~dp0app.ico" +set NAME=Qiyuan_Q07-multi-lan-installer +set SRC=Qiyuan_Q07-multi-lan-installer.py title %NAME% - Build echo ============================================================ @@ -27,14 +28,42 @@ if errorlevel 1 ( %PY% -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple ) -echo [2/6] Clean... +echo [2/7] 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... +echo [3/7] Checking resources... +if not exist "%ICON%" ( + echo [ERROR] app.ico not found in Q07 folder: %ICON% + pause + exit /b 1 +) +if not exist "%TOOLS%\adb.exe" ( + echo [ERROR] Missing ADB file: %TOOLS%\adb.exe + pause + exit /b 1 +) +if not exist "%TOOLS%\AdbWinApi.dll" ( + echo [ERROR] Missing ADB file: %TOOLS%\AdbWinApi.dll + pause + exit /b 1 +) +if not exist "%TOOLS%\AdbWinUsbApi.dll" ( + echo [ERROR] Missing ADB file: %TOOLS%\AdbWinUsbApi.dll + pause + exit /b 1 +) +if not exist "%TOOLS%\7za.exe" ( + echo [ERROR] Missing 7za file: %TOOLS%\7za.exe + pause + exit /b 1 +) + +echo [4/7] Cython compile... mkdir dist_cy 2>nul copy %SRC% dist_cy\_core.py >nul +set "PYD=" %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')" @@ -42,54 +71,50 @@ cd dist_cy %PY% setup_cython.py build_ext --inplace if errorlevel 1 ( cd .. - echo [WARN] Cython failed, fallback - goto :NORMAL + echo [ERROR] Cython build failed. Install Microsoft C++ Build Tools and retry. + pause + exit /b 1 ) for %%f in (_core*.pyd) do set PYD=%%f if "%PYD%"=="" ( cd .. - echo [WARN] No pyd, fallback - goto :NORMAL + echo [ERROR] Cython build did not generate _core*.pyd. Stop. + pause + exit /b 1 ) 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... +echo [5/7] Copy resources... copy "%TOOLS%\adb.exe" . >nul copy "%TOOLS%\AdbWinApi.dll" . >nul copy "%TOOLS%\AdbWinUsbApi.dll" . >nul copy "%TOOLS%\7za.exe" . >nul -if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul +copy "%ICON%" . >nul -echo [5/6] PyInstaller... -%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 +echo [6/7] PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ICON%" --add-data "%ICON%;." --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 + exit /b 1 ) -echo [6/6] Cleanup... +echo [7/7] 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 -echo [INFO] Normal PyInstaller... -%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 ) diff --git a/README.md b/README.md index f703d7f..70050f8 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,24 @@ -# language_installer - -长安语言刷入工具套件,适用于启源Q07、深蓝S05、X5plus、长安逸动、UNI-Z、Mazda-EZ60 等 Android 车机多语言 APK 刷入/推送场景。 - -## 常用入口 - -| 文件 | 用途 | 打包脚本 | -|------|------|----------| -| `Q07/app.py` | 启源Q07 | `Q07/pack_q07.bat` | -| `S05/S05.py` | 深蓝S05原版 | `S05/pack_s05.bat` | -| `S05/S05_fixed.py` | 深蓝S05修复版 | `S05/pack_s05_fixed.bat` | +# language_installer + +长安语言刷入工具套件,适用于启源Q07、深蓝S05、X5plus、长安逸动、UNI-Z、Mazda-EZ60 等 Android 车机多语言 APK 刷入/推送场景。 + +## 常用入口 + +| 文件 | 用途 | 打包脚本 | +|------|------|----------| +| `Q07/app.py` | 启源Q07 | `Q07/pack_q07.bat` | +| `S05/S05.py` | 深蓝S05原版 | `S05/pack_s05.bat` | +| `S05/S05_fixed.py` | 深蓝S05修复版 | `S05/pack_s05_fixed.bat` | | `X5plus/X5plusTool.py` | X5plus | `X5plus/pack_x5plus.bat` | -| `Yidong/app-install.py` | 长安逸动通用 | `Yidong/pack_common.bat` | +| `CS55-Q05/CS55-Q05_Installer.py` | CS55Plus/Q05 通用 | `CS55-Q05/pack_cs55_q05.bat` | +| `CS75Pro/CS75Pro_Installer.py` | CS75Pro | `CS75Pro/pack_cs75pro.bat` | | `Yidong/app-yidong.py` | 长安逸动 | `Yidong/pack_yidong.bat` | -| `UNIZ/UNIZ.py` | UNI-Z 文件推送 | `UNIZ/pack_uniz.bat` | -| `Mazda-EZ60/Mazda-EZ60.py` | Mazda-EZ60 OS 1.2 | `Mazda-EZ60/pack_mazda_ez60.bat` | -| `A07/Qiyuan_A07_Multi-lan-installer.py` | 启源A07 | `A07/pack_a07.bat` | -| `Q05-Lidar/Q05-Lidar_Installer.py` | Q05_Lidar | `Q05-Lidar/pack_q05_lidar.bat` | - -公共工具如 `adb.exe`、`fastboot.exe`、`7za.exe` 及相关 DLL 统一放在根目录 `tools/` 管理;`app.ico`、`package.bin` 保留在仓库根目录。各车型脚本会自动回退查找这些资源。 - -详细维护说明见 `AGENTS.md` / `CLAUDE.md`。 +| `UNIZ/UNIZ.py` | UNI-Z 文件推送 | `UNIZ/pack_uniz.bat` | +| `Mazda-EZ60/Mazda-EZ60_1.2.py` | Mazda-EZ60 OS 1.2 | `Mazda-EZ60/pack_mazda_ez60_1.2.bat` | +| `Mazda-EZ60/Mazda_EZ60-Language-Install_v1.0.py` | Mazda-EZ60 OS 1.0 | `Mazda-EZ60/pack_mazda_ez60_1.0.bat` | +| `A07/Qiyuan_A07_Multi-lan-installer.py` | 启源A07 | `A07/pack_a07.bat` | +| `Q05-Lidar/Q05-Lidar_Installer.py` | Q05_Lidar | `Q05-Lidar/pack_q05_lidar.bat` | + +公共工具如 `adb.exe`、`fastboot.exe`、`7za.exe` 及相关 DLL 统一放在根目录 `tools/` 管理;`app.ico`、`package.bin` 保留在仓库根目录。各车型脚本会自动回退查找这些资源。 + +详细维护说明见 `AGENTS.md` / `CLAUDE.md`。 diff --git a/S05/S05_fixed.py b/S05/Deepal_S05.py similarity index 53% rename from S05/S05_fixed.py rename to S05/Deepal_S05.py index 19f69a1..9ce1290 100644 --- a/S05/S05_fixed.py +++ b/S05/Deepal_S05.py @@ -7,6 +7,7 @@ import subprocess import json import re import threading +import atexit import tkinter as tk from tkinter import ttk, scrolledtext, filedialog, messagebox, simpledialog from pathlib import Path @@ -23,6 +24,17 @@ import shutil import time +def set_windows_app_id(): + if sys.platform != 'win32': + return + try: + import ctypes + app_id = 'DeepalS05.LanguageInstaller.1.0' + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id) + except Exception: + pass + + def get_app_dir(): return Path(sys.executable).resolve().parent if getattr(sys, 'frozen', False) else Path(__file__).resolve().parent @@ -59,13 +71,16 @@ def find_tool(file_name, fallback=None): path = find_resource(file_name) if path.exists(): return str(path) - return fallback or str(path) + return fallback or str(path) class ADKAPKGUI: def __init__(self): + set_windows_app_id() self.root = tk.Tk() - self.root.title("长安语言安装工具") + self.root.title("Deepal S05") self.root.geometry("650x640") self.root.resizable(True, True) + self.set_window_icon() + self.root.after(200, self.set_window_icon) # 设置颜色主题 self.colors_dark = { @@ -124,17 +139,160 @@ class ADKAPKGUI: 'auth_yes': '已授权', 'auth_no': '未授权', 'btn_refresh': '🔄 检查', - 'hint_factory': '🔧 工厂模式:拨号 *#*#888 ,密码:369875', + 'hint_factory': '🔧 断开车机网络,拨号获取到的密码进入工厂模式。', 'theme_dark': '🌙 暗色', 'theme_light': '☀️ 亮色', 'lang_zh': '中', - 'lang_en': 'EN', + 'lang_en': 'English', 'switch_lang': '语言 / Language', 'switch_theme': '切换主题', - 'about_company': '宜宾科宜科技有限公司 - 出口改装一站式服务', + 'pwd_query_label': '工程密码查询:', + 'vin_placeholder': '请输入VIN', + 'btn_query_pwd': '查询密码', + 'pwd_empty': '', + 'pwd_success': '密码: *#{password}#*', + 'pwd_failed': '失败: {message}', + 'pwd_request_failed': '请求失败', + 'hint_lines': [ + '1. 安装语言过程中请保持车辆和电脑电量充足,不可中途停止。', + '2. 获取权限以后,车辆自动重启以后再进入语言刷入。', + '3. 部分语言需要重启后生效,可以一切工作完成以后再重启。', + ], + 'msg_warn_title': '警告', + 'msg_error_title': '错误', + 'msg_success_title': '成功', + 'msg_hint_title': '提示', + 'msg_device_not_connected_title': '设备未连接', + 'msg_device_not_connected': '请先连接设备并点击「检查」按钮刷新状态!', + 'msg_need_vin': '请先刷新设备状态并获取VIN码', + 'msg_auth_failed_title': '授权失败', + 'msg_device_unauthorized': '设备未授权', + 'msg_data_prepare_failed': '资源准备失败!', + 'msg_resource_dir_missing': '资源目录未找到', + 'msg_flash_warning_title': '⚠️ 重要提示', + 'msg_flash_warning': '刷入过程中请勿:\n ● 重启车机\n ● 退出本程序\n ● 关闭电脑\n\n否则可能导致车机系统损坏!', + 'msg_no_apks_in_folder': '所选文件夹中没有APK文件!', + 'msg_install_confirm_title': '确认安装', + 'msg_install_confirm_folder': '找到 {count} 个APK文件\n\n是否开始批量安装?', + 'msg_install_confirm_many': '已选择 {count} 个APK文件\n\n是否开始安装?', + 'msg_install_done_title': '安装完成', + 'msg_install_done_all': '成功安装 {count} 个APK!', + 'msg_install_partial_title': '部分成功', + 'msg_install_partial': '成功: {success}\n失败: {failed}', + 'msg_install_failed_title': '安装失败', + 'msg_install_failed_all': '所有APK安装失败!', + 'msg_install_exception': '安装过程异常', + 'file_select_folder_title': '选择包含APK文件的文件夹', + 'file_select_apk_title': '选择APK文件', + 'filetype_apk': 'APK文件', + 'filetype_all': '所有文件', + 'quick_lang_title': '快捷语言设置', + 'quick_lang_header': '选择目标语言', + 'quick_lang_hint': '点击按钮即可将系统语言切换为对应语言,重启后生效', + 'quick_lang_system': '⚙️ 打开系统语言设置(手动选择)', + 'quick_lang_success_title': '设置成功', + 'quick_lang_success': '系统语言已设置为 {language}\n\n⚠️ 请重启设备使其生效。', + 'quick_lang_failed_title': '设置失败', + 'quick_lang_failed': '语言设置失败!', + 'quick_lang_names': ['🇨🇳 中文', '英 English', '俄 Русский', '法 Français', '西 Español', '葡 Português', '意 Italiano', '阿 العربية'], + 'msg_reboot_title': '确认重启', + 'msg_reboot_confirm': '确定要重启设备吗?', + 'msg_disable_ota_title': '确认禁用升级', + 'msg_disable_ota_confirm': '⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n是否确定要禁用系统升级应用?', + 'msg_disable_ota_success': '系统升级已成功禁用!', + 'msg_disable_ota_failed': '禁用失败', + 'debug_title': '调试模式', + 'debug_prompt': '请输入调试密码:', + 'debug_password_verifying': '正在校验调试模式密码...', + 'debug_verify_failed': '调试模式密码校验失败: {message}', + 'debug_status': '🔧 调试模式', + 'msg_debug_wrong_password': '密码错误', + 'debug_need_enable': '请先按 Ctrl+Shift+D 开启调试模式', + 'debug_extract_title': '测试解压', + 'debug_extract_prompt': '请输入 package.bin 解压密码:', + 'debug_extract_success_title': '测试解压成功', + 'debug_extract_success': '资源已解压到:\n{path}', + 'debug_extract_failed_title': '测试解压失败', + 'debug_extract_failed': '请查看日志中的 7za 输出', + 'progress_loading': '资源加载中', + 'progress_loaded': '资源加载完成', + 'progress_flashing': '正在刷入', + 'progress_flash_done': '刷入完成', + 'progress_aborted': '已终止', + 'progress_installing': '安装中', + 'progress_installing_name': '安装中 ({name})', + 'progress_done': '完成', + 'progress_install_done': '安装完成', + 'log_lang_changed': '语言已切换为中文', + 'log_cleared': '日志已清空', + 'log_device_connected': '设备已连接', + 'log_device_disconnected': '设备未连接', + 'log_vin': 'VIN: {vin}', + 'log_vin_unavailable': '无法获取VIN', + 'log_refresh_failed': '刷新设备状态失败', + 'log_debug_skip_auth': '调试模式: 跳过授权验证', + 'log_auth_checking': '正在验证授权...', + 'log_auth_success': '授权验证通过', + 'log_auth_failed': '授权验证失败', + 'log_vehicle_name': '车辆名称: {vehicle}', + 'log_need_adb': '请先连接adb!', + 'log_data_prepare_failed': '资源准备失败', + 'log_package_missing': '未找到资源包文件', + 'log_adb_missing': '未找到adb命令,请将ADB文件放入本目录', + 'log_cache_invalid': '资源缓存无效', + 'log_resource_missing': '资源目录异常', + 'log_resource_invalid': '资源校验失败', + 'log_resource_ready': '资源准备完成', + 'log_resource_failed': '资源准备失败,请检查网络连接后重试', + 'log_no_language_files': '未找到语言包文件', + 'log_permission_root_failed': '获取 root 失败', + 'log_permission_failed': '获取权限失败', + 'log_permission_reboot_required': '首次获取权限,需要重启设备...', + 'log_permission_rebooting': '设备即将重启,重启后权限生效', + 'log_permission_reboot_failed': '重启失败', + 'log_permission_success': '已获取权限', + 'log_flash_readonly': '请先点击「获取权限」获取权限后再试', + 'log_flash_done': '语言包刷入完成,共 {total} 个', + 'log_flash_effective': '语言包已刷入完成,重启设备后生效,您可在适当时候重启', + 'log_flash_partial': '部分刷入成功({success}/{total})', + 'log_batch_install_start': '开始批量安装 {count} 个APK...', + 'log_install_many_start': '开始安装 {count} 个APK...', + 'log_install_done_all': '安装完成:全部 {count} 个成功', + 'log_install_done_partial': '安装完成:{success}/{count} 成功', + 'log_install_success': '安装成功', + 'log_install_failed': '安装失败', + 'log_install_exception': '安装过程异常', + 'log_quick_lang_setting': '正在设置系统语言为: {language} ({locale})', + 'log_quick_lang_success': '语言已设置为 {language}', + 'log_quick_lang_failed': '语言设置失败', + 'log_rebooting': '设备正在重启...', + 'log_disable_ota_cancelled': '已取消禁用升级操作', + 'log_disable_ota_success': '系统升级已禁用', + 'log_disable_ota_failed': '禁用系统升级失败', + 'log_pwd_success': '密码查询成功 VIN={vin}', + 'log_pwd_failed': '密码查询失败', + 'log_pwd_request_failed': '密码查询请求失败', + 'log_debug_off': '调试模式已关闭', + 'log_debug_on': '调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志', + 'err_extract_wrong_password': '资源准备失败', + 'err_extract_data': '资源准备失败', + 'err_extract_headers': '资源准备失败', + 'err_extract_detail': '资源准备失败', + 'err_extract_default': '资源准备失败,请检查解压密码是否正确', + 'msg_enter_vin': '请输入VIN码', + 'msg_start_failed': '程序启动失败', + 'msg_python_version_error': '错误:需要Python 3.6或更高版本', + 'log_flash_start_notice': '开始刷入语言包,请勿断电或重启电脑和车机。', + 'log_resource_prepare_start': '资源准备中', + 'log_extract_password_missing': '资源准备失败', + 'log_7za_missing': '资源准备失败', + 'log_resource_dir_missing': '资源目录异常', + 'log_debug_extract_success': '测试解压成功', + 'log_debug_extract_failed': '测试解压失败', + 'unknown_error': '未知错误', }, 'en': { - 'title': 'Shenlan S05 Multi-Language', + 'title': 'Deepal S05 Multi-Language', 'btn_root': '🔓 Get Root', 'btn_push': '📦 Flash Lang Pkg', 'btn_install': '📱 Install App', @@ -157,17 +315,162 @@ class ADKAPKGUI: 'auth_yes': 'Authorized', 'auth_no': 'Unauthorized', 'btn_refresh': '🔄 Check', - 'hint_factory': '🔧 Factory Mode: dial *#*#888, password: 369875', + 'hint_factory': '🔧 Disconnect the head unit network, then enter Factory Mode with the password from dialing.', 'theme_dark': '🌙 Dark', 'theme_light': '☀️ Light', - 'lang_zh': '中', - 'lang_en': 'EN', + 'lang_zh': '中文', + 'lang_en': 'English', 'switch_lang': 'Language', 'switch_theme': 'Theme', - 'about_company': 'Yibin Keyi Technology - Export Modification Service', + 'pwd_query_label': 'Factory password:', + 'vin_placeholder': 'Enter VIN', + 'btn_query_pwd': 'Query Password', + 'pwd_empty': '', + 'pwd_success': 'Password: *#{password}#*', + 'pwd_failed': 'Failed: {message}', + 'pwd_request_failed': 'Request failed', + 'hint_lines': [ + '1. Keep the vehicle and computer powered during language installation.', + '2. After getting permission, wait for the vehicle to reboot before flashing.', + '3. Some languages apply after reboot; reboot after all work is complete.', + ], + 'msg_warn_title': 'Warning', + 'msg_error_title': 'Error', + 'msg_success_title': 'Success', + 'msg_hint_title': 'Hint', + 'msg_device_not_connected_title': 'Device not connected', + 'msg_device_not_connected': 'Connect the device and click "Check" first.', + 'msg_need_vin': 'Refresh device status and get VIN first', + 'msg_auth_failed_title': 'Authorization failed', + 'msg_device_unauthorized': 'Device is not authorized', + 'msg_data_prepare_failed': 'Resource preparation failed!', + 'msg_resource_dir_missing': 'Resource directory not found', + 'msg_flash_warning_title': 'Important warning', + 'msg_flash_warning': 'During flashing, do not:\n - reboot the head unit\n - close this program\n - shut down the computer\n\nOtherwise the system may be damaged.', + 'msg_no_apks_in_folder': 'No APK files found in the selected folder.', + 'msg_install_confirm_title': 'Confirm install', + 'msg_install_confirm_folder': 'Found {count} APK file(s).\n\nStart batch install?', + 'msg_install_confirm_many': 'Selected {count} APK file(s).\n\nStart installing?', + 'msg_install_done_title': 'Install complete', + 'msg_install_done_all': 'Successfully installed {count} APK file(s).', + 'msg_install_partial_title': 'Partially complete', + 'msg_install_partial': 'Succeeded: {success}\nFailed: {failed}', + 'msg_install_failed_title': 'Install failed', + 'msg_install_failed_all': 'All APK installs failed.', + 'msg_install_exception': 'Install process exception', + 'file_select_folder_title': 'Select folder containing APK files', + 'file_select_apk_title': 'Select APK files', + 'filetype_apk': 'APK files', + 'filetype_all': 'All files', + 'quick_lang_title': 'Quick Language', + 'quick_lang_header': 'Select Target Language', + 'quick_lang_hint': 'Tap a language to switch system locale. Reboot to apply.', + 'quick_lang_system': 'Open system language settings', + 'quick_lang_success_title': 'Set Successfully', + 'quick_lang_success': 'System language has been set to {language}.\n\nReboot the device to apply.', + 'quick_lang_failed_title': 'Set Failed', + 'quick_lang_failed': 'Language setting failed.', + 'quick_lang_names': ['🇨🇳 Chinese', 'English', 'Russian', 'French', 'Spanish', 'Portuguese', 'Italian', 'Arabic'], + 'msg_reboot_title': 'Confirm reboot', + 'msg_reboot_confirm': 'Reboot the device now?', + 'msg_disable_ota_title': 'Confirm Disable OTA', + 'msg_disable_ota_confirm': 'Warning: after disabling OTA, the system will not receive updates.\n\nDisable the OTA app now?', + 'msg_disable_ota_success': 'System OTA has been disabled.', + 'msg_disable_ota_failed': 'Disable failed', + 'debug_title': 'Debug Mode', + 'debug_prompt': 'Enter debug password:', + 'debug_password_verifying': 'Verifying debug mode password...', + 'debug_verify_failed': 'Debug mode password verification failed: {message}', + 'debug_status': 'Debug Mode', + 'msg_debug_wrong_password': 'Wrong password', + 'debug_need_enable': 'Press Ctrl+Shift+D to enable debug mode first', + 'debug_extract_title': 'Extract Test', + 'debug_extract_prompt': 'Enter package.bin extraction password:', + 'debug_extract_success_title': 'Extract Test Succeeded', + 'debug_extract_success': 'Resources extracted to:\n{path}', + 'debug_extract_failed_title': 'Extract Test Failed', + 'debug_extract_failed': 'Check the log for 7za output', + 'progress_loading': 'Preparing resources', + 'progress_loaded': 'Resources ready', + 'progress_flashing': 'Flashing', + 'progress_flash_done': 'Flash complete', + 'progress_aborted': 'Aborted', + 'progress_installing': 'Installing', + 'progress_installing_name': 'Installing ({name})', + 'progress_done': 'Done', + 'progress_install_done': 'Install complete', + 'log_lang_changed': 'Language switched to English', + 'log_cleared': 'Log cleared', + 'log_device_connected': 'Device connected', + 'log_device_disconnected': 'Device disconnected', + 'log_vin': 'VIN: {vin}', + 'log_vin_unavailable': 'Unable to read VIN', + 'log_refresh_failed': 'Failed to refresh device status', + 'log_debug_skip_auth': 'Debug mode: skipping authorization', + 'log_auth_checking': 'Checking authorization...', + 'log_auth_success': 'Authorization passed', + 'log_auth_failed': 'Authorization failed', + 'log_vehicle_name': 'Vehicle name: {vehicle}', + 'log_need_adb': 'Connect ADB first.', + 'log_data_prepare_failed': 'Resource preparation failed', + 'log_package_missing': 'Resource file not found', + 'log_adb_missing': 'adb not found. Place ADB files in this folder.', + 'log_cache_invalid': 'Resource cache is invalid', + 'log_resource_missing': 'Resource directory is invalid', + 'log_resource_invalid': 'Resource validation failed', + 'log_resource_ready': 'Resources ready', + 'log_resource_failed': 'Resource preparation failed. Check the network and try again.', + 'log_no_language_files': 'No language package files found', + 'log_permission_root_failed': 'Root permission failed', + 'log_permission_failed': 'Permission failed', + 'log_permission_reboot_required': 'First permission setup requires reboot...', + 'log_permission_rebooting': 'Device will reboot; permission takes effect after reboot', + 'log_permission_reboot_failed': 'Reboot failed', + 'log_permission_success': 'Permission ready', + 'log_flash_readonly': 'Get permission first, then try again', + 'log_flash_done': 'Language package flashing complete, total {total}', + 'log_flash_effective': 'Language package flashing complete. Reboot later to apply.', + 'log_flash_partial': 'Partially flashed ({success}/{total})', + 'log_batch_install_start': 'Starting batch install for {count} APK file(s)...', + 'log_install_many_start': 'Starting install for {count} APK file(s)...', + 'log_install_done_all': 'Install complete: all {count} succeeded', + 'log_install_done_partial': 'Install complete: {success}/{count} succeeded', + 'log_install_success': 'Install succeeded', + 'log_install_failed': 'Install failed', + 'log_install_exception': 'Install process exception', + 'log_quick_lang_setting': 'Setting system language to {language} ({locale})', + 'log_quick_lang_success': 'Language set to {language}', + 'log_quick_lang_failed': 'Language setting failed', + 'log_rebooting': 'Device is rebooting...', + 'log_disable_ota_cancelled': 'Disable OTA operation cancelled', + 'log_disable_ota_success': 'System OTA disabled', + 'log_disable_ota_failed': 'Disable OTA failed', + 'log_pwd_success': 'Password query succeeded VIN={vin}', + 'log_pwd_failed': 'Password query failed', + 'log_pwd_request_failed': 'Password query request failed', + 'log_debug_off': 'Debug mode disabled', + 'log_debug_on': 'Debug mode enabled - authorization/device checks skipped, detailed ADB logs shown', + 'err_extract_wrong_password': 'Resource preparation failed', + 'err_extract_data': 'Resource preparation failed', + 'err_extract_headers': 'Resource preparation failed', + 'err_extract_detail': 'Resource preparation failed', + 'err_extract_default': 'Resource preparation failed. Check the extraction password.', + 'msg_enter_vin': 'Enter VIN first', + 'msg_start_failed': 'Program startup failed', + 'msg_python_version_error': 'Error: Python 3.6 or later is required', + 'log_flash_start_notice': 'Starting language package flash. Do not power off or restart the computer or head unit.', + 'log_resource_prepare_start': 'Preparing resources', + 'log_extract_password_missing': 'Resource preparation failed', + 'log_7za_missing': 'Resource preparation failed', + 'log_resource_dir_missing': 'Resource directory is invalid', + 'log_debug_extract_success': 'Extract test succeeded', + 'log_debug_extract_failed': 'Extract test failed', + 'unknown_error': 'Unknown error', } } + self.root.title(self.t('title')) + # 从 exe/py 所在目录查找资源文件 self.base_dir = get_app_dir() self.adb = find_tool('adb.exe', 'adb') @@ -178,14 +481,18 @@ class ADKAPKGUI: self.priv_apps_dir = None self.temp_dir = None self.api_url = "https://api.changan.softwindy.cn/api/authorizations/auth-check" + self.debug_password_api_url = "https://api.changan.softwindy.cn/api/authorizations/verify-debug-mode-password" self.vin = None + self.vehicle_name = "" self.device_connected = False self._refreshing = False # 防止并发刷新 self.debug_mode = False # 调试模式 + atexit.register(self.cleanup_cache_on_exit) # 设置样式 self.setup_styles() self.setup_ui() + self.root.protocol("WM_DELETE_WINDOW", self.on_close) self.center_window() # 检查环境 @@ -225,29 +532,39 @@ class ADKAPKGUI: 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() + title_content_frame = tk.Frame(title_frame, bg=self.colors['bg_dark']) + title_content_frame.pack(fill=tk.X, expand=True) - subtitle_label = tk.Label(title_frame, - text="宜宾科宜科技有限公司 - 智能设备管理平台", - font=('Microsoft YaHei', 9), - fg=self.colors['text_secondary'], - bg=self.colors['bg_dark']) - subtitle_label.pack() + # 标题 + self.title_label = tk.Label(title_content_frame, + text="🚀 " + self.t('title'), + font=('Microsoft YaHei', 18, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.title_label.pack(side=tk.LEFT, expand=True, padx=(0, 10)) + + self.btn_lang_switch = tk.Button(title_content_frame, text=self.t('lang_en'), + command=self.toggle_lang, + font=('Microsoft YaHei', 9, 'bold'), + fg='white', + bg=self.colors['accent'], + activeforeground='white', + activebackground=self.colors['accent_hover'], + relief=tk.FLAT, + cursor='hand2', + width=7, + height=1) + self.btn_lang_switch.pack(side=tk.RIGHT, padx=(8, 4)) # 工程密码查询区域 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.pwd_query_label = tk.Label(pwd_query_frame, text=self.t('pwd_query_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.pwd_query_label.pack(side=tk.LEFT, padx=(10, 5), pady=5) self.vin_input = tk.Entry(pwd_query_frame, font=('Consolas', 9), @@ -256,12 +573,12 @@ class ADKAPKGUI: insertbackground='white', relief=tk.FLAT, width=20) - self.vin_input.insert(0, "请输入VIN") + self.vin_input.insert(0, self.t('vin_placeholder')) 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="查询密码", + self.btn_query_pwd = tk.Button(pwd_query_frame, text=self.t('btn_query_pwd'), command=self.query_password_by_vin, font=('Microsoft YaHei', 8), fg='white', @@ -270,7 +587,7 @@ class ADKAPKGUI: cursor='hand2') self.btn_query_pwd.pack(side=tk.LEFT, padx=5, pady=5) - self.pwd_result_label = tk.Label(pwd_query_frame, text="", + self.pwd_result_label = tk.Label(pwd_query_frame, text=self.t('pwd_empty'), font=('Microsoft YaHei', 9, 'bold'), fg=self.colors['success'], bg=self.colors['bg_light']) @@ -279,10 +596,11 @@ class ADKAPKGUI: # 工厂模式提示 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) + self.hint_label = tk.Label(factory_hint_frame, text=self.t('hint_factory'), + font=('Microsoft YaHei', 8), + fg=self.colors['warning'], + bg=self.colors['bg_dark']) + self.hint_label.pack(side=tk.LEFT, padx=2) # 按钮区域(两排,每排5个) @@ -303,25 +621,25 @@ class ADKAPKGUI: 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="🔓 获取权限", + self.btn_root = tk.Button(row1_frame, text=self.t('btn_root'), 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="📦 刷入语言包", + self.btn_push = tk.Button(row1_frame, text=self.t('btn_push'), command=self.push_all_apks, bg=self.colors['accent'], **btn_params) self.btn_push.pack(side=tk.LEFT, padx=4) - self.btn_install_all = tk.Button(row1_frame, text="📱 安装App", + self.btn_install_all = tk.Button(row1_frame, text=self.t('btn_install'), command=self.install_apps, bg=self.colors['accent'], **btn_params) self.btn_install_all.pack(side=tk.LEFT, padx=4) - self.btn_language = tk.Button(row1_frame, text="🌐 语言设置", + self.btn_language = tk.Button(row1_frame, text=self.t('btn_language'), command=self.open_language_quick_set, bg=self.colors['accent'], **btn_params) @@ -331,25 +649,25 @@ class ADKAPKGUI: row2_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) row2_frame.pack(pady=(4, 8)) - self.btn_timezone = tk.Button(row2_frame, text="⏰ 时区设置", + self.btn_timezone = tk.Button(row2_frame, text=self.t('btn_timezone'), command=self.open_timezone_settings, bg=self.colors['accent'], **btn_params) self.btn_timezone.pack(side=tk.LEFT, padx=4) - self.btn_settings = tk.Button(row2_frame, text="⚙️ 安卓设置", + self.btn_settings = tk.Button(row2_frame, text=self.t('btn_settings'), command=self.open_android_settings, bg=self.colors['accent'], **btn_params) self.btn_settings.pack(side=tk.LEFT, padx=4) - self.btn_reboot = tk.Button(row2_frame, text="🔄 重启设备", + self.btn_reboot = tk.Button(row2_frame, text=self.t('btn_reboot'), command=self.reboot_device, bg=self.colors['warning'], **btn_params) self.btn_reboot.pack(side=tk.LEFT, padx=4) - self.btn_exit = tk.Button(row2_frame, text="❌ 禁用升级", + self.btn_exit = tk.Button(row2_frame, text=self.t('btn_disable_upgrade'), command=self.on_disable_upgrade, bg=self.colors['error'], **btn_params) @@ -368,26 +686,28 @@ class ADKAPKGUI: 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_label = tk.Label(status_indicator_frame, text=self.t('device_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.device_label.pack(side=tk.LEFT, padx=(5, 3)) - self.device_status_label = tk.Label(status_indicator_frame, text="未检测", + self.device_status_label = tk.Label(status_indicator_frame, text=self.t('status_detecting'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light'], - anchor='w', width=4) + anchor='w', width=11) 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="未获取", + vin_frame.pack(side=tk.LEFT, padx=8, pady=5) + self.vin_label_title = tk.Label(vin_frame, text=self.t('vin_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.vin_label_title.pack(side=tk.LEFT) + self.vin_label = tk.Label(vin_frame, text=self.t('vin_none'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light'], @@ -396,47 +716,47 @@ class ADKAPKGUI: # 授权状态 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="未验证", + auth_frame.pack(side=tk.LEFT, padx=8, pady=5) + self.auth_label_title = tk.Label(auth_frame, text=self.t('auth_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.auth_label_title.pack(side=tk.LEFT) + self.auth_label = tk.Label(auth_frame, text=self.t('auth_none'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light'], - anchor='w', width=4) + anchor='w', width=10) 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) + self.btn_refresh = tk.Button(status_bar_frame, text=self.t('btn_refresh'), + command=self.refresh_device_status, + font=('Microsoft YaHei', 8), + fg=self.colors['accent'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2', + width=8) + self.btn_refresh.pack(side=tk.RIGHT, padx=(4, 8), 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. 部分语言需要重启后生效,可以一切工作完成以后再重启。", - ] - + self.tips_labels = [] + tips = self.t('hint_lines') 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) + label = tk.Label(tip_row, text=tip, + font=('Microsoft YaHei', 9), + fg=self.colors['warning'], + bg=self.colors['bg_light'], + wraplength=600, + justify=tk.LEFT) + label.pack(side=tk.LEFT) + self.tips_labels.append(label) # 解压进度条框架 progress_frame = tk.Frame(main_frame, bg=self.colors['bg_dark']) @@ -468,12 +788,13 @@ class ADKAPKGUI: 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.log_title_label = tk.Label(log_title_frame, text=self.t('log_title'), + font=('Microsoft YaHei', 10, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.log_title_label.pack(side=tk.LEFT, padx=10) - self.btn_clear = tk.Button(log_title_frame, text="🗑 清空日志", + self.btn_clear = tk.Button(log_title_frame, text=self.t('btn_clear_log'), command=self.clear_log, font=('Microsoft YaHei', 8), fg=self.colors['text_secondary'], @@ -509,14 +830,14 @@ class ADKAPKGUI: bottom_status.pack(fill=tk.X, pady=(5, 0)) bottom_status.pack_propagate(False) - self.status_text = tk.Label(bottom_status, text="就绪", + self.status_text = tk.Label(bottom_status, text=self.t('status_ready'), font=('Microsoft YaHei', 8), fg=self.colors['text_secondary'], bg=self.colors['bg_light']) self.status_text.pack(side=tk.LEFT, padx=10) - # 主题和语言切换按钮 - self.btn_theme_switch = tk.Button(bottom_status, text="🌙 暗色", + # 主题切换按钮 + self.btn_theme_switch = tk.Button(bottom_status, text=self.t('theme_light'), command=self.toggle_theme, font=('Microsoft YaHei', 8), fg=self.colors['accent'], @@ -525,15 +846,6 @@ class ADKAPKGUI: 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) @@ -545,7 +857,8 @@ class ADKAPKGUI: """绑定按钮悬停效果""" 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] + self.btn_reboot, self.btn_clear, self.btn_exit, + self.btn_lang_switch] for btn in buttons: original_bg = btn.cget('bg') @@ -570,6 +883,24 @@ class ADKAPKGUI: return '#00a884' return color + def set_window_icon(self): + """Set Tk window/taskbar icon at runtime; PyInstaller --icon only sets the exe file icon.""" + try: + icon_path = find_resource("app.ico") + if icon_path.exists(): + self.root.iconbitmap(str(icon_path)) + if sys.platform == 'win32': + import ctypes + hwnd = self.root.winfo_id() + image = ctypes.windll.user32.LoadImageW( + None, str(icon_path), 1, 0, 0, 0x00000010 + ) + if image: + ctypes.windll.user32.SendMessageW(hwnd, 0x0080, 0, image) + ctypes.windll.user32.SendMessageW(hwnd, 0x0080, 1, image) + except Exception: + pass + def center_window(self): """将窗口居中显示在屏幕上""" self.root.update_idletasks() @@ -583,7 +914,7 @@ class ADKAPKGUI: def run_on_ui_thread(self, func, *args, **kwargs): """将函数调度到主线程执行,确保线程安全""" - self.root.after(0, func, *args, **kwargs) + self.root.after(0, lambda: func(*args, **kwargs)) def _adb_cmd(self): """返回可安全用于 shell 命令字符串的 adb 路径""" @@ -593,12 +924,24 @@ class ADKAPKGUI: """获取翻译文本""" return self.T.get(self.lang, self.T['zh']).get(key, key) + def tf(self, key, **kwargs): + try: + return self.t(key).format(**kwargs) + except Exception: + return self.t(key) + + def is_placeholder_vin(self, value): + return value in ( + self.T['zh'].get('vin_placeholder'), + self.T['en'].get('vin_placeholder'), + ) + def toggle_lang(self): """切换语言""" self.lang = 'en' if self.lang == 'zh' else 'zh' self.btn_lang_switch.config(text=self.t('lang_en') if self.lang == 'zh' else self.t('lang_zh')) self._refresh_ui_texts() - self.log(f"语言已切换为 {'English' if self.lang == 'en' else '中文'}", "INFO") + self.log(self.t('log_lang_changed'), "INFO") def toggle_theme(self): """切换主题""" @@ -635,9 +978,11 @@ class ADKAPKGUI: def _refresh_ui_texts(self): """刷新所有UI文本""" t = self.t + self.root.title(t('title')) widgets = [ (getattr(self, 'title_label', None), 'title', None), - (getattr(self, 'subtitle_label', None), 'about_company', None), + (getattr(self, 'pwd_query_label', None), 'pwd_query_label', None), + (getattr(self, 'btn_query_pwd', None), 'btn_query_pwd', 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), @@ -657,15 +1002,43 @@ class ADKAPKGUI: ] for w, key, _ in widgets: if w: - w.config(text=t(key)) + text = t(key) + if key == 'title': + text = "🚀 " + text + w.config(text=text) self.btn_theme_switch.config(text=t('theme_light') if self.theme == 'dark' else t('theme_dark')) self.btn_lang_switch.config(text=t('lang_en') if self.lang == 'zh' else t('lang_zh')) - if self.vin: - self._update_device_status_impl(self.device_connected, self.vin, - getattr(self, '_last_authorized', False)) + if self.is_placeholder_vin(self.vin_input.get()): + self.vin_input.delete(0, tk.END) + self.vin_input.insert(0, t('vin_placeholder')) + for label, tip in zip(getattr(self, 'tips_labels', []), t('hint_lines')): + label.config(text=tip) + self._update_device_status_impl(self.device_connected, self.vin, + getattr(self, '_last_authorized', False)) + + def _sanitize_user_log_message(self, message): + text = str(message) + replacements = [ + (r'com\.[\w.\-]+', '相关应用'), + (r'cn\.[\w.\-]+', '相关应用'), + (r'[\w.\-]+\.apk', '文件'), + (r'package\.bin', '资源文件'), + (r'7za(?:\.exe)?', '资源工具'), + (r'adb(?:\.exe)?', '设备连接工具'), + (r'pm\s+\S+', '系统操作'), + (r'(? 5 else "" - return False, f"发现 0KB APK: {preview}{suffix}" + return False, f"0KB APK: {preview}{suffix}" return True, "" def _clear_extracted_cache(self): @@ -856,8 +1233,21 @@ class ADKAPKGUI: self.apps_dir = None self.priv_apps_dir = None + def cleanup_cache_on_exit(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 self.temp_dir or self.temp_dir != cache_dir) and cache_dir.exists(): + shutil.rmtree(cache_dir, ignore_errors=True) + self._clear_extracted_cache() + + def on_close(self): + self.cleanup_cache_on_exit() + self.root.destroy() + def _format_extract_error(self, err_msg): text = (err_msg or "").lower() + if self.debug_mode and err_msg and err_msg.strip(): + return f"Resource preparation failed: {err_msg.strip()[:1000]}" if any(marker in text for marker in ( "wrong password", "incorrect password", @@ -865,14 +1255,14 @@ class ADKAPKGUI: "data error in encrypted file", "can not open encrypted archive", )): - return "解压密码错误,请重新确认 package.bin 密码" + return self.t('err_extract_wrong_password') if "data error" in text: - return "资源包数据错误,可能是密码错误或 package.bin 损坏" + return self.t('err_extract_data') if "headers error" in text or "unexpected end" in text: - return "资源包损坏或不完整,请检查 package.bin" + return self.t('err_extract_headers') if err_msg.strip(): - return f"资源准备失败: {err_msg.strip()[:300]}" - return "资源准备失败,请检查解压密码是否正确" + return self.t('err_extract_detail') + return self.t('err_extract_default') def _decode_7z_output(self, output): """解码 7za 输出,兼容中文 Windows 控制台编码""" @@ -885,7 +1275,7 @@ class ADKAPKGUI: def _extract_with_7za_progress(self): """运行 7za 并实时解析百分比进度""" - self.update_progress(0, 100, "资源加载中...") + self.update_progress(0, 100, self.t('progress_loading')) cmd = [ self.sz, 'x', str(self.package_file), f'-p{self.extract_password}', @@ -922,12 +1312,14 @@ class ADKAPKGUI: percent = min(100, int(matches[-1])) if percent != last_percent: last_percent = percent - self.update_progress(percent, 100, "资源加载中...") + self.update_progress(percent, 100, self.t('progress_loading')) return_code = proc.wait() decoded_output = self._decode_7z_output(bytes(output)) + if self.debug_mode: + self.log("7ZA OUTPUT:\n" + decoded_output, "CMD" if return_code == 0 else "ERROR") if return_code == 0: - self.update_progress(100, 100, "资源加载完成") + self.update_progress(100, 100, self.t('progress_loaded')) return True, decoded_output return False, decoded_output @@ -948,15 +1340,19 @@ class ADKAPKGUI: def extract_package_silent(self): """静默解压语言包(带进度)""" if not self.package_file.exists(): - self.log(f"错误:未找到资源包 ({self.package_file})", "ERROR") + if self.debug_mode: + self.log(f"Package file missing: {self.package_file}", "ERROR") + self.log(self.t('log_package_missing'), "ERROR") return False if not self.extract_password: - self.log("错误:解压密码未设置", "ERROR") + self.log(self.t('log_extract_password_missing'), "ERROR") return False if not os.path.exists(self.sz): - self.log(f"错误:未找到 7za.exe ({self.sz})", "ERROR") + if self.debug_mode: + self.log(f"7za missing: {self.sz}", "ERROR") + self.log(self.t('log_7za_missing'), "ERROR") return False try: @@ -979,7 +1375,7 @@ class ADKAPKGUI: 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") + self.log(self.t('log_resource_prepare_start'), "SUCCESS") ok, err_msg = self._extract_with_7za_progress() if not ok: @@ -1000,27 +1396,27 @@ class ADKAPKGUI: self.priv_apps_dir = priv_app_candidates[0] if not self.apps_dir and not self.priv_apps_dir: - self.log("警告:未找到对应目录", "WARNING") + self.log(self.t('log_resource_dir_missing'), "WARNING") self._clear_extracted_cache() 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 ok, reason = self._validate_extracted_apks() if not ok: - self.log(f"解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。", "ERROR") + if self.debug_mode: + self.log(f"Extracted resource invalid: {reason}", "ERROR") + self.log(self.t('log_resource_invalid'), "ERROR") self._clear_extracted_cache() return False - self.log(f"资源准备完成", "SUCCESS") + self.log(self.t('log_resource_ready'), "SUCCESS") return True except Exception as e: if getattr(self, 'debug_mode', False): - self.log(f"资源准备失败: {str(e)}", "ERROR") + self.log(f"Resource preparation failed: {str(e)}", "ERROR") import traceback self.log(traceback.format_exc(), "ERROR") else: - self.log("资源准备失败,请检查网络连接后重试", "ERROR") + self.log(self.t('log_resource_failed'), "ERROR") self._clear_extracted_cache() return False @@ -1031,13 +1427,13 @@ class ADKAPKGUI: if result.returncode == 0: self.refresh_device_status() if not self.package_file.exists(): - self.log("未找到资源包文件", "WARNING") + self.log(self.t('log_package_missing'), "WARNING") else: self._try_reuse_extracted() else: - self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") + self.log(self.t('log_adb_missing'), "ERROR") except FileNotFoundError: - self.log("未找到adb命令,请将ADB文件放入本目录", "ERROR") + self.log(self.t('log_adb_missing'), "ERROR") def _try_reuse_extracted(self): """检查磁盘上是否已有解压好的资源,有则直接复用""" @@ -1066,7 +1462,9 @@ class ADKAPKGUI: self.temp_dir = cache_dir ok, reason = self._validate_extracted_apks() if not ok: - self.log(f"缓存资源无效,已清理: {reason}", "WARNING") + if self.debug_mode: + self.log(f"Cached resource invalid, cleared: {reason}", "WARNING") + self.log(self.t('log_cache_invalid'), "WARNING") self._clear_extracted_cache() return # self.log("已复用缓存的资源文件", "INFO") @@ -1090,7 +1488,7 @@ class ADKAPKGUI: if devices: # 只在首次连接时打日志 if not was_connected: - self.log("设备已连接", "SUCCESS") + self.log(self.t('log_device_connected'), "SUCCESS") # 获取VIN — 兼容两种 key,过滤 Android null 返回值 vin = '' @@ -1103,20 +1501,22 @@ class ADKAPKGUI: break vin = '' if vin: - self.log(f"当前车辆VIN: {vin}", "INFO") + self.log(self.tf('log_vin', vin=vin), "SUCCESS") # 验证授权 authorized = self.check_authorization(vin) self.update_device_status(True, vin, authorized) else: - self.log("无法获取VIN", "WARNING") + self.log(self.t('log_vin_unavailable'), "WARNING") self.update_device_status(True, None, False) else: if was_connected: - self.log("设备未连接", "WARNING") + self.log(self.t('log_device_disconnected'), "WARNING") self.update_device_status(False) except Exception as e: - self.log(f"刷新设备状态失败: {str(e)}", "ERROR") + if self.debug_mode: + self.log(f"Refresh failed: {str(e)}", "ERROR") + self.log(self.t('log_refresh_failed'), "ERROR") finally: self._refreshing = False @@ -1125,38 +1525,75 @@ class ADKAPKGUI: def check_authorization(self, vin): """检查授权""" if self.debug_mode: - self.log("调试模式: 跳过授权验证", "WARNING") + self.log(self.t('log_debug_skip_auth'), "WARNING") return True - self.log("正在验证授权...", "INFO") + self.log(self.t('log_auth_checking'), "INFO") try: - url = f"{self.api_url}?{urlencode({'vin': vin})}" - req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + authorized, vehicle_name, _ = self.query_authorization_info(vin) - 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") + if authorized: + self.log(self.t('log_auth_success'), "SUCCESS") + if vehicle_name: + self.log(self.tf('log_vehicle_name', vehicle=vehicle_name), "SUCCESS") return True else: - self.log(f"❌ 授权验证失败", "ERROR") + self.log(self.t('log_auth_failed'), "ERROR") return False except Exception as e: - self.log(f"❌ 授权验证失败", "ERROR") + if self.debug_mode: + self.log(f"Auth check failed: {str(e)}", "ERROR") + self.log(self.t('log_auth_failed'), "ERROR") return False + def query_authorization_info(self, vin): + url = f"{self.api_url}?{urlencode({'vin': vin})}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + payload = data.get('data', {}) if isinstance(data, dict) else {} + vehicle_name = payload.get('vehicleName') or payload.get('vehicle_name') or "" + vehicle_name = str(vehicle_name).strip() + authorized = data.get('authorized') is True or payload.get('authorized') is True + if authorized and vehicle_name: + self.vehicle_name = vehicle_name + return authorized, vehicle_name, data + + def _post_json(self, url, payload, timeout=10): + body = json.dumps(payload).encode('utf-8') + req = Request( + url, + data=body, + method='POST', + headers={ + 'User-Agent': 'Mozilla/5.0', + 'Content-Type': 'application/json', + }, + ) + with urlopen(req, timeout=timeout) as response: + return json.loads(response.read().decode('utf-8')) + def fetch_package_password(self): """从服务端获取资源包解压密码""" if not self.vin: - self.log("请先连接adb!", "ERROR") + self.log(self.t('log_need_adb'), "ERROR") return False try: + vehicle_name = self.vehicle_name + if not vehicle_name: + authorized, vehicle_name, _ = self.query_authorization_info(self.vin) + if not authorized: + self.log(self.t('log_auth_failed'), "ERROR") + return False + if not vehicle_name: + self.log(self.t('log_data_prepare_failed'), "ERROR") + return False + pwd_api_url = "https://api.changan.softwindy.cn/api/authorizations/package-key" - url = f"{pwd_api_url}?{urlencode({'vin': self.vin})}" + url = f"{pwd_api_url}?{urlencode({'vin': self.vin, 'vehicleName': vehicle_name})}" + if self.debug_mode: + self.log(f"PACKAGE KEY URL: {url}", "CMD") req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) with urlopen(req, timeout=10) as response: @@ -1164,13 +1601,20 @@ class ADKAPKGUI: if data.get('success') and 'data' in data and 'password' in data['data']: self.extract_password = data['data']['password'] + if self.debug_mode: + self.log("PACKAGE KEY: password received", "CMD") return True else: - self.log(f"数据准备失败: {data.get('message', '未知错误')}", "ERROR") + if self.debug_mode: + self.log(f"PACKAGE KEY RESPONSE: {data}", "CMD") + self.log(self.t('log_data_prepare_failed'), "ERROR") return False except Exception as e: - self.log(f"数据准备失败: {str(e)}", "ERROR") + if self.debug_mode: + import traceback + self.log(traceback.format_exc(), "ERROR") + self.log(self.t('log_data_prepare_failed'), "ERROR") return False def run_adb_command(self, command): @@ -1184,9 +1628,9 @@ class ADKAPKGUI: out = result.stdout.strip() err = result.stderr.strip() if out: - self.log(f" -> {out[:300]}", "CMD") + self.log(f"STDOUT:\n{out}", "CMD") if err: - self.log(f" !! {err[:300]}", "ERROR") + self.log(f"STDERR:\n{err}", "ERROR") if result.returncode == 0: return True, result.stdout.strip() else: @@ -1202,13 +1646,13 @@ class ADKAPKGUI: ok, err = self.run_adb_command(f'adb -d push "{apk_path}" {temp_apk_path}') if not ok: - return False, f"push失败: {err}" + return False, f"push failed: {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 False, f"copy failed: {err}" return True, "" @@ -1217,35 +1661,47 @@ class ADKAPKGUI: if not self.check_device_connection(): return if not self.vin: - messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码") + messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_need_vin')) return - messagebox.showwarning("⚠️ 重要提示", - "刷入过程中请勿:\n" - " ● 重启车机\n" - " ● 退出本程序\n" - " ● 关闭电脑\n\n" - "否则可能导致车机系统损坏!") + messagebox.showwarning(self.t('msg_flash_warning_title'), self.t('msg_flash_warning')) + self.log(self.t('log_flash_start_notice'), "SUCCESS") def do_push_all(): if not self.check_authorization(self.vin): - self.run_on_ui_thread(lambda: messagebox.showerror("授权失败", "设备未授权")) + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_auth_failed_title'), + self.t('msg_device_unauthorized') + ) return if not self.extract_password: if not self.fetch_package_password(): - self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源准备失败!")) + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_error_title'), + self.t('msg_data_prepare_failed') + ) 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("错误", "资源准备失败!")) + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_error_title'), + self.t('msg_data_prepare_failed') + ) 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("错误", "资源目录未找到")) + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_error_title'), + self.t('msg_resource_dir_missing') + ) return self.show_progress(True, is_push=True) @@ -1265,7 +1721,7 @@ class ADKAPKGUI: 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.log(self.t('log_no_language_files'), "WARNING") self.show_progress(False, is_push=True) return # 重新收集 @@ -1277,7 +1733,7 @@ class ADKAPKGUI: for apk in self.priv_apps_dir.glob("*.apk"): all_apks.append((apk, "priv-app")) if not all_apks: - self.log("未找到语言包文件", "WARNING") + self.log(self.t('log_no_language_files'), "WARNING") self.show_progress(False, is_push=True) return @@ -1291,20 +1747,22 @@ class ADKAPKGUI: success_count += 1 else: if "Read-only file system" in err: - self.log("请先点击「获取权限」获取权限后再试", "ERROR") + if self.debug_mode: + self.log(err, "ERROR") + self.log(self.t('log_flash_readonly'), "ERROR") aborted = True break - self.update_progress(i, total, "正在刷入...", is_push=True) + self.update_progress(i, total, self.t('progress_flashing'), is_push=True) - self.update_progress(total, total, "刷入完成" if not aborted else "已终止", is_push=True) + self.update_progress(total, total, self.t('progress_flash_done') if not aborted else self.t('progress_aborted'), is_push=True) if success_count == total: - self.log(f"刷入完成,共 {total} 个语言包", "SUCCESS") - self.log("语言包已刷入完成,重启设备后生效,您可在适当时候重启", "WARNING") + self.log(self.tf('log_flash_done', total=total), "SUCCESS") + self.log(self.t('log_flash_effective'), "WARNING") elif success_count > 0: - self.log(f"部分刷入成功({success_count}/{total})", "WARNING") + self.log(self.tf('log_flash_partial', success=success_count, total=total), "WARNING") if not aborted: - self.log("语言包已刷入完成,重启设备后生效,您可在适当时候重启", "WARNING") + self.log(self.t('log_flash_effective'), "WARNING") self.show_progress(False, is_push=True) @@ -1315,48 +1773,68 @@ class ADKAPKGUI: if not self.check_device_connection(): return - apk_dir = filedialog.askdirectory(title="选择包含APK文件的文件夹") + apk_dir = filedialog.askdirectory(title=self.t('file_select_folder_title')) if not apk_dir: return apk_files = list(Path(apk_dir).glob("*.apk")) if not apk_files: - messagebox.showerror("错误", "所选文件夹中没有APK文件!") + messagebox.showerror(self.t('msg_error_title'), self.t('msg_no_apks_in_folder')) return - result = messagebox.askyesno("确认安装", - f"找到 {len(apk_files)} 个APK文件\n\n是否开始批量安装?") + result = messagebox.askyesno( + self.t('msg_install_confirm_title'), + self.tf('msg_install_confirm_folder', count=len(apk_files)) + ) if not result: return def install(): self.show_progress(True, is_push=True) total = len(apk_files) - self.log(f"开始批量安装 {total} 个APK...", "INFO") + self.log(self.tf('log_batch_install_start', count=total), "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) + self.update_progress(i, total, self.t('progress_installing'), 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) + self.update_progress(total, total, self.t('progress_install_done'), is_push=True) if success_count == total: - self.log(f"安装完成:全部 {total} 个成功", "SUCCESS") - self.run_on_ui_thread(messagebox.showinfo, "安装完成", f"成功安装 {total} 个APK!") + self.log(self.tf('log_install_done_all', count=total), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('msg_install_done_title'), + self.tf('msg_install_done_all', count=total) + ) 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}") + self.log(self.tf('log_install_done_partial', success=success_count, count=total), "WARNING") + self.run_on_ui_thread( + messagebox.showwarning, + self.t('msg_install_partial_title'), + self.tf('msg_install_partial', success=success_count, failed=total - success_count) + ) else: - self.log("安装失败", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "安装失败", "所有APK安装失败!") + self.log(self.t('log_install_failed'), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_install_failed_title'), + self.t('msg_install_failed_all') + ) except Exception as e: - self.log(f"安装过程异常: {str(e)}", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "安装失败", f"安装过程异常:{str(e)}") + if self.debug_mode: + self.log(f"Install exception: {str(e)}", "ERROR") + self.log(self.t('log_install_exception'), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_install_failed_title'), + self.t('msg_install_exception') + ) finally: self.run_adb_command('adb -d shell setprop vecentek.model 0') self.show_progress(False, is_push=True) @@ -1370,8 +1848,8 @@ class ADKAPKGUI: return file_path = filedialog.askopenfilename( - title="选择APK文件", - filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + title=self.t('file_select_apk_title'), + filetypes=[(self.t('filetype_apk'), "*.apk"), (self.t('filetype_all'), "*.*")] ) if not file_path: @@ -1379,17 +1857,19 @@ class ADKAPKGUI: def install(): self.show_progress(True, is_push=True) - self.update_progress(50, 100, f"安装中", is_push=True) + self.update_progress(50, 100, self.t('progress_installing'), 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) + self.update_progress(100, 100, self.t('progress_done'), is_push=True) if success: - self.log("✓ 安装成功", "SUCCESS") + self.log(self.t('log_install_success'), "SUCCESS") else: - self.log("✗ 安装失败", "ERROR") + self.log(self.t('log_install_failed'), "ERROR") except Exception as e: - self.log(f"安装过程异常: {str(e)}", "ERROR") + if self.debug_mode: + self.log(f"Install exception: {str(e)}", "ERROR") + self.log(self.t('log_install_exception'), "ERROR") finally: self.run_adb_command('adb -d shell setprop vecentek.model 0') self.show_progress(False, is_push=True) @@ -1410,7 +1890,7 @@ class ADKAPKGUI: # 创建弹窗 popup = tk.Toplevel(self.root) - popup.title("快捷语言设置") + popup.title(self.t('quick_lang_title')) popup.geometry("520x320") popup.configure(bg=self.colors['bg_dark']) popup.resizable(False, False) @@ -1424,29 +1904,21 @@ class ADKAPKGUI: popup.grab_set() # 标题 - header = tk.Label(popup, text="选择目标语言", + header = tk.Label(popup, text=self.t('quick_lang_header'), font=('Microsoft YaHei', 13, 'bold'), fg=self.colors['accent'], bg=self.colors['bg_dark']) header.pack(pady=(15, 10)) - hint = tk.Label(popup, text="点击按钮即可将系统语言切换为对应语言,重启后生效", + hint = tk.Label(popup, text=self.t('quick_lang_hint'), font=('Microsoft YaHei', 9), fg=self.colors['text_secondary'], bg=self.colors['bg_dark']) hint.pack(pady=(0, 12)) # 语言列表:(显示名, locale_code) - 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"), - ] + language_codes = ["zh-CN", "en-US", "ru-RU", "fr-FR", "es-ES", "pt-BR", "it-IT", "ar-SA"] + languages = list(zip(self.t('quick_lang_names'), language_codes)) # 创建按钮容器 btn_frame = tk.Frame(popup, bg=self.colors['bg_dark']) @@ -1480,7 +1952,7 @@ class ADKAPKGUI: 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="⚙️ 打开系统语言设置(手动选择)", + sys_btn = tk.Button(popup, text=self.t('quick_lang_system'), command=lambda: self._open_sys_and_close(popup), font=('Microsoft YaHei', 9), fg=self.colors['text_secondary'], @@ -1494,21 +1966,27 @@ class ADKAPKGUI: popup.destroy() def do_set(): - self.log(f"正在设置系统语言为: {language_name} ({locale_code})", "INFO") + self.log(self.tf('log_quick_lang_setting', language=language_name, locale=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.log(self.tf('log_quick_lang_success', language=language_name), "SUCCESS") self.run_on_ui_thread( messagebox.showinfo, - "设置成功", - f"系统语言已设置为 {language_name}\n\n⚠️ 请重启设备使其生效。" + self.t('quick_lang_success_title'), + self.tf('quick_lang_success', language=language_name) ) else: - self.log(f"✗ 语言设置失败: {output}", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "设置失败", f"语言设置失败!\n\n{output}") + if self.debug_mode: + self.log(f"Language setting failed: {output}", "ERROR") + self.log(self.t('log_quick_lang_failed'), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('quick_lang_failed_title'), + self.t('quick_lang_failed') + ) threading.Thread(target=do_set, daemon=True).start() @@ -1533,10 +2011,10 @@ class ADKAPKGUI: """重启设备""" if not self.check_device_connection(): return - if messagebox.askyesno("确认重启", "确定要重启设备吗?"): + if messagebox.askyesno(self.t('msg_reboot_title'), self.t('msg_reboot_confirm')): subprocess.Popen(f'{self._adb_cmd()} -d shell reboot', shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - self.log("设备正在重启...", "INFO") + self.log(self.t('log_rebooting'), "SUCCESS") self.update_device_status(False) def on_disable_upgrade(self): @@ -1547,15 +2025,12 @@ class ADKAPKGUI: # 弹窗确认 result = messagebox.askyesno( - "确认禁用升级", - "⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n" - "是否确定要禁用系统升级应用?\n\n" - "禁用命令:\n" - "adb -d shell pm disable-user --user 0 com.incall.apps.softmanager" + self.t('msg_disable_ota_title'), + self.t('msg_disable_ota_confirm') ) if not result: - self.log("已取消禁用升级操作", "INFO") + self.log(self.t('log_disable_ota_cancelled'), "INFO") return def disable(): @@ -1563,32 +2038,42 @@ class ADKAPKGUI: 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, "成功", "系统升级已成功禁用!") + self.log(self.t('log_disable_ota_success'), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('msg_success_title'), + self.t('msg_disable_ota_success') + ) else: - self.log("禁用系统升级失败", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "错误", f"禁用失败:{output}") + if self.debug_mode: + self.log(f"Disable OTA failed: {output}", "ERROR") + self.log(self.t('log_disable_ota_failed'), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_error_title'), + self.t('msg_disable_ota_failed') + ) 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": + if self.is_placeholder_vin(self.vin_input.get()): self.vin_input.delete(0, tk.END) self.vin_input.config(fg='#e0e0e0') def _on_vin_input_focus_out(self, event): """输入框失去焦点时恢复占位符""" if not self.vin_input.get(): - self.vin_input.insert(0, "请输入VIN") + self.vin_input.insert(0, self.t('vin_placeholder')) self.vin_input.config(fg='#636e72') def query_password_by_vin(self): """通过VIN查询密码""" vin = self.vin_input.get().strip() - if not vin: - messagebox.showwarning("提示", "请输入VIN码") + if not vin or self.is_placeholder_vin(vin): + messagebox.showwarning(self.t('msg_hint_title'), self.t('msg_enter_vin')) return def do_query(): @@ -1602,29 +2087,35 @@ class ADKAPKGUI: def update_ui(): if data.get('success'): - pwd = data.get('data', {}).get('devicePassword', '未知') + pwd = data.get('data', {}).get('devicePassword', self.t('unknown_error')) self.pwd_result_label.config( - text=f"密码: *#{pwd}#*", + text=self.tf('pwd_success', password=pwd), fg=self.colors['success'] ) - self.log(f"密码查询成功 VIN={vin} -> {pwd}", "SUCCESS") + if self.debug_mode: + self.log(f"Password query succeeded VIN={vin} password={pwd}", "CMD") + self.log(self.tf('log_pwd_success', vin=vin), "SUCCESS") else: - msg = data.get('message', '查询失败') + msg = data.get('message', self.t('log_pwd_failed')) self.pwd_result_label.config( - text=f"失败: {msg}", + text=self.tf('pwd_failed', message=msg), fg=self.colors['error'] ) - self.log(f"密码查询失败: {msg}", "ERROR") + if self.debug_mode: + self.log(f"Password query failed: {msg}", "ERROR") + self.log(self.t('log_pwd_failed'), "ERROR") self.run_on_ui_thread(update_ui) except Exception as e: def update_ui_error(): self.pwd_result_label.config( - text=f"请求失败", + text=self.t('pwd_request_failed'), fg=self.colors['error'] ) - self.log(f"密码查询请求失败: {str(e)}", "ERROR") + if self.debug_mode: + self.log(f"Password query request failed: {str(e)}", "ERROR") + self.log(self.t('log_pwd_request_failed'), "ERROR") self.run_on_ui_thread(update_ui_error) threading.Thread(target=do_query, daemon=True).start() @@ -1633,27 +2124,51 @@ class ADKAPKGUI: """切换调试模式(隐藏入口,Ctrl+Shift+D)""" if self.debug_mode: self.debug_mode = False - self.log("调试模式已关闭", "WARNING") - self.status_text.config(text="就绪") + self.log(self.t('log_debug_off'), "WARNING") + self.status_text.config(text=self.t('status_ready')) 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("错误", "密码错误") + pwd = simpledialog.askstring(self.t('debug_title'), self.t('debug_prompt'), show='*', parent=self.root) + if not pwd: + return + + self.log(self.t('debug_password_verifying'), "WARNING") + + def verify(): + valid, message = self.verify_debug_mode_password(pwd) + if valid: + def enable_debug(): + self.debug_mode = True + self.update_device_status(True, "", True) + self.log(self.t('log_debug_on'), "WARNING") + self.status_text.config(text=self.t('debug_status')) + self.run_on_ui_thread(enable_debug) + else: + def show_failed(): + msg = message or self.t('msg_debug_wrong_password') + self.log(self.tf('debug_verify_failed', message=msg), "WARNING") + messagebox.showwarning(self.t('msg_error_title'), msg) + self.run_on_ui_thread(show_failed) + + threading.Thread(target=verify, daemon=True).start() + + def verify_debug_mode_password(self, password): + try: + data = self._post_json(self.debug_password_api_url, {"password": password}) + if data.get('success') is True and data.get('valid') is True: + return True, data.get('message', '') + return False, data.get('message') or self.t('msg_debug_wrong_password') + except Exception as e: + return False, str(e) def _debug_test_extract(self, event=None): """调试模式下仅测试资源包解压,不检查设备和授权""" if not self.debug_mode: - messagebox.showwarning("调试模式", "请先按 Ctrl+Shift+D 开启调试模式") + messagebox.showwarning(self.t('debug_title'), self.t('debug_need_enable')) return - pwd = simpledialog.askstring("测试解压", "请输入 package.bin 解压密码:", show='*', parent=self.root) + pwd = simpledialog.askstring(self.t('debug_extract_title'), self.t('debug_extract_prompt'), show='*', parent=self.root) if not pwd: return @@ -1666,15 +2181,19 @@ class ADKAPKGUI: try: self.show_progress(True, is_push=False) if self.extract_package_silent(): - self.log("测试解压成功", "SUCCESS") + self.log(self.t('log_debug_extract_success'), "SUCCESS") self.run_on_ui_thread( messagebox.showinfo, - "测试解压成功", - f"资源已解压到:\n{self.temp_dir}" + self.t('debug_extract_success_title'), + self.tf('debug_extract_success', path=self.temp_dir) ) else: - self.log("测试解压失败", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "测试解压失败", "请查看日志中的 7za 输出") + self.log(self.t('log_debug_extract_failed'), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('debug_extract_failed_title'), + self.t('debug_extract_failed') + ) finally: self.show_progress(False, is_push=False) self.extract_password = old_password @@ -1690,27 +2209,30 @@ class ADKAPKGUI: return file_paths = filedialog.askopenfilenames( - title="选择APK文件", - filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + title=self.t('file_select_apk_title'), + filetypes=[(self.t('filetype_apk'), "*.apk"), (self.t('filetype_all'), "*.*")] ) if not file_paths: return count = len(file_paths) - result = messagebox.askyesno("确认安装", f"已选择 {count} 个APK文件\n\n是否开始安装?") + result = messagebox.askyesno( + self.t('msg_install_confirm_title'), + self.tf('msg_install_confirm_many', count=count) + ) if not result: return def install(): self.show_progress(True, is_push=True) - self.log(f"开始安装 {count} 个APK...", "INFO") + self.log(self.tf('log_install_many_start', count=count), "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) + self.update_progress(i, count, self.tf('progress_installing_name', name=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") @@ -1718,20 +2240,38 @@ class ADKAPKGUI: else: self.log(f"✗ {apk_name}.apk", "ERROR") - self.update_progress(count, count, "安装完成", is_push=True) + self.update_progress(count, count, self.t('progress_install_done'), is_push=True) if success_count == count: - self.log(f"安装完成:全部 {count} 个成功", "SUCCESS") - self.run_on_ui_thread(messagebox.showinfo, "安装完成", f"成功安装 {count} 个APK!") + self.log(self.tf('log_install_done_all', count=count), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('msg_install_done_title'), + self.tf('msg_install_done_all', count=count) + ) 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}") + self.log(self.tf('log_install_done_partial', success=success_count, count=count), "WARNING") + self.run_on_ui_thread( + messagebox.showwarning, + self.t('msg_install_partial_title'), + self.tf('msg_install_partial', success=success_count, failed=count - success_count) + ) else: - self.log("安装失败", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "安装失败", "所有APK安装失败!") + self.log(self.t('log_install_failed'), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_install_failed_title'), + self.t('msg_install_failed_all') + ) except Exception as e: - self.log(f"安装过程异常: {str(e)}", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "安装失败", f"安装过程异常:{str(e)}") + if self.debug_mode: + self.log(f"Install exception: {str(e)}", "ERROR") + self.log(self.t('log_install_exception'), "ERROR") + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_install_failed_title'), + self.t('msg_install_exception') + ) finally: self.run_adb_command('adb -d shell setprop vecentek.model 0') self.show_progress(False, is_push=True) @@ -1745,17 +2285,17 @@ class ADKAPKGUI: def main(): """主函数""" if sys.version_info < (3, 6): - print("错误:需要Python 3.6或更高版本") + print("Error: Python 3.6 or later is required") sys.exit(1) try: app = ADKAPKGUI() app.run() except Exception as e: - print(f"启动失败: {e}") + print(f"Startup failed: {e}") import traceback traceback.print_exc() - messagebox.showerror("错误", f"程序启动失败: {e}") + messagebox.showerror("Error", f"Program startup failed: {e}") if __name__ == "__main__": main() diff --git a/S05/S05.py b/S05/S05.py deleted file mode 100644 index faa516f..0000000 --- a/S05/S05.py +++ /dev/null @@ -1,1615 +0,0 @@ -#!/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_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.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 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} -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 - if has_app or has_priv: - ok, reason = self._validate_extracted_apks() - if not ok: - self.log(f"已解压缓存无效: {reason}", "ERROR") - self._clear_extracted_cache() - return False - return has_app or has_priv - - def _validate_extracted_apks(self): - apks = [] - if self.apps_dir and self.apps_dir.exists(): - apks.extend(self.apps_dir.glob("*.apk")) - if self.priv_apps_dir and self.priv_apps_dir.exists(): - apks.extend(self.priv_apps_dir.glob("*.apk")) - if not apks: - return False, "未找到可用 APK" - zero_apks = [apk.name for apk in apks if apk.stat().st_size <= 0] - if zero_apks: - preview = ", ".join(zero_apks[:5]) - suffix = "..." if len(zero_apks) > 5 else "" - return False, f"发现 0KB APK: {preview}{suffix}" - return True, "" - - def _clear_extracted_cache(self): - if self.temp_dir and self.temp_dir.exists(): - shutil.rmtree(self.temp_dir, ignore_errors=True) - time.sleep(0.5) - self.apps_dir = None - self.priv_apps_dir = None - - def _format_extract_error(self, err_msg, return_code): - text = (err_msg or "").lower() - if any(marker in text for marker in ( - "wrong password", - "incorrect password", - "password is incorrect", - "data error in encrypted file", - "can not open encrypted archive", - )): - return "解压密码错误,请重新确认 package.bin 密码" - if "data error" in text: - return "资源包数据错误,可能是密码错误或 package.bin 损坏" - if "headers error" in text or "unexpected end" in text: - return "资源包损坏或不完整,请检查 package.bin" - if err_msg.strip(): - return f"解压失败: {err_msg.strip()[:300]}" - return f"解压失败 (返回码 {return_code}),请检查密码是否正确" - - def 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") - - # 使用 7za 解压(不用 text=True 避免编码问题) - 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, - creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 - ) - - if result.returncode != 0: - err_msg = "" - for output in [result.stderr, result.stdout]: - if output: - for enc in ['gbk', 'utf-8']: - try: - err_msg += output.decode(enc, errors='replace') - break - except: - continue - self.log(self._format_extract_error(err_msg, result.returncode), "ERROR") - self._clear_extracted_cache() - 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") - self._clear_extracted_cache() - 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 - ok, reason = self._validate_extracted_apks() - if not ok: - self.log(f"解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。", "ERROR") - self._clear_extracted_cache() - return False - self.log(f"资源准备完成 (app: {apk_count}, priv-app: {priv_count})", "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") - self._clear_extracted_cache() - 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 - ok, reason = self._validate_extracted_apks() - if not ok: - self.log(f"缓存资源无效,已清理: {reason}", "WARNING") - self._clear_extracted_cache() - return - # 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/S05/app.ico b/S05/app.ico new file mode 100644 index 0000000..2d65a13 Binary files /dev/null and b/S05/app.ico differ diff --git a/S05/pack_s05_fixed.bat b/S05/pack_s05_fixed.bat index 700c2c7..41c27fc 100644 --- a/S05/pack_s05_fixed.bat +++ b/S05/pack_s05_fixed.bat @@ -1,96 +1,160 @@ @echo off -chcp 65001 >nul +setlocal EnableExtensions cd /d "%~dp0" + set "ROOT=%~dp0.." set "TOOLS=%ROOT%\tools" -set NAME=深蓝S05刷入工具_fixed -set SRC=S05_fixed.py -title %NAME% - Build +set "NAME=Deepal_S05_Installer" +set "SRC=Deepal_S05.py" +set "ICON=%~dp0app.ico" +title %NAME% - Cython Build echo ============================================================ echo %NAME% - Cython Build echo ============================================================ echo. -where python >nul 2>&1 +where python >nul 2>nul if errorlevel 1 ( echo [ERROR] Python not found pause - exit /b + exit /b 1 ) -for /f "delims=" %%i in ('where python') do set PY=%%i +for /f "delims=" %%i in ('where python') do set "PY=%%i" echo Python: %PY% +if not exist "%SRC%" ( + echo [ERROR] Source not found: %SRC% + pause + exit /b 1 +) +if not exist "%ICON%" ( + echo [ERROR] Icon not found: %ICON% + pause + exit /b 1 +) +if not exist "%TOOLS%\adb.exe" ( + echo [ERROR] adb.exe not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\AdbWinApi.dll" ( + echo [ERROR] AdbWinApi.dll not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\AdbWinUsbApi.dll" ( + echo [ERROR] AdbWinUsbApi.dll not found in %TOOLS% + pause + exit /b 1 +) +if not exist "%TOOLS%\7za.exe" ( + echo [ERROR] 7za.exe not found in %TOOLS% + pause + exit /b 1 +) + echo [1/6] Installing deps... -%PY% -m pip install pyinstaller cython pyzipper -q +"%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 + "%PY%" -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple +) +if errorlevel 1 ( + echo [ERROR] Dependency install failed + pause + exit /b 1 ) 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 +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 +if exist "%NAME%.spec" del /q "%NAME%.spec" 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 +mkdir "dist_cy" 2>nul +copy "%SRC%" "dist_cy\_core.py" >nul if errorlevel 1 ( - cd .. - echo [WARN] Cython failed, fallback - goto :NORMAL + echo [ERROR] Copy source failed + pause + exit /b 1 ) -for %%f in (_core*.pyd) do set PYD=%%f +"%PY%" -c "open('dist_cy/setup_cython.py','w',encoding='utf-8').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')" +if errorlevel 1 ( + echo [ERROR] Failed to create Cython setup script + pause + exit /b 1 +) + +cd /d "dist_cy" +"%PY%" "setup_cython.py" build_ext --inplace +if errorlevel 1 ( + cd /d "%~dp0" + echo [ERROR] Cython failed. Build stopped. + pause + exit /b 1 +) + +set "PYD=" +for %%f in (_core*.pyd) do set "PYD=%%f" if "%PYD%"=="" ( - cd .. - echo [WARN] No pyd, fallback - goto :NORMAL + cd /d "%~dp0" + echo [ERROR] No Cython PYD generated. Build stopped. + pause + exit /b 1 ) echo PYD: %PYD% -copy "%PYD%" _core.pyd >nul +copy "%PYD%" "_core.pyd" >nul +if errorlevel 1 ( + cd /d "%~dp0" + echo [ERROR] Failed to copy Cython PYD + pause + exit /b 1 +) -%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')" +"%PY%" -c "open('launcher.py','w',encoding='utf-8').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')" +if errorlevel 1 ( + cd /d "%~dp0" + echo [ERROR] Failed to create launcher.py + pause + exit /b 1 +) echo [4/6] Copy resources... copy "%TOOLS%\adb.exe" . >nul copy "%TOOLS%\AdbWinApi.dll" . >nul copy "%TOOLS%\AdbWinUsbApi.dll" . >nul copy "%TOOLS%\7za.exe" . >nul -if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul +copy "%ICON%" . >nul +if errorlevel 1 ( + cd /d "%~dp0" + echo [ERROR] Copy resources failed + pause + exit /b 1 +) echo [5/6] PyInstaller... -%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 +"%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-data "app.ico;." --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 .. + cd /d "%~dp0" echo [ERROR] PyInstaller failed pause - exit /b + exit /b 1 ) 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 +del /q "_core.py" "_core.c" "_core.pyd" "%PYD%" "launcher.py" "setup_cython.py" "app.ico" 2>nul +rmdir /s /q "build" 2>nul +cd /d "%~dp0" -:NORMAL -echo [INFO] Normal PyInstaller... -%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 + echo [ERROR] Output exe was not generated + pause + exit /b 1 ) pause diff --git a/UNI-T/UNI-T-multi-lan-installer.py b/UNI-T/UNI-T-multi-lan-installer.py new file mode 100644 index 0000000..2c13a05 --- /dev/null +++ b/UNI-T/UNI-T-multi-lan-installer.py @@ -0,0 +1,2112 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import subprocess +import json +import re +import threading +import tkinter as tk +import atexit +from tkinter import ttk, scrolledtext, filedialog, messagebox, simpledialog +from pathlib import Path +from urllib.request import urlopen, Request +from urllib.error import URLError, HTTPError +from urllib.parse import urlencode +from datetime import datetime +import zipfile +try: + import pyzipper +except ImportError: + pyzipper = None +import shutil +import time + + +def get_app_dir(): + return Path(sys.executable).resolve().parent if getattr(sys, 'frozen', False) else Path(__file__).resolve().parent + + +def resource_candidates(file_name): + base_dir = get_app_dir() + candidates = [] + if getattr(sys, 'frozen', False): + candidates.append(Path(getattr(sys, '_MEIPASS', base_dir)) / file_name) + candidates.extend([ + base_dir / file_name, + base_dir / 'tools' / file_name, + base_dir / 'shared' / file_name, + base_dir.parent / 'tools' / file_name, + base_dir.parent / 'shared' / file_name, + base_dir.parent / file_name, + ]) + unique = [] + for candidate in candidates: + if candidate not in unique: + unique.append(candidate) + return unique + + +def find_resource(file_name): + candidates = resource_candidates(file_name) + for candidate in candidates: + if candidate.exists(): + return candidate + return candidates[0] + + +def find_tool(file_name, fallback=None): + path = find_resource(file_name) + if path.exists(): + return str(path) + return fallback or str(path) + + +def set_windows_app_user_model_id(): + if sys.platform != 'win32': + return + try: + import ctypes + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + "yibin.keyi.unit.language.installer" + ) + except Exception: + pass + + +class ADKAPKGUI: + def __init__(self): + set_windows_app_user_model_id() + self.root = tk.Tk() + self.root.title("长安语言安装工具") + self.root.geometry("650x640") + self.root.resizable(True, True) + self.set_window_icon() + + # 固定颜色 + 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.lang = 'zh' + self.T = { + 'zh': { + 'title': '适用于UNI-T多语言安装', + 'btn_root': '🔓 获取权限', + 'btn_push': '📦 刷入语言包', + 'btn_install': '📱 安装App', + 'btn_language': '🌐 语言设置', + 'btn_timezone': '⏰ 时区设置', + 'btn_settings': '⚙️ 安卓设置', + 'btn_reboot': '🔄 重启设备', + 'btn_disable_upgrade': '❌ 禁用升级', + 'btn_clear_log': '🗑 清空日志', + 'btn_debug_extract': '解压测试', + 'device_label': '设备:', + 'vin_label': 'VIN码:', + 'auth_label': '授权:', + 'log_title': '📋 运行日志', + 'status_ready': '就绪', + 'status_connected': '已连接', + 'status_disconnected': '未连接', + 'status_detecting': '未检测', + 'vin_none': '未获取', + 'auth_none': '未验证', + 'auth_yes': '已授权', + 'auth_no': '未授权', + 'btn_refresh': '🔄 检查', + 'hint_factory': '🔧 关闭车辆WI-FI和4G网络,拨号获取的密码进入工程模式', + 'warn_no_device': '设备未连接', + 'warn_connect_first': '请先连接设备并点击「检查」按钮刷新状态!', + 'warn_no_vin': '请先刷新设备状态并获取VIN码', + 'err_auth_fail': '授权失败', + 'err_device_not_auth': '设备未授权', + 'err_resource_fail': '资源准备失败!', + 'err_no_resource_dir': '资源目录未找到', + 'info_flash_start': '开始刷入语言包...', + 'info_flash_done': '语言包刷入完成,重启设备后生效', + 'info_flash_fail': '语言包刷入失败', + 'warn_no_apk': '未找到语言包文件', + 'info_installing': '安装中...', + 'info_install_done': '安装完成', + 'info_log_cleared': '日志已清空', + 'confirm_reboot': '确定要重启设备吗?', + 'info_rebooting': '设备正在重启...', + 'warn_device_disconnected': '设备已断开连接', + 'info_device_connected': '设备已连接', + 'info_checking_auth': '正在验证授权状态...', + 'info_auth_pass': '✅ 授权验证通过!', + 'info_auth_fail': '❌ 授权验证失败', + 'info_preparing': '正在准备资源...', + 'err_no_package': '错误:未找到资源包', + 'err_no_password': '错误:解压密码未设置', + 'err_no_7za': '错误:未找到 7za.exe', + 'err_extract_fail': '解压失败', + 'info_extracting': '资源准备中...', + 'info_extract_done': '资源准备完成', + 'err_extract_user': '资源准备失败,请检查网络连接后重试', + 'warn_no_app_dir': '警告:未找到 app/priv-app 目录', + 'progress_resource_loading': '资源准备中...', + 'progress_resource_done': '资源准备完成', + 'progress_extracting_percent': '资源准备中 {percent}%', + 'progress_flashing': '正在刷入', + 'progress_flash_done': '刷入完成', + 'progress_aborted': '已终止', + 'progress_installing': '安装中', + 'progress_installing_name': '安装中 ({name})', + 'progress_done': '完成', + 'lang_zh': '中', + 'lang_en': 'EN', + 'switch_lang': '语言 / Language', + 'tip_1': '1. 安装语言过程中请保持车辆和电脑的电量充足,不可中途停止。', + 'tip_2': '2. 获取权限以后,车辆自动重启以后再进入语言刷入。', + 'tip_3': '3. 部分语言需要重启后生效,可以一切工作完成以后再重启。', + 'warn_flash_warning': '⚠️ 重要提示', + 'warn_flash_msg': '刷入过程中请勿:\n ● 重启车机\n ● 退出本程序\n ● 关闭电脑\n\n否则可能导致车机系统损坏!', + 'err_wrong_password': '请检查密码是否正确', + 'title_pop_lang': '快捷语言设置', + 'quick_lang_header': '选择目标语言', + 'quick_lang_hint': '点击按钮即可将系统语言切换为对应语言,重启后生效', + 'quick_lang_system': '⚙️ 打开系统语言设置(手动选择)', + 'msg_warn_title': '警告', + 'msg_error_title': '错误', + 'msg_done_title': '完成', + 'msg_success_title': '成功', + 'msg_auth_failed_title': '授权失败', + 'msg_device_unauthorized': '设备未授权', + 'msg_resource_prepare_failed': '资源准备失败!', + 'msg_resource_dir_missing': '资源目录未找到', + 'msg_no_apk_in_folder': '所选文件夹中没有APK文件!', + 'msg_confirm_install_title': '确认安装', + 'msg_confirm_install_many': '已选择 {count} 个APK文件\n\n是否开始安装?', + 'msg_confirm_install_folder': '找到 {count} 个APK文件\n\n是否开始批量安装?', + 'msg_install_success_many': '成功安装 {count} 个APK!', + 'msg_install_partial': '成功: {success}\n失败: {failed}', + 'msg_install_all_failed': '所有APK安装失败!', + 'msg_install_exception': '安装过程异常:{error}', + 'msg_quick_lang_success': '系统语言已设置为 {language}\n\n⚠️ 请重启设备使其生效。', + 'msg_quick_lang_failed': '语言设置失败!\n\n{output}', + 'msg_disable_confirm_title': '确认禁用升级', + 'msg_disable_confirm': '⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n是否确定要禁用系统升级应用?', + 'msg_disable_success': '系统升级已成功禁用!', + 'msg_disable_failed': '禁用失败:{output}', + 'msg_reboot_confirm_title': '确认重启', + 'file_apk': 'APK文件', + 'file_all': '所有文件', + 'dialog_select_apk': '选择APK文件', + 'dialog_select_apk_folder': '选择包含APK文件的文件夹', + 'unknown_error': '未知错误', + 'status_debug': '调试模式', + 'debug_password_prompt': '请输入调试密码:', + 'debug_password_verifying': '正在校验调试密码...', + 'debug_wrong_password': '密码错误', + 'debug_verify_failed': '调试密码校验失败: {message}', + 'debug_need_enable': '请先按 Ctrl+Shift+D 进入调试模式', + 'debug_need_vin': '调试解压测试需要 VIN。请先连接设备刷新,或在调试模式中手动设置 VIN。', + 'log_debug_on': '🔧 调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志', + 'log_debug_off': '调试模式已关闭', + 'log_lang_switched': '语言已切换为中文', + 'log_cache_invalid': '已解压缓存无效: {reason}', + 'log_cache_reuse_invalid': '缓存资源无效,已清理: {reason}', + 'log_package_missing': '未找到资源包文件: {path}', + 'log_adb_missing': '未找到adb命令,请将ADB文件放入本目录', + 'log_device_connected': '设备已连接', + 'log_device_disconnected': '设备已断开连接', + 'log_current_vin': '当前VIN: {vin}', + 'log_vin_unavailable': '无法读取VIN', + 'log_refresh_failed': '刷新设备状态失败: {error}', + 'log_auth_skip_debug': '调试模式:跳过授权', + 'log_auth_checking': '正在验证授权状态...', + 'log_auth_ok': '授权验证通过', + 'log_auth_failed': '授权验证失败', + 'log_vehicle_name': '车型名称: {name}', + 'log_adb_required': '请先连接 ADB 并获取 VIN', + 'log_data_prepare_failed_detail': '资源准备失败: {error}', + 'log_extract_password_missing': '错误:解压密码未设置', + 'log_7za_missing': '错误:未找到 7za.exe ({path})', + 'log_extracted_resource_invalid': '解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。', + 'log_extract_done': '资源准备完成', + 'log_extract_exception': '资源准备失败: {error}', + 'err_extract_wrong_password': '解压密码错误,请重新确认 UNI-T_package.bin 密码', + 'err_extract_data': '资源包数据错误,可能是密码错误或 UNI-T_package.bin 损坏', + 'err_extract_corrupt': '资源包损坏或不完整,请检查 UNI-T_package.bin', + 'err_extract_failed': '解压失败: {error}', + 'err_extract_failed_code': '解压失败 (返回码 {code}),请检查密码是否正确', + 'log_root_failed': '获取 root 失败', + 'log_permission_failed': '获取权限失败', + 'log_permission_reboot_required': '首次获取权限,需要重启设备...', + 'log_permission_rebooting': '设备即将重启,重启后权限生效', + 'log_reboot_failed': '重启失败', + 'log_permission_ok': '已获取权限', + 'log_flash_readonly': '请先点击「获取权限」获取权限后再试', + 'log_flash_complete_count': '刷入完成,共 {count} 个语言包', + 'log_flash_effect_after_reboot': '语言包已刷入完成,重启设备后生效,您可在适当时候重启', + 'log_flash_partial': '部分刷入成功({success}/{total})', + 'log_install_start': '开始安装 {count} 个APK...', + 'log_install_done_all': '安装完成:全部 {count} 个成功', + 'log_install_done_partial': '安装完成:{success}/{total} 成功', + 'log_install_failed': '安装失败', + 'log_install_exception': '安装过程异常: {error}', + 'log_install_success_item': '✓ {name}', + 'log_install_failed_item': '✗ {name}', + 'log_quick_lang_setting': '正在设置系统语言为: {language} ({locale})', + 'log_quick_lang_success': '✓ 语言已设置为 {language}', + 'log_quick_lang_failed': '✗ 语言设置失败: {output}', + 'log_rebooting': '设备正在重启...', + 'log_disable_cancelled': '已取消禁用升级操作', + 'log_disable_success': '系统升级已禁用', + 'log_disable_failed': '禁用系统升级失败', + 'log_package_key_failed': 'package-key 获取失败', + 'log_package_extract_success': '资源准备完成', + 'log_package_extract_failed': 'UNI-T_package.bin 解压测试失败', + 'err_no_usable_apk': '未找到可用 APK', + 'err_zero_apk': '发现 0KB APK: {files}', + 'err_push_failed': 'push失败: {error}', + 'err_cp_failed': 'cp失败: {error}', + }, + 'en': { + 'title': 'UNI-T 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', + 'btn_debug_extract': 'Extract', + 'device_label': 'Device:', + 'vin_label': 'VIN:', + 'auth_label': 'Auth:', + 'log_title': '📋 Log', + 'status_ready': 'Ready', + 'status_connected': 'Connected', + 'status_disconnected': 'Disconnected', + 'status_detecting': 'Detecting', + 'vin_none': 'None', + 'auth_none': 'Unknown', + 'auth_yes': 'Authorized', + 'auth_no': 'Unauthorized', + 'btn_refresh': '🔄 Check', + 'hint_factory': '🔧 Turn off WiFi & 4G, enter factory mode with dial code', + 'warn_no_device': 'Device not connected', + 'warn_connect_first': 'Please connect device and click Check button!', + 'warn_no_vin': 'Please refresh device status and get VIN', + 'err_auth_fail': 'Authorization failed', + 'err_device_not_auth': 'Device not authorized', + 'err_resource_fail': 'Resource preparation failed!', + 'err_no_resource_dir': 'Resource directory not found', + 'info_flash_start': 'Starting language pack flashing...', + 'info_flash_done': 'Flashing complete, reboot device to take effect', + 'info_flash_fail': 'Flashing failed', + 'warn_no_apk': 'No APK files found', + 'info_installing': 'Installing...', + 'info_install_done': 'Install complete', + 'info_log_cleared': 'Log cleared', + 'confirm_reboot': 'Are you sure you want to reboot?', + 'info_rebooting': 'Device rebooting...', + 'warn_device_disconnected': 'Device disconnected', + 'info_device_connected': 'Device connected', + 'info_checking_auth': 'Verifying authorization...', + 'info_auth_pass': '✅ Authorization passed!', + 'info_auth_fail': '❌ Authorization failed', + 'info_preparing': 'Preparing resources...', + 'err_no_package': 'Error: UNI-T_package.bin not found', + 'err_no_password': 'Error: password not set', + 'err_no_7za': 'Error: 7za.exe not found', + 'err_extract_fail': 'Extraction failed', + 'info_extracting': 'Preparing resources...', + 'info_extract_done': 'Resource preparation complete', + 'err_extract_user': 'Resource preparation failed, check network and retry', + 'warn_no_app_dir': 'Warning: app/priv-app directory not found', + 'progress_resource_loading': 'Preparing resources...', + 'progress_resource_done': 'Resources ready', + 'progress_extracting_percent': 'Preparing resources {percent}%', + 'progress_flashing': 'Flashing', + 'progress_flash_done': 'Flash complete', + 'progress_aborted': 'Aborted', + 'progress_installing': 'Installing', + 'progress_installing_name': 'Installing ({name})', + 'progress_done': 'Done', + 'lang_zh': '中', + 'lang_en': 'EN', + 'switch_lang': 'Language', + 'tip_1': '1. Keep the vehicle and PC powered during language installation.', + 'tip_2': '2. After permission is obtained, wait for the vehicle to reboot before flashing.', + 'tip_3': '3. Some languages take effect after reboot; reboot after all work is finished.', + 'warn_flash_warning': '⚠️ Warning', + 'warn_flash_msg': 'During flashing, DO NOT:\n ● Reboot vehicle\n ● Close this app\n ● Power off PC\n\nSystem damage may occur!', + 'err_wrong_password': 'Please check password', + 'title_pop_lang': 'Quick Language Setting', + 'quick_lang_header': 'Select target language', + 'quick_lang_hint': 'Click a button to set the system language. Reboot to apply.', + 'quick_lang_system': '⚙️ Open system language settings', + 'msg_warn_title': 'Warning', + 'msg_error_title': 'Error', + 'msg_done_title': 'Done', + 'msg_success_title': 'Success', + 'msg_auth_failed_title': 'Authorization failed', + 'msg_device_unauthorized': 'Device unauthorized', + 'msg_resource_prepare_failed': 'Resource preparation failed!', + 'msg_resource_dir_missing': 'Resource directory not found', + 'msg_no_apk_in_folder': 'No APK files found in the selected folder!', + 'msg_confirm_install_title': 'Confirm install', + 'msg_confirm_install_many': '{count} APK files selected.\n\nStart installation?', + 'msg_confirm_install_folder': '{count} APK files found.\n\nStart batch installation?', + 'msg_install_success_many': '{count} APKs installed successfully!', + 'msg_install_partial': 'Success: {success}\nFailed: {failed}', + 'msg_install_all_failed': 'All APK installations failed!', + 'msg_install_exception': 'Installation error: {error}', + 'msg_quick_lang_success': 'System language set to {language}.\n\n⚠️ Reboot the device to apply.', + 'msg_quick_lang_failed': 'Language setting failed!\n\n{output}', + 'msg_disable_confirm_title': 'Confirm Disable OTA', + 'msg_disable_confirm': '⚠️ Warning: after disabling OTA, the system will no longer receive updates.\n\nDisable the OTA app?', + 'msg_disable_success': 'System upgrade has been disabled!', + 'msg_disable_failed': 'Disable failed: {output}', + 'msg_reboot_confirm_title': 'Confirm reboot', + 'file_apk': 'APK files', + 'file_all': 'All files', + 'dialog_select_apk': 'Select APK file', + 'dialog_select_apk_folder': 'Select a folder containing APK files', + 'unknown_error': 'unknown error', + 'status_debug': 'Debug mode', + 'debug_password_prompt': 'Enter debug password:', + 'debug_password_verifying': 'Verifying debug password...', + 'debug_wrong_password': 'Wrong password', + 'debug_verify_failed': 'Debug password verification failed: {message}', + 'debug_need_enable': 'Press Ctrl+Shift+D first.', + 'debug_need_vin': 'Extract test needs a VIN. Refresh a connected device or set VIN in debug mode.', + 'log_debug_on': '🔧 Debug mode enabled - authorization and device checks are skipped, detailed ADB logs are shown', + 'log_debug_off': 'Debug mode disabled', + 'log_lang_switched': 'Language switched to English', + 'log_cache_invalid': 'Extract cache invalid: {reason}', + 'log_cache_reuse_invalid': 'Cached resources invalid and cleaned: {reason}', + 'log_package_missing': 'Resource package not found: {path}', + 'log_adb_missing': 'adb not found. Put ADB files in this directory.', + 'log_device_connected': 'Device connected', + 'log_device_disconnected': 'Device disconnected', + 'log_current_vin': 'Current VIN: {vin}', + 'log_vin_unavailable': 'Unable to read VIN', + 'log_refresh_failed': 'Refresh device status failed: {error}', + 'log_auth_skip_debug': 'Debug mode: skip authorization', + 'log_auth_checking': 'Checking authorization...', + 'log_auth_ok': 'Authorization passed', + 'log_auth_failed': 'Authorization failed', + 'log_vehicle_name': 'Vehicle name: {name}', + 'log_adb_required': 'Connect ADB and get VIN first', + 'log_data_prepare_failed_detail': 'Resource preparation failed: {error}', + 'log_extract_password_missing': 'Extraction password is not set', + 'log_7za_missing': '7za.exe not found: {path}', + 'log_extracted_resource_invalid': 'Extracted resources are invalid: {reason}. Flashing stopped. Check the password or package.', + 'log_extract_done': 'Resources ready', + 'log_extract_exception': 'Resource preparation failed: {error}', + 'err_extract_wrong_password': 'Incorrect extraction password. Check the UNI-T_package.bin password.', + 'err_extract_data': 'Package data error. The password may be wrong or UNI-T_package.bin may be damaged.', + 'err_extract_corrupt': 'Package is damaged or incomplete. Check UNI-T_package.bin.', + 'err_extract_failed': 'Extraction failed: {error}', + 'err_extract_failed_code': 'Extraction failed (exit code {code}). Check whether the password is correct.', + 'log_root_failed': 'Failed to get root', + 'log_permission_failed': 'Failed to get permission', + 'log_permission_reboot_required': 'First permission attempt requires a reboot...', + 'log_permission_rebooting': 'Device will reboot; permission takes effect after reboot', + 'log_reboot_failed': 'Reboot failed', + 'log_permission_ok': 'Permission granted', + 'log_flash_readonly': 'Click Get Root first, then try again', + 'log_flash_complete_count': 'Flashing complete, {count} language packages', + 'log_flash_effect_after_reboot': 'Language package flashed. Reboot the device when convenient.', + 'log_flash_partial': 'Partially flashed ({success}/{total})', + 'log_install_start': 'Installing {count} APKs...', + 'log_install_done_all': 'Installation complete: all {count} succeeded', + 'log_install_done_partial': 'Installation complete: {success}/{total} succeeded', + 'log_install_failed': 'Installation failed', + 'log_install_exception': 'Installation error: {error}', + 'log_install_success_item': '✓ {name}', + 'log_install_failed_item': '✗ {name}', + 'log_quick_lang_setting': 'Setting system language to: {language} ({locale})', + 'log_quick_lang_success': '✓ Language set to {language}', + 'log_quick_lang_failed': '✗ Language setting failed: {output}', + 'log_rebooting': 'Device rebooting...', + 'log_disable_cancelled': 'Disable OTA cancelled', + 'log_disable_success': 'System upgrade disabled', + 'log_disable_failed': 'Failed to disable system upgrade', + 'log_package_key_failed': 'package-key fetch failed', + 'log_package_extract_success': 'Resources ready', + 'log_package_extract_failed': 'UNI-T_package.bin extract test failed', + 'err_no_usable_apk': 'No usable APK found', + 'err_zero_apk': '0KB APK found: {files}', + 'err_push_failed': 'push failed: {error}', + 'err_cp_failed': 'cp failed: {error}', + } + } + + # 从 exe/py 所在目录查找资源文件 + self.base_dir = get_app_dir() + self.adb = find_tool('adb.exe', 'adb') + self.sz = find_tool('7za.exe') + self.package_file = self._find_package_file() + 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.debug_password_api_url = "https://api.changan.softwindy.cn/api/authorizations/verify-debug-mode-password" + self.vin = None + self.vehicle_name = "" + self.device_connected = False + self._refreshing = False # 防止并发刷新 + self.debug_mode = False # 调试模式 + atexit.register(self.cleanup_cache_on_exit) + + # 设置样式 + self.setup_styles() + self.setup_ui() + self.root.after(200, self.set_window_icon) + self.center_window() + + # 检查环境 + self.check_environment() + + # 启动设备状态监控 + self.start_device_monitor() + + def set_window_icon(self): + """Set the Tk window/taskbar icon at runtime; PyInstaller --icon only sets the exe file icon.""" + try: + icon_path = find_resource("app.ico") + if icon_path.exists(): + self.root.iconbitmap(str(icon_path)) + self._set_windows_hwnd_icon(icon_path) + except Exception: + pass + + def _set_windows_hwnd_icon(self, icon_path): + if sys.platform != 'win32': + return + try: + import ctypes + user32 = ctypes.windll.user32 + hwnd = self.root.winfo_id() + image_icon = 1 + lr_loadfromfile = 0x00000010 + wm_seticon = 0x0080 + icon_small = 0 + icon_big = 1 + path = str(icon_path) + small = user32.LoadImageW(None, path, image_icon, 16, 16, lr_loadfromfile) + big = user32.LoadImageW(None, path, image_icon, 32, 32, lr_loadfromfile) + if small: + user32.SendMessageW(hwnd, wm_seticon, icon_small, small) + if big: + user32.SendMessageW(hwnd, wm_seticon, icon_big, big) + except Exception: + pass + + def setup_styles(self): + """设置自定义样式""" + style = ttk.Style() + style.theme_use('clam') + + # 配置主颜色 + style.configure('TFrame', background=self.colors['bg_dark']) + style.configure('TLabel', background=self.colors['bg_dark'], foreground=self.colors['text']) + style.configure('TLabelframe', background=self.colors['bg_dark'], foreground=self.colors['text']) + style.configure('TLabelframe.Label', background=self.colors['bg_dark'], foreground=self.colors['accent']) + + # 配置进度条 + style.configure('TProgressbar', + background=self.colors['accent'], + troughcolor=self.colors['bg_light'], + borderwidth=0) + + def setup_ui(self): + """设置UI界面""" + # 配置根窗口 + self.root.title(self.t('title')) + 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=45) + title_frame.pack(fill=tk.X, pady=(0, 10)) + title_frame.pack_propagate(False) + + # 标题 + self.title_label = tk.Label(title_frame, + text="🚀 适用于UNI-T多语言安装", + font=('Microsoft YaHei', 18, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.title_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) + + self.debug_button_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) + self.btn_debug_extract = tk.Button(self.debug_button_frame, text=self.t('btn_debug_extract'), + command=self.debug_test_package_extract, + bg=self.colors['info'], + **btn_params) + self.btn_debug_extract.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') + + self.device_label = tk.Label(status_indicator_frame, text="设备:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.device_label.pack(side=tk.LEFT, padx=(5, 3)) + + self.device_status_label = tk.Label(status_indicator_frame, text=self.t('status_detecting'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + 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) + self.vin_label_title = tk.Label(vin_frame, text="VIN码:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.vin_label_title.pack(side=tk.LEFT) + self.vin_label = tk.Label(vin_frame, text=self.t('vin_none'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + 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) + self.auth_label_title = tk.Label(auth_frame, text="授权:", + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.auth_label_title.pack(side=tk.LEFT) + self.auth_label = tk.Label(auth_frame, text=self.t('auth_none'), + font=('Microsoft YaHei', 9, 'bold'), + fg='#636e72', + bg=self.colors['bg_light'], + anchor='w', width=4) + self.auth_label.pack(side=tk.LEFT, padx=(5, 0)) + + # 刷新按钮 + self.btn_refresh = 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') + self.btn_refresh.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 = [self.t('tip_1'), self.t('tip_2'), self.t('tip_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)) + label = tk.Label(tip_row, text=tip, + font=('Microsoft YaHei', 9), + fg=self.colors['warning'], + bg=self.colors['bg_light'], + wraplength=600, + justify=tk.LEFT) + label.pack(side=tk.LEFT) + setattr(self, f'tip_label_{i + 1}', label) + + # 解压进度条框架 + 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) + + self.log_title_label = tk.Label(log_title_frame, text="📋 运行日志", + font=('Microsoft YaHei', 10, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.log_title_label.pack(side=tk.LEFT, padx=10) + + self.btn_clear = tk.Button(log_title_frame, text="🗑 清空日志", + 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_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.root.protocol("WM_DELETE_WINDOW", self.on_close) + + # 绑定悬停效果 + 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, + self.btn_debug_extract] + + 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, lambda: func(*args, **kwargs)) + + def _adb_cmd(self): + return subprocess.list2cmdline([self.adb]) + + def _find_package_file(self): + unit_package = find_resource("UNI-T_package.bin") + if unit_package.exists(): + return unit_package + legacy_package = find_resource("package.bin") + if legacy_package.exists(): + return legacy_package + return unit_package + + def t(self, key): + """获取翻译文本""" + return self.T.get(self.lang, self.T['zh']).get(key, key) + + def tf(self, key, **kwargs): + try: + return self.t(key).format(**kwargs) + except Exception: + return self.t(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(self.t('log_lang_switched'), "INFO") + + def _refresh_ui_texts(self): + """刷新所有UI文本""" + t = self.t + self.root.title(t('title')) + widgets = [ + (getattr(self, 'title_label', None), 'title', 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_debug_extract', None), 'btn_debug_extract', None), + (getattr(self, 'btn_clear', None), 'btn_clear_log', None), + (getattr(self, 'log_title_label', None), 'log_title', None), + (getattr(self, 'device_label', None), 'device_label', None), + (getattr(self, 'vin_label_title', None), 'vin_label', None), + (getattr(self, 'auth_label_title', None), 'auth_label', None), + (getattr(self, 'btn_refresh', None), 'btn_refresh', None), + (getattr(self, 'hint_label', None), 'hint_factory', None), + (getattr(self, 'tip_label_1', None), 'tip_1', None), + (getattr(self, 'tip_label_2', None), 'tip_2', None), + (getattr(self, 'tip_label_3', None), 'tip_3', None), + ] + for w, key, _ in widgets: + if w: + w.config(text=t(key)) + if getattr(self, 'status_text', None): + self.status_text.config(text=t('status_debug') if self.debug_mode else t('status_ready')) + self.btn_lang_switch.config(text=t('lang_en') if self.lang == 'zh' else t('lang_zh')) + self._update_device_status_impl( + self.device_connected, + self.vin, + getattr(self, '_last_authorized', False) + ) + + def set_debug_buttons_visible(self, visible): + if not hasattr(self, 'debug_button_frame'): + return + if visible: + self.debug_button_frame.pack(pady=(0, 8)) + else: + self.debug_button_frame.pack_forget() + + 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(self.t('info_log_cleared'), "INFO") + + def _show_progress_impl(self, show=True, is_push=False): + """显示/隐藏进度条的实际实现(必须在主线程调用)""" + if is_push: + if show: + self.push_progress_label.pack() + self.push_progress.pack(fill=tk.X, pady=(2, 0)) + self.push_progress['value'] = 0 + else: + self.push_progress_label.pack_forget() + self.push_progress.pack_forget() + else: + if show: + self.progress_label.pack() + self.progress.pack(fill=tk.X, pady=(2, 0)) + self.progress['value'] = 0 + else: + self.progress_label.pack_forget() + self.progress.pack_forget() + + def show_progress(self, show=True, is_push=False): + """显示/隐藏进度条(线程安全)""" + self.run_on_ui_thread(self._show_progress_impl, show, is_push) + + def _update_progress_impl(self, value, max_value=100, label="", is_push=False): + """更新进度条的实际实现(必须在主线程调用)""" + if is_push: + percent = (value / max_value) * 100 + self.push_progress['value'] = percent + self.push_progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)") + else: + percent = (value / max_value) * 100 + self.progress['value'] = percent + self.progress_label.config(text=f"{label}: {value}/{max_value} ({percent:.1f}%)") + self.root.update_idletasks() + + def update_progress(self, value, max_value=100, label="", is_push=False): + """更新进度条(线程安全)""" + self.run_on_ui_thread(self._update_progress_impl, value, max_value, label, is_push) + + def update_device_status(self, connected, vin=None, authorized=False): + """更新设备状态显示(线程安全:立即设状态变量,UI走主线程)""" + self.device_connected = connected + if vin is not None: + self.vin = vin + self.run_on_ui_thread(self._update_device_status_impl, connected, vin, authorized) + + def _update_device_status_impl(self, connected, vin, authorized): + """设备状态UI更新的实际实现(必须在主线程调用)""" + self._last_authorized = authorized + t = self.t + if connected: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['success']) + self.device_status_label.config(text=t('status_connected'), fg=self.colors['success']) + if vin: + self.vin_label.config(text=vin, fg=self.colors['success']) + if authorized: + self.auth_label.config(text=t('auth_yes'), fg=self.colors['success']) + else: + self.auth_label.config(text=t('auth_no'), fg=self.colors['error']) + else: + self.vin_label.config(text=t('vin_none'), fg=self.colors['error']) + self.auth_label.config(text=t('auth_none'), fg=self.colors['error']) + else: + self.status_indicator.itemconfig(self.status_dot, fill=self.colors['error']) + self.device_status_label.config(text=t('status_disconnected'), fg=self.colors['error']) + self.vin_label.config(text=t('vin_none'), fg=self.colors['error']) + self.auth_label.config(text=t('auth_none'), fg=self.colors['error']) + + def check_device_connection(self): + """检查设备是否连接""" + if self.debug_mode: + return True + if not self.device_connected: + messagebox.showwarning(self.t('warn_no_device'), self.t('warn_connect_first')) + 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(self.t('log_device_disconnected'), "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) + + ok_setenforce, _ = self.run_adb_command('adb -d shell setenforce 0') + if not ok_setenforce: + self.log(self.t('log_permission_failed'), "ERROR") + self.show_progress(False, is_push=False) + return + + # 执行 adb -d root + ok_root, out_root = self.run_adb_command('adb -d root') + if not ok_root: + self.log(self.t('log_root_failed'), "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(self.t('log_permission_failed'), "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(self.t('log_permission_reboot_required'), "INFO") + ok, _ = self.run_adb_command('adb -d shell reboot') + if ok: + self.log(self.t('log_permission_rebooting'), "INFO") + self.update_device_status(False) + else: + self.log(self.t('log_reboot_failed'), "ERROR") + else: + self.log(self.t('log_permission_ok'), "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 + if has_app or has_priv: + ok, reason = self._validate_extracted_apks() + if not ok: + self.log(self.tf('log_cache_invalid', reason=reason), "ERROR") + self._clear_extracted_cache() + return False + return has_app or has_priv + + def _validate_extracted_apks(self): + apks = [] + if self.apps_dir and self.apps_dir.exists(): + apks.extend(self.apps_dir.glob("*.apk")) + if self.priv_apps_dir and self.priv_apps_dir.exists(): + apks.extend(self.priv_apps_dir.glob("*.apk")) + if not apks: + return False, self.t('err_no_usable_apk') + + zero_apks = [apk.name for apk in apks if apk.stat().st_size <= 0] + if zero_apks: + preview = ", ".join(zero_apks[:5]) + suffix = "..." if len(zero_apks) > 5 else "" + return False, self.tf('err_zero_apk', files=f"{preview}{suffix}") + return True, "" + + def _clear_extracted_cache(self): + if self.temp_dir and self.temp_dir.exists(): + shutil.rmtree(self.temp_dir, ignore_errors=True) + time.sleep(0.5) + self.apps_dir = None + self.priv_apps_dir = None + + def cleanup_cache_on_exit(self): + self._clear_extracted_cache() + + def on_close(self): + self.cleanup_cache_on_exit() + self.root.destroy() + + def _format_extract_error(self, err_msg, return_code): + text = (err_msg or "").lower() + if any(marker in text for marker in ( + "wrong password", + "incorrect password", + "password is incorrect", + "data error in encrypted file", + "can not open encrypted archive", + )): + return self.t('err_extract_wrong_password') + if "data error" in text: + return self.t('err_extract_data') + if "headers error" in text or "unexpected end" in text: + return self.t('err_extract_corrupt') + if err_msg.strip(): + return self.tf('err_extract_failed', error=err_msg.strip()[:300]) + return self.tf('err_extract_failed_code', code=return_code) + + def _decode_7z_output(self, output): + for enc in ('gbk', 'utf-8'): + try: + return output.decode(enc) + except UnicodeDecodeError: + continue + return output.decode('utf-8', errors='replace') + + def _seven_zip_supports_progress_stream(self): + try: + result = subprocess.run( + [self.sz], + capture_output=True, + text=True, + errors='ignore', + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + return '-bs{o|e|p}' in (result.stdout + result.stderr) + except Exception: + return False + + def _extract_with_7za_progress(self): + self.update_progress(0, 100, self.t('progress_resource_loading')) + cmd = [ + self.sz, 'x', str(self.package_file), + f'-p{self.extract_password}', + f'-o{self.temp_dir}', '-y' + ] + use_progress_switch = self._seven_zip_supports_progress_stream() + if use_progress_switch: + cmd.extend(['-bsp1', '-bso0', '-bse1']) + + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.DEVNULL, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0, + bufsize=0 + ) + + output = bytearray() + last_percent = -1 + while True: + chunk = proc.stdout.read(1) if proc.stdout else b'' + if not chunk: + if proc.poll() is not None: + break + time.sleep(0.05) + continue + + output.extend(chunk) + if len(output) > 60000: + del output[:-60000] + + matches = re.findall(rb'(\d{1,3})%', bytes(output[-512:])) + if matches: + percent = min(100, int(matches[-1])) + if percent != last_percent: + last_percent = percent + self.update_progress( + percent, + 100, + self.tf('progress_extracting_percent', percent=percent) + ) + + return_code = proc.wait() + decoded_output = self._decode_7z_output(bytes(output)) + if return_code == 0: + self.update_progress(100, 100, self.t('progress_resource_done')) + return True, decoded_output + if use_progress_switch and "incorrect command line" in decoded_output.lower(): + return self._extract_with_7za_basic() + return False, decoded_output + + def _extract_with_7za_basic(self): + cmd = [ + self.sz, 'x', str(self.package_file), + f'-p{self.extract_password}', + f'-o{self.temp_dir}', '-y' + ] + result = subprocess.run( + cmd, + capture_output=True, + stdin=subprocess.DEVNULL, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 + ) + decoded_output = self._decode_7z_output(result.stdout + result.stderr) + if result.returncode == 0: + self.update_progress(100, 100, self.t('progress_resource_done')) + return True, decoded_output + return False, decoded_output + + def extract_package_silent(self): + """Extract UNI-T_package.bin silently with progress.""" + if not self.package_file.exists(): + self.log(self.tf('log_package_missing', path=self.package_file), "ERROR") + return False + + if not self.extract_password: + self.log(self.t('log_extract_password_missing'), "ERROR") + return False + + if not os.path.exists(self.sz): + self.log(self.tf('log_7za_missing', path=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_UNI_T" + + if self.temp_dir.exists(): + shutil.rmtree(self.temp_dir, ignore_errors=True) + time.sleep(0.5) + + self.temp_dir.mkdir(parents=True, exist_ok=True) + + if sys.platform == 'win32': + subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True) + subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True) + + self.log(self.t('info_extracting'), "INFO") + + ok, err_msg = self._extract_with_7za_progress() + if not ok: + self.log(self._format_extract_error(err_msg, 1), "ERROR") + self._clear_extracted_cache() + return False + + self.apps_dir = None + 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(self.t('warn_no_app_dir'), "WARNING") + self._clear_extracted_cache() + return False + + ok, reason = self._validate_extracted_apks() + if not ok: + self.log(self.tf('log_extracted_resource_invalid', reason=reason), "ERROR") + self._clear_extracted_cache() + return False + self.log(self.t('log_extract_done'), "SUCCESS") + return True + + except Exception as e: + if getattr(self, 'debug_mode', False): + self.log(self.tf('log_extract_exception', error=str(e)), "ERROR") + import traceback + self.log(traceback.format_exc(), "ERROR") + else: + self.log(self.t('err_extract_user'), "ERROR") + self._clear_extracted_cache() + 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(self.tf('log_package_missing', path=self.package_file), "WARNING") + else: + self._try_reuse_extracted() + else: + self.log(self.t('log_adb_missing'), "ERROR") + except FileNotFoundError: + self.log(self.t('log_adb_missing'), "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_UNI_T" + 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 + ok, reason = self._validate_extracted_apks() + if not ok: + self.log(self.tf('log_cache_reuse_invalid', reason=reason), "WARNING") + self._clear_extracted_cache() + return + # self.log("已复用缓存的资源文件", "INFO") + + def refresh_device_status(self): + """Refresh device status.""" + 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().splitlines() + devices = [line for line in lines[1:] if line.strip() and 'device' in line and 'offline' not in line] + + if devices: + if not was_connected: + self.log(self.t('log_device_connected'), "SUCCESS") + + vin = '' + vin_commands = [ + 'shell getprop persist.vendor.car.VIN', + 'shell settings get global VIN', + 'shell settings get system VIN', + 'shell settings get system ca_vin_info', + ] + for command in vin_commands: + vin_result = subprocess.run( + f'{self._adb_cmd()} -d {command}', + shell=True, capture_output=True, text=True) + vin = vin_result.stdout.strip() + if vin and vin != 'null': + break + vin = '' + if vin: + self.log(self.tf('log_current_vin', vin=vin), "INFO") + authorized = self.check_authorization(vin) + self.update_device_status(True, vin, authorized) + else: + self.log(self.t('log_vin_unavailable'), "WARNING") + self.update_device_status(True, None, False) + else: + if was_connected: + self.log(self.t('log_device_disconnected'), "WARNING") + self.update_device_status(False) + except Exception as e: + self.log(self.tf('log_refresh_failed', error=str(e)), "ERROR") + finally: + self._refreshing = False + + threading.Thread(target=refresh, daemon=True).start() + + def check_authorization(self, vin): + """Check authorization.""" + if getattr(self, 'debug_mode', False): + self.log(self.t('log_auth_skip_debug'), "WARNING") + return True + self.log(self.t('log_auth_checking'), "INFO") + try: + authorized, vehicle_name, _ = self.query_authorization_info(vin) + if authorized: + self.log(self.t('log_auth_ok'), "SUCCESS") + if vehicle_name: + self.vehicle_name = vehicle_name + self.log(self.tf('log_vehicle_name', name=vehicle_name), "INFO") + return True + else: + self.log(self.t('log_auth_failed'), "ERROR") + return False + + except Exception: + self.log(self.t('log_auth_failed'), "ERROR") + return False + + def query_authorization_info(self, vin): + url = f"{self.api_url}?{urlencode({'vin': vin})}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + payload = data.get('data', {}) if isinstance(data, dict) else {} + vehicle_name = payload.get('vehicleName') or payload.get('vehicle_name') or "" + vehicle_name = str(vehicle_name).strip() + if data.get('authorized') is True and vehicle_name: + self.vehicle_name = vehicle_name + return data.get('authorized') is True, vehicle_name, data + + def fetch_package_password(self): + """Fetch package password from server.""" + if not self.vin: + self.log(self.t('log_adb_required'), "ERROR") + return False + + try: + vehicle_name = self.vehicle_name + if not vehicle_name: + authorized, vehicle_name, _ = self.query_authorization_info(self.vin) + if not authorized: + self.log(self.t('log_auth_failed'), "ERROR") + return False + if not vehicle_name: + self.log(self.t('log_auth_failed'), "ERROR") + return False + + pwd_api_url = "https://api.changan.softwindy.cn/api/authorizations/package-key" + url = f"{pwd_api_url}?{urlencode({'vin': self.vin, 'vehicleName': vehicle_name})}" + req = Request(url, method='GET', headers={'User-Agent': 'Mozilla/5.0'}) + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + if data.get('success') and 'data' in data and 'password' in data['data']: + self.extract_password = data['data']['password'] + return True + else: + self.log(self.tf('log_data_prepare_failed_detail', error=data.get('message', self.t('unknown_error'))), "ERROR") + return False + + except Exception as e: + self.log(self.tf('log_data_prepare_failed_detail', error=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, self.tf('err_push_failed', error=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, self.tf('err_cp_failed', error=err) + + return True, "" + + def push_car_system_ui(self, apk_path): + """Push package-root CarSystemUI.apk to system_ext priv-app.""" + temp_apk_path = "/data/local/tmp/CarSystemUI.apk" + target_dir = "/system/system_ext/priv-app/CarSystemUI" + target_apk_path = f"{target_dir}/CarSystemUI.apk" + + ok, err = self.run_adb_command(f'adb -d push "{apk_path}" {temp_apk_path}') + if not ok: + return False, self.tf('err_push_failed', error=err) + + self.run_adb_command(f'adb -d shell mkdir -p {target_dir}') + ok, err = self.run_adb_command(f'adb -d shell cp -f {temp_apk_path} {target_apk_path}') + self.run_adb_command(f'adb -d shell rm -f {temp_apk_path}') + if not ok: + return False, self.tf('err_cp_failed', error=err) + + return True, "" + + def disable_unit_user10_packages_before_flash(self): + packages = [ + "com.wt.roadbook", + "com.thunder.carplay", + "com.tencent.wecarmas", + "com.tencent.qqlive.audiobox", + "com.incall.apps.softmanager", + "com.changan.appmarket", + "com.bytedance.byteautoservice", + ] + for package in packages: + self.run_adb_command(f'adb -d shell pm disable-user --user 10 {package}') + + def push_all_apks(self): + """推送APK到系统分区(支持app和priv-app)""" + if not self.check_device_connection(): + return + if not self.vin and not self.debug_mode: + messagebox.showwarning(self.t('msg_warn_title'), self.t('warn_no_vin')) + return + + messagebox.showwarning(self.t('warn_flash_warning'), self.t('warn_flash_msg')) + + def do_push_all(): + if not self.check_authorization(self.vin): + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized'))) + return + if not self.extract_password: + if not self.fetch_package_password(): + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_prepare_failed'))) + 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(self.t('msg_error_title'), self.t('msg_resource_prepare_failed'))) + return + self.show_progress(False, is_push=False) + + if (not self.apps_dir or not self.apps_dir.exists()) and \ + (not self.priv_apps_dir or not self.priv_apps_dir.exists()): + self.run_on_ui_thread(lambda: messagebox.showerror(self.t('msg_error_title'), self.t('msg_resource_dir_missing'))) + return + + self.show_progress(True, is_push=True) + self.disable_unit_user10_packages_before_flash() + 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 self.temp_dir and self.temp_dir.exists(): + car_system_ui = self.temp_dir / "CarSystemUI.apk" + if car_system_ui.exists(): + all_apks.append((car_system_ui, "car-system-ui")) + + 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(self.t('warn_no_apk'), "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 self.temp_dir and self.temp_dir.exists(): + car_system_ui = self.temp_dir / "CarSystemUI.apk" + if car_system_ui.exists(): + all_apks.append((car_system_ui, "car-system-ui")) + if not all_apks: + self.log(self.t('warn_no_apk'), "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 + if apk_type == "car-system-ui": + ok, err = self.push_car_system_ui(apk_path) + else: + 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(self.t('log_flash_readonly'), "ERROR") + aborted = True + break + self.update_progress(i, total, self.t('progress_flashing'), is_push=True) + + self.update_progress(total, total, self.t('progress_flash_done') if not aborted else self.t('progress_aborted'), is_push=True) + + if success_count == total: + self.log(self.tf('log_flash_complete_count', count=total), "SUCCESS") + self.log(self.t('log_flash_effect_after_reboot'), "WARNING") + elif success_count > 0: + self.log(self.tf('log_flash_partial', success=success_count, total=total), "WARNING") + if not aborted: + self.log(self.t('log_flash_effect_after_reboot'), "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=self.t('dialog_select_apk_folder')) + if not apk_dir: + return + + apk_files = list(Path(apk_dir).glob("*.apk")) + if not apk_files: + messagebox.showerror(self.t('msg_error_title'), self.t('msg_no_apk_in_folder')) + return + + result = messagebox.askyesno( + self.t('msg_confirm_install_title'), + self.tf('msg_confirm_install_folder', count=len(apk_files)) + ) + if not result: + return + + def install(): + self.show_progress(True, is_push=True) + total = len(apk_files) + self.log(self.tf('log_install_start', count=total), "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, self.t('progress_installing'), 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, self.t('progress_done'), is_push=True) + + if success_count == total: + self.log(self.tf('log_install_done_all', count=total), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('info_install_done'), self.tf('msg_install_success_many', count=total)) + elif success_count > 0: + self.log(self.tf('log_install_done_partial', success=success_count, total=total), "WARNING") + self.run_on_ui_thread(messagebox.showwarning, self.t('info_install_done'), self.tf('msg_install_partial', success=success_count, failed=total - success_count)) + else: + self.log(self.t('log_install_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('log_install_failed'), self.t('msg_install_all_failed')) + except Exception as e: + self.log(self.tf('log_install_exception', error=str(e)), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('log_install_failed'), self.tf('msg_install_exception', error=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=self.t('dialog_select_apk'), + filetypes=[(self.t('file_apk'), "*.apk"), (self.t('file_all'), "*.*")] + ) + + if not file_path: + return + + def install(): + self.show_progress(True, is_push=True) + self.update_progress(50, 100, self.t('progress_installing'), 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, self.t('progress_done'), is_push=True) + if success: + self.log(self.t('info_install_done'), "SUCCESS") + else: + self.log(self.t('log_install_failed'), "ERROR") + except Exception as e: + self.log(self.tf('log_install_exception', error=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(self.t('title_pop_lang')) + popup.geometry("520x320") + popup.configure(bg=self.colors['bg_dark']) + popup.resizable(False, False) + + # 居中显示 + popup.update_idletasks() + x = self.root.winfo_x() + (self.root.winfo_width() - 520) // 2 + y = self.root.winfo_y() + (self.root.winfo_height() - 320) // 2 + popup.geometry(f"+{x}+{y}") + popup.transient(self.root) + popup.grab_set() + + # 标题 + header = tk.Label(popup, text=self.t('quick_lang_header'), + font=('Microsoft YaHei', 13, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + header.pack(pady=(15, 10)) + + hint = tk.Label(popup, text=self.t('quick_lang_hint'), + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_dark']) + hint.pack(pady=(0, 12)) + + # 语言列表:(显示名, locale_code) + 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=self.t('quick_lang_system'), + command=lambda: self._open_sys_and_close(popup), + font=('Microsoft YaHei', 9), + fg=self.colors['text_secondary'], + bg=self.colors['bg_light'], + relief=tk.FLAT, + cursor='hand2') + sys_btn.pack(pady=(0, 10)) + + def _quick_set_language(self, locale_code, language_name, popup): + """执行快捷语言设置""" + popup.destroy() + + def do_set(): + self.log(self.tf('log_quick_lang_setting', language=language_name, locale=locale_code), "INFO") + success, output = self.run_adb_command( + f'adb -d shell settings put system system_locales {locale_code}' + ) + + if success: + self.log(self.tf('log_quick_lang_success', language=language_name), "SUCCESS") + self.run_on_ui_thread( + messagebox.showinfo, + self.t('msg_success_title'), + self.tf('msg_quick_lang_success', language=language_name) + ) + else: + self.log(self.tf('log_quick_lang_failed', output=output), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.tf('msg_quick_lang_failed', output=output)) + + threading.Thread(target=do_set, daemon=True).start() + + def _open_sys_and_close(self, popup): + """关闭弹窗并打开系统语言设置""" + popup.destroy() + self.open_language_settings() + + def open_timezone_settings(self): + """打开时区设置""" + if not self.check_device_connection(): + return + self.run_adb_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(self.t('msg_reboot_confirm_title'), self.t('confirm_reboot')): + subprocess.Popen(f'{self._adb_cmd()} -d shell reboot', shell=True, + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + self.log(self.t('log_rebooting'), "INFO") + self.update_device_status(False) + + def on_disable_upgrade(self): + """禁用系统升级""" + # 检查设备连接 + if not self.check_device_connection(): + return + + # 弹窗确认 + result = messagebox.askyesno( + self.t('msg_disable_confirm_title'), + self.t('msg_disable_confirm') + ) + + if not result: + self.log(self.t('log_disable_cancelled'), "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(self.t('log_disable_success'), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('msg_success_title'), self.t('msg_disable_success')) + else: + self.log(self.t('log_disable_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.tf('msg_disable_failed', output=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(self.t('log_debug_off'), "WARNING") + self.status_text.config(text=self.t('status_ready')) + self.set_debug_buttons_visible(False) + self.refresh_device_status() + return + + pwd = simpledialog.askstring(self.t('status_debug'), self.t('debug_password_prompt'), show='*', parent=self.root) + if not pwd: + return + + self.log(self.t('debug_password_verifying'), "INFO") + + def verify(): + valid, message = self.verify_debug_mode_password(pwd) + if valid: + def enable_debug(): + self.debug_mode = True + self.update_device_status(True, "", True) + self.log(self.t('log_debug_on'), "WARNING") + self.status_text.config(text=self.t('status_debug')) + self.set_debug_buttons_visible(True) + self.run_on_ui_thread(enable_debug) + else: + def show_failed(): + msg = message or self.t('debug_wrong_password') + self.log(self.tf('debug_verify_failed', message=msg), "WARNING") + messagebox.showwarning(self.t('msg_error_title'), msg) + self.run_on_ui_thread(show_failed) + + threading.Thread(target=verify, daemon=True).start() + + def verify_debug_mode_password(self, password): + try: + payload = json.dumps({"password": password}).encode('utf-8') + req = Request( + self.debug_password_api_url, + data=payload, + method='POST', + headers={ + 'Content-Type': 'application/json', + 'User-Agent': 'Mozilla/5.0', + } + ) + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + if data.get('success') is True and data.get('valid') is True: + return True, data.get('message', '') + return False, data.get('message') or self.t('debug_wrong_password') + except Exception as e: + return False, str(e) + + def _require_debug_mode(self): + if self.debug_mode: + return True + messagebox.showwarning(self.t('status_debug'), self.t('debug_need_enable')) + return False + + def install_apps(self): + """安装App — 支持单选或多选APK文件""" + if not self.check_device_connection(): + return + if not self.vin and not self.debug_mode: + messagebox.showwarning(self.t('msg_warn_title'), self.t('warn_no_vin')) + return + + file_paths = filedialog.askopenfilenames( + title=self.t('dialog_select_apk'), + filetypes=[(self.t('file_apk'), "*.apk"), (self.t('file_all'), "*.*")] + ) + if not file_paths: + return + + count = len(file_paths) + result = messagebox.askyesno( + self.t('msg_confirm_install_title'), + self.tf('msg_confirm_install_many', count=count) + ) + if not result: + return + + def install(): + if not self.check_authorization(self.vin): + self.run_on_ui_thread( + messagebox.showerror, + self.t('msg_auth_failed_title'), + self.t('msg_device_unauthorized') + ) + return + + self.show_progress(True, is_push=True) + self.log(self.tf('log_install_start', count=count), "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).name + self.update_progress(i, count, self.tf('progress_installing_name', name=apk_name), is_push=True) + success, _ = self.run_adb_command(f'adb -d install -r "{file_path}"') + if success: + self.log(self.tf('log_install_success_item', name=apk_name), "SUCCESS") + success_count += 1 + else: + self.log(self.tf('log_install_failed_item', name=apk_name), "ERROR") + + self.update_progress(count, count, self.t('progress_done'), is_push=True) + + if success_count == count: + self.log(self.tf('log_install_done_all', count=count), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('info_install_done'), self.tf('msg_install_success_many', count=count)) + elif success_count > 0: + self.log(self.tf('log_install_done_partial', success=success_count, total=count), "WARNING") + self.run_on_ui_thread(messagebox.showwarning, self.t('info_install_done'), self.tf('msg_install_partial', success=success_count, failed=count - success_count)) + else: + self.log(self.t('log_install_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('log_install_failed'), self.t('msg_install_all_failed')) + except Exception as e: + self.log(self.tf('log_install_exception', error=str(e)), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('log_install_failed'), self.tf('msg_install_exception', error=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 _debug_test_extract(self, event=None): + self.debug_test_package_extract() + + def debug_test_package_extract(self): + """Debug-only package extraction test using package-key.""" + if not self._require_debug_mode(): + return + + if not self.vin: + vin = simpledialog.askstring(self.t('status_debug'), 'VIN:', parent=self.root) + if vin: + self.vin = vin.strip().upper() + if not self.vin: + messagebox.showwarning(self.t('status_debug'), self.t('debug_need_vin')) + return + + def do_extract(): + old_password = self.extract_password + try: + self.log(self.t('info_extracting'), "INFO") + self.extract_password = None + if not self.fetch_package_password(): + self.log(self.t('log_package_key_failed'), "ERROR") + return + self.show_progress(True, is_push=False) + if self.extract_package_silent(): + self.log(self.t('log_package_extract_success'), "SUCCESS") + else: + self.log(self.t('log_package_extract_failed'), "ERROR") + finally: + self.extract_password = old_password + self.show_progress(False, is_push=False) + + threading.Thread(target=do_extract, daemon=True).start() + + def run(self): + """运行程序""" + self.root.mainloop() + +def main(): + """主函数""" + if sys.version_info < (3, 6): + print("错误:需要Python 3.6或更高版本") + sys.exit(1) + + try: + app = ADKAPKGUI() + app.run() + except Exception as e: + print(f"启动失败: {e}") + import traceback + traceback.print_exc() + messagebox.showerror("错误", f"程序启动失败: {e}") + +if __name__ == "__main__": + main() diff --git a/S05/pack_s05.bat b/UNI-T/pack_unit.bat similarity index 55% rename from S05/pack_s05.bat rename to UNI-T/pack_unit.bat index b03bd4e..a90736c 100644 --- a/S05/pack_s05.bat +++ b/UNI-T/pack_unit.bat @@ -3,8 +3,9 @@ chcp 65001 >nul cd /d "%~dp0" set "ROOT=%~dp0.." set "TOOLS=%ROOT%\tools" -set NAME=深蓝S05刷入工具 -set SRC=S05.py +set "ICON=%~dp0app.ico" +set NAME=UNI-T-multi-lan-installer +set SRC=UNI-T-multi-lan-installer.py title %NAME% - Build echo ============================================================ @@ -27,14 +28,42 @@ if errorlevel 1 ( %PY% -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple ) -echo [2/6] Clean... +echo [2/7] 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... +echo [3/7] Checking resources... +if not exist "%ICON%" ( + echo [ERROR] app.ico not found in UNI-T folder: %ICON% + pause + exit /b 1 +) +if not exist "%TOOLS%\adb.exe" ( + echo [ERROR] Missing ADB file: %TOOLS%\adb.exe + pause + exit /b 1 +) +if not exist "%TOOLS%\AdbWinApi.dll" ( + echo [ERROR] Missing ADB file: %TOOLS%\AdbWinApi.dll + pause + exit /b 1 +) +if not exist "%TOOLS%\AdbWinUsbApi.dll" ( + echo [ERROR] Missing ADB file: %TOOLS%\AdbWinUsbApi.dll + pause + exit /b 1 +) +if not exist "%TOOLS%\7za.exe" ( + echo [ERROR] Missing 7za file: %TOOLS%\7za.exe + pause + exit /b 1 +) + +echo [4/7] Cython compile... mkdir dist_cy 2>nul copy %SRC% dist_cy\_core.py >nul +set "PYD=" %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')" @@ -42,54 +71,50 @@ cd dist_cy %PY% setup_cython.py build_ext --inplace if errorlevel 1 ( cd .. - echo [WARN] Cython failed, fallback - goto :NORMAL + echo [ERROR] Cython build failed. Install Microsoft C++ Build Tools and retry. + pause + exit /b 1 ) for %%f in (_core*.pyd) do set PYD=%%f if "%PYD%"=="" ( cd .. - echo [WARN] No pyd, fallback - goto :NORMAL + echo [ERROR] Cython build did not generate _core*.pyd. Stop. + pause + exit /b 1 ) 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... +echo [5/7] Copy resources... copy "%TOOLS%\adb.exe" . >nul copy "%TOOLS%\AdbWinApi.dll" . >nul copy "%TOOLS%\AdbWinUsbApi.dll" . >nul copy "%TOOLS%\7za.exe" . >nul -if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul +copy "%ICON%" . >nul -echo [5/6] PyInstaller... -%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 +echo [6/7] PyInstaller... +%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ICON%" --add-data "%ICON%;." --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 + exit /b 1 ) -echo [6/6] Cleanup... +echo [7/7] 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 -echo [INFO] Normal PyInstaller... -%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 ) diff --git a/X5plus/X5plusTool.py b/X5plus/X5plusTool.py index 1c93b33..31e0042 100644 --- a/X5plus/X5plusTool.py +++ b/X5plus/X5plusTool.py @@ -7,6 +7,7 @@ import subprocess import json import re import threading +import atexit import tkinter as tk from tkinter import ttk, scrolledtext, filedialog, messagebox, simpledialog from pathlib import Path @@ -59,11 +60,26 @@ def find_tool(file_name, fallback=None): path = find_resource(file_name) if path.exists(): return str(path) - return fallback or str(path) + return fallback or str(path) + + +def set_windows_app_user_model_id(): + if sys.platform != 'win32': + return + try: + import ctypes + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + "yibin.keyi.x5plus.flash.tool" + ) + except Exception: + pass + + class ADKAPKGUI: def __init__(self): + set_windows_app_user_model_id() self.root = tk.Tk() - self.root.title("长安语言安装工具") + self.root.title("X5plus") self.root.geometry("650x640") self.root.resizable(True, True) @@ -101,8 +117,9 @@ class ADKAPKGUI: self.lang = 'zh' self.T = { 'zh': { - 'title': '适用于X5plus多语言安装', + 'title': '长安X5Plus刷机工具', 'btn_push': '📦 刷入语言包', + 'btn_patch_52': '🧩 5.2补丁', 'btn_install': '📱 安装App', 'btn_language': '🌐 语言设置', 'btn_timezone': '⏰ 时区设置', @@ -115,6 +132,7 @@ class ADKAPKGUI: 'auth_label': '授权:', 'log_title': '📋 运行日志', 'status_ready': '就绪', + 'status_debug': '🔧 调试模式', 'status_connected': '已连接', 'status_disconnected': '未连接', 'status_detecting': '未检测', @@ -123,15 +141,145 @@ class ADKAPKGUI: 'auth_yes': '已授权', 'auth_no': '未授权', 'btn_refresh': '🔄 检查', + 'hint_factory_dynamic': '工程模式:拨号 *#*#888 ,工程密码:{password},调试密码:3821', + 'hint_lines': [ + '1. 安装语言过程中请保持车辆和电脑电量充足,不可中途停止。', + '2. 获取权限以后,车辆自动重启以后再进入语言刷入。', + '3. 部分语言需要重启后生效,可以一切工作完成以后再重启。', + ], 'theme_dark': '🌙 暗色', 'theme_light': '☀️ 亮色', 'lang_zh': '中', 'lang_en': 'EN', - 'about_company': '宜宾科宜科技有限公司 - 智能设备管理平台', + 'msg_warn_title': '警告', + 'msg_error_title': '错误', + 'msg_success_title': '成功', + 'msg_done_title': '完成', + 'msg_device_not_connected_title': '设备未连接', + 'msg_device_not_connected': '请先连接设备并点击「检查」按钮刷新状态!', + 'msg_need_vin': '请先刷新设备状态并获取VIN码', + 'msg_auth_failed_title': '授权失败', + 'msg_device_unauthorized': '设备未授权', + 'msg_data_prepare_failed': '资源准备失败!', + 'msg_resource_dir_missing': '资源目录未找到', + 'msg_flash_warning_title': '⚠️ 重要提示', + 'msg_flash_warning': '刷入过程中请勿:\n ● 重启车机\n ● 退出本程序\n ● 关闭电脑\n\n否则可能导致车机系统损坏!', + 'msg_patch_missing': '未找到 5.2 补丁文件', + 'msg_patch_push_failed': '5.2 补丁推送失败', + 'msg_patch_apply_failed': '5.2 补丁刷入失败', + 'msg_patch_done': '5.2 补丁刷入完成,重启设备后生效', + 'msg_no_apks_in_folder': '所选文件夹中没有APK文件!', + 'msg_install_confirm_title': '确认安装', + 'msg_install_confirm_folder': '找到 {count} 个APK文件\n\n是否开始批量安装?', + 'msg_install_confirm_many': '已选择 {count} 个APK文件\n\n是否开始安装?', + 'msg_install_done_title': '安装完成', + 'msg_install_done_all': '成功安装 {count} 个APK!', + 'msg_install_partial_title': '部分成功', + 'msg_install_partial': '成功: {success}\n失败: {failed}', + 'msg_install_failed_title': '安装失败', + 'msg_install_failed_all': '所有APK安装失败!', + 'msg_install_exception': '安装过程异常', + 'file_select_folder_title': '选择包含APK文件的文件夹', + 'file_select_apk_title': '选择APK文件', + 'filetype_apk': 'APK文件', + 'filetype_all': '所有文件', + 'quick_lang_title': '快捷语言设置', + 'quick_lang_header': '选择目标语言', + 'quick_lang_hint': '点击按钮即可将系统语言切换为对应语言,重启后生效', + 'quick_lang_system': '⚙️ 打开系统语言设置(手动选择)', + 'quick_lang_success_title': '设置成功', + 'quick_lang_success': '系统语言已设置为 {language}\n\n⚠️ 请重启设备使其生效。', + 'quick_lang_failed_title': '设置失败', + 'quick_lang_failed': '语言设置失败!', + 'quick_lang_names': ['🇨🇳 中文', '英 English', '俄 Русский', '法 Français', '西 Español', '葡 Português', '意 Italiano', '阿 العربية'], + 'msg_reboot_title': '确认重启', + 'msg_reboot_confirm': '确定要重启设备吗?', + 'msg_disable_ota_title': '确认禁用升级', + 'msg_disable_ota_confirm': '⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n是否确定要禁用系统升级应用?', + 'msg_disable_ota_success': '系统升级已成功禁用!', + 'msg_disable_ota_failed': '禁用失败', + 'debug_title': '调试模式', + 'debug_prompt': '请输入调试密码:', + 'debug_password_verifying': '正在校验调试模式密码...', + 'debug_verify_failed': '调试模式密码校验失败: {message}', + 'msg_debug_wrong_password': '密码错误', + 'debug_need_enable': '请先按 Ctrl+Shift+D 开启调试模式', + 'debug_extract_title': '测试解压', + 'debug_extract_prompt': '请输入 package.bin 解压密码:', + 'debug_extract_success_title': '测试解压成功', + 'debug_extract_success': '资源已解压到:\n{path}', + 'debug_extract_failed_title': '测试解压失败', + 'debug_extract_failed': '请查看日志中的 7za 输出', + 'progress_loading': '资源加载中', + 'progress_loaded': '资源加载完成', + 'progress_flashing': '正在刷入', + 'progress_flash_done': '刷入完成', + 'progress_installing': '安装中', + 'progress_installing_name': '安装中 ({name})', + 'progress_done': '完成', + 'progress_install_done': '安装完成', + 'progress_patch_prepare': '准备刷入 5.2 补丁', + 'progress_patch_push': '推送补丁文件', + 'progress_patch_apply': '应用补丁文件', + 'progress_patch_done': '5.2 补丁完成', + 'log_lang_changed': '语言已切换为中文', + 'log_cleared': '日志已清空', + 'log_device_connected': '设备已连接', + 'log_device_disconnected': '设备未连接', + 'log_vin': 'VIN: {vin}', + 'log_vin_unavailable': '无法获取VIN', + 'log_refresh_failed': '刷新设备状态失败', + 'log_debug_skip_auth': '调试模式: 跳过授权验证', + 'log_auth_checking': '正在验证授权...', + 'log_auth_success': '授权验证通过', + 'log_auth_failed': '授权验证失败', + 'log_vehicle_name': '车辆名称: {vehicle}', + 'log_need_adb': '请先连接adb!', + 'log_data_prepare_failed': '资源准备失败', + 'log_package_missing': '未找到资源包文件', + 'log_adb_missing': '未找到adb命令,请将ADB文件放入本目录', + 'log_cache_invalid': '资源缓存无效', + 'log_resource_invalid': '资源校验失败', + 'log_resource_ready': '资源准备完成', + 'log_resource_failed': '资源准备失败,请检查网络连接后重试', + 'log_resource_dir_missing': '资源目录异常', + 'log_no_language_files': '未找到语言包文件', + 'log_cleanup_start': '正在执行刷入后清理...', + 'log_cleanup_done': '刷入后清理完成', + 'log_cleanup_partial': '刷入后清理完成,部分项目未完全成功', + 'log_flash_start_notice': '开始刷入语言包,请勿断电或重启电脑和车机。', + 'log_flash_done': '语言包刷入完成,共 {total} 个', + 'log_flash_effective': '语言包已刷入完成,重启设备后生效,您可在适当时候重启', + 'log_flash_partial': '部分刷入成功({success}/{total})', + 'log_patch_missing': '未找到 5.2 补丁文件', + 'log_patch_push_failed': '5.2 补丁推送失败', + 'log_patch_apply_failed': '5.2 补丁刷入失败', + 'log_patch_done': '5.2 补丁刷入完成,重启设备后生效', + 'log_batch_install_start': '开始批量安装 {count} 个APK...', + 'log_install_many_start': '开始安装 {count} 个APK...', + 'log_install_done_all': '安装完成:全部 {count} 个成功', + 'log_install_done_partial': '安装完成:{success}/{count} 成功', + 'log_install_success': '安装成功', + 'log_install_failed': '安装失败', + 'log_install_exception': '安装过程异常', + 'log_quick_lang_setting': '正在设置系统语言为: {language} ({locale})', + 'log_quick_lang_success': '语言已设置为 {language}', + 'log_quick_lang_failed': '语言设置失败', + 'log_rebooting': '设备正在重启...', + 'log_disable_ota_cancelled': '已取消禁用升级操作', + 'log_disable_ota_success': '系统升级已禁用', + 'log_disable_ota_failed': '禁用系统升级失败', + 'log_debug_off': '调试模式已关闭', + 'log_debug_on': '调试模式已开启 - 跳过授权和设备校验,显示详细ADB日志', + 'err_extract_default': '资源准备失败', + 'msg_start_failed': '程序启动失败', + 'msg_python_version_error': '错误:需要Python 3.6或更高版本', + 'unknown_error': '未知错误', }, 'en': { - 'title': 'X5plus Multi-Language', + 'title': 'Changan X5Plus Flash Tool', 'btn_push': '📦 Flash Lang Pkg', + 'btn_patch_52': '🧩 5.2 Patch', 'btn_install': '📱 Install App', 'btn_language': '🌐 Language', 'btn_timezone': '⏰ Timezone', @@ -144,6 +292,7 @@ class ADKAPKGUI: 'auth_label': 'Auth:', 'log_title': '📋 Log', 'status_ready': 'Ready', + 'status_debug': '🔧 Debug Mode', 'status_connected': 'Connected', 'status_disconnected': 'Disconnected', 'status_detecting': 'Detecting', @@ -152,11 +301,140 @@ class ADKAPKGUI: 'auth_yes': 'Authorized', 'auth_no': 'Unauthorized', 'btn_refresh': '🔄 Check', + 'hint_factory_dynamic': 'Factory mode: dial *#*#888, factory password: {password}, debug password: 3821', + 'hint_lines': [ + '1. Keep the vehicle and computer powered during language installation.', + '2. After getting permission, wait for the vehicle to reboot before flashing.', + '3. Some languages apply after reboot; reboot after all work is complete.', + ], 'theme_dark': '🌙 Dark', 'theme_light': '☀️ Light', 'lang_zh': '中', 'lang_en': 'EN', - 'about_company': 'Yibin Keyi Technology - Smart Device Platform', + 'msg_warn_title': 'Warning', + 'msg_error_title': 'Error', + 'msg_success_title': 'Success', + 'msg_done_title': 'Done', + 'msg_device_not_connected_title': 'Device not connected', + 'msg_device_not_connected': 'Connect the device and click "Check" first.', + 'msg_need_vin': 'Refresh device status and get VIN first', + 'msg_auth_failed_title': 'Authorization failed', + 'msg_device_unauthorized': 'Device is not authorized', + 'msg_data_prepare_failed': 'Resource preparation failed!', + 'msg_resource_dir_missing': 'Resource directory not found', + 'msg_flash_warning_title': 'Important warning', + 'msg_flash_warning': 'During flashing, do not:\n - reboot the head unit\n - close this program\n - shut down the computer\n\nOtherwise the system may be damaged.', + 'msg_patch_missing': '5.2 patch file not found', + 'msg_patch_push_failed': '5.2 patch push failed', + 'msg_patch_apply_failed': '5.2 patch failed', + 'msg_patch_done': '5.2 patch completed. Reboot the device to apply it.', + 'msg_no_apks_in_folder': 'No APK files found in the selected folder.', + 'msg_install_confirm_title': 'Confirm install', + 'msg_install_confirm_folder': 'Found {count} APK file(s).\n\nStart batch install?', + 'msg_install_confirm_many': 'Selected {count} APK file(s).\n\nStart installing?', + 'msg_install_done_title': 'Install complete', + 'msg_install_done_all': 'Successfully installed {count} APK file(s).', + 'msg_install_partial_title': 'Partially complete', + 'msg_install_partial': 'Succeeded: {success}\nFailed: {failed}', + 'msg_install_failed_title': 'Install failed', + 'msg_install_failed_all': 'All APK installs failed.', + 'msg_install_exception': 'Install process exception', + 'file_select_folder_title': 'Select folder containing APK files', + 'file_select_apk_title': 'Select APK file', + 'filetype_apk': 'APK files', + 'filetype_all': 'All files', + 'quick_lang_title': 'Quick Language', + 'quick_lang_header': 'Select target language', + 'quick_lang_hint': 'Click a button to switch the system language. Reboot to apply.', + 'quick_lang_system': '⚙️ Open system language settings', + 'quick_lang_success_title': 'Success', + 'quick_lang_success': 'System language has been set to {language}\n\n⚠️ Reboot the device to apply it.', + 'quick_lang_failed_title': 'Failed', + 'quick_lang_failed': 'Language setup failed!', + 'quick_lang_names': ['🇨🇳 Chinese', '英 English', '俄 Russian', '法 French', '西 Spanish', '葡 Portuguese', '意 Italian', '阿 Arabic'], + 'msg_reboot_title': 'Confirm reboot', + 'msg_reboot_confirm': 'Reboot the device now?', + 'msg_disable_ota_title': 'Confirm OTA disable', + 'msg_disable_ota_confirm': '⚠️ Warning: after disabling OTA, the system will not receive updates.\n\nDisable the system update app now?', + 'msg_disable_ota_success': 'System update has been disabled.', + 'msg_disable_ota_failed': 'Disable failed', + 'debug_title': 'Debug Mode', + 'debug_prompt': 'Enter debug password:', + 'debug_password_verifying': 'Verifying debug password...', + 'debug_verify_failed': 'Debug password verification failed: {message}', + 'msg_debug_wrong_password': 'Wrong password', + 'debug_need_enable': 'Press Ctrl+Shift+D to enable debug mode first', + 'debug_extract_title': 'Test Extraction', + 'debug_extract_prompt': 'Enter package.bin password:', + 'debug_extract_success_title': 'Test extraction succeeded', + 'debug_extract_success': 'Resources extracted to:\n{path}', + 'debug_extract_failed_title': 'Test extraction failed', + 'debug_extract_failed': 'Check the 7za output in logs', + 'progress_loading': 'Preparing resources', + 'progress_loaded': 'Resources ready', + 'progress_flashing': 'Flashing', + 'progress_flash_done': 'Flash complete', + 'progress_installing': 'Installing', + 'progress_installing_name': 'Installing ({name})', + 'progress_done': 'Done', + 'progress_install_done': 'Install complete', + 'progress_patch_prepare': 'Preparing 5.2 patch', + 'progress_patch_push': 'Pushing patch file', + 'progress_patch_apply': 'Applying patch file', + 'progress_patch_done': '5.2 patch complete', + 'log_lang_changed': 'Language switched to English', + 'log_cleared': 'Log cleared', + 'log_device_connected': 'Device connected', + 'log_device_disconnected': 'Device disconnected', + 'log_vin': 'VIN: {vin}', + 'log_vin_unavailable': 'Unable to read VIN', + 'log_refresh_failed': 'Failed to refresh device status', + 'log_debug_skip_auth': 'Debug mode: authorization skipped', + 'log_auth_checking': 'Checking authorization...', + 'log_auth_success': 'Authorization passed', + 'log_auth_failed': 'Authorization failed', + 'log_vehicle_name': 'Vehicle name: {vehicle}', + 'log_need_adb': 'Connect ADB first!', + 'log_data_prepare_failed': 'Resource preparation failed', + 'log_package_missing': 'Resource file not found', + 'log_adb_missing': 'ADB not found. Put ADB files in this folder.', + 'log_cache_invalid': 'Resource cache is invalid', + 'log_resource_invalid': 'Resource validation failed', + 'log_resource_ready': 'Resources ready', + 'log_resource_failed': 'Resource preparation failed. Check the network and retry.', + 'log_resource_dir_missing': 'Resource directory error', + 'log_no_language_files': 'No language package files found', + 'log_cleanup_start': 'Running post-flash cleanup...', + 'log_cleanup_done': 'Post-flash cleanup complete', + 'log_cleanup_partial': 'Post-flash cleanup complete, with partial failures', + 'log_flash_start_notice': 'Starting language flashing. Do not power off or reboot the computer or head unit.', + 'log_flash_done': 'Language package flashing complete, {total} item(s)', + 'log_flash_effective': 'Language package flashing complete. Reboot the device when convenient.', + 'log_flash_partial': 'Partially flashed ({success}/{total})', + 'log_patch_missing': '5.2 patch file not found', + 'log_patch_push_failed': '5.2 patch push failed', + 'log_patch_apply_failed': '5.2 patch failed', + 'log_patch_done': '5.2 patch completed. Reboot the device to apply it.', + 'log_batch_install_start': 'Starting batch install: {count} APK file(s)...', + 'log_install_many_start': 'Starting install: {count} APK file(s)...', + 'log_install_done_all': 'Install complete: all {count} succeeded', + 'log_install_done_partial': 'Install complete: {success}/{count} succeeded', + 'log_install_success': 'Install succeeded', + 'log_install_failed': 'Install failed', + 'log_install_exception': 'Install process exception', + 'log_quick_lang_setting': 'Setting system language to: {language} ({locale})', + 'log_quick_lang_success': 'Language set to {language}', + 'log_quick_lang_failed': 'Language setup failed', + 'log_rebooting': 'Device is rebooting...', + 'log_disable_ota_cancelled': 'OTA disable cancelled', + 'log_disable_ota_success': 'System update disabled', + 'log_disable_ota_failed': 'Failed to disable system update', + 'log_debug_off': 'Debug mode disabled', + 'log_debug_on': 'Debug mode enabled - detailed ADB logs will be shown', + 'err_extract_default': 'Resource preparation failed', + 'msg_start_failed': 'Program startup failed', + 'msg_python_version_error': 'Error: Python 3.6 or later is required', + 'unknown_error': 'Unknown error', } } @@ -170,14 +448,19 @@ class ADKAPKGUI: self.priv_apps_dir = None self.temp_dir = None self.api_url = "https://api.changan.softwindy.cn/api/authorizations/auth-check" + self.debug_password_api_url = "https://api.changan.softwindy.cn/api/authorizations/verify-debug-mode-password" self.vin = None + self.vehicle_name = "" self.device_connected = False self._refreshing = False # 防止并发刷新 self.debug_mode = False # 调试模式 + atexit.register(self.cleanup_cache_on_exit) # 设置样式 self.setup_styles() self.setup_ui() + self.root.protocol("WM_DELETE_WINDOW", self.on_close) + self.set_window_icon() self.center_window() # 检查环境 @@ -186,6 +469,38 @@ class ADKAPKGUI: # 启动设备状态监控 self.start_device_monitor() + def set_window_icon(self): + """Set the Tk window/taskbar icon at runtime; PyInstaller --icon only sets the exe file icon.""" + try: + icon_path = find_resource("app.ico") + if icon_path.exists(): + self.root.iconbitmap(str(icon_path)) + self._set_windows_hwnd_icon(icon_path) + except Exception: + pass + + def _set_windows_hwnd_icon(self, icon_path): + if sys.platform != 'win32': + return + try: + import ctypes + user32 = ctypes.windll.user32 + hwnd = self.root.winfo_id() + image_icon = 1 + lr_loadfromfile = 0x00000010 + wm_seticon = 0x0080 + icon_small = 0 + icon_big = 1 + path = str(icon_path) + small = user32.LoadImageW(None, path, image_icon, 16, 16, lr_loadfromfile) + big = user32.LoadImageW(None, path, image_icon, 32, 32, lr_loadfromfile) + if small: + user32.SendMessageW(hwnd, wm_seticon, icon_small, small) + if big: + user32.SendMessageW(hwnd, wm_seticon, icon_big, big) + except Exception: + pass + def setup_styles(self): """设置自定义样式""" style = ttk.Style() @@ -206,6 +521,7 @@ class ADKAPKGUI: def setup_ui(self): """设置UI界面""" # 配置根窗口 + self.root.title(self.t('title')) self.root.configure(bg=self.colors['bg_dark']) # 创建主框架 @@ -213,24 +529,38 @@ class ADKAPKGUI: 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 = tk.Frame(main_frame, bg=self.colors['bg_dark'], height=42) 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() + # 标题:图标和文字分开,避免 emoji 与中文字体基线错位。 + self.title_group = tk.Frame(title_frame, bg=self.colors['bg_dark']) + self.title_group.place(relx=0.5, rely=0.5, anchor=tk.CENTER) + self.title_icon_label = tk.Label(self.title_group, + text="🚀", + font=('Segoe UI Emoji', 17), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.title_icon_label.pack(side=tk.LEFT, padx=(0, 8), pady=(1, 0)) + self.title_label = tk.Label(self.title_group, + text=self.t('title'), + font=('Microsoft YaHei', 18, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.title_label.pack(side=tk.LEFT) - subtitle_label = tk.Label(title_frame, - text="宜宾科宜科技有限公司 - 智能设备管理平台", - font=('Microsoft YaHei', 9), - fg=self.colors['text_secondary'], - bg=self.colors['bg_dark']) - subtitle_label.pack() + self.btn_lang_switch = tk.Button(title_frame, text=self.t('lang_en'), + command=self.toggle_lang, + font=('Microsoft YaHei', 10, 'bold'), + fg='white', + bg=self.colors['accent'], + activebackground=self.colors['accent_hover'], + activeforeground='white', + relief=tk.FLAT, + cursor='hand2', + width=8, + height=1) + self.btn_lang_switch.pack(side=tk.RIGHT, padx=(8, 6), pady=6) # 工程模式提示 eng_tips_frame = tk.Frame(main_frame, bg=self.colors['bg_light'], relief=tk.RAISED, bd=1) @@ -263,19 +593,25 @@ class ADKAPKGUI: row1_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) row1_frame.pack(pady=(8, 4)) - self.btn_push = tk.Button(row1_frame, text="📦 刷入语言包", + self.btn_push = tk.Button(row1_frame, text=self.t('btn_push'), command=self.push_all_apks, bg=self.colors['accent'], **btn_params) self.btn_push.pack(side=tk.LEFT, padx=4) - self.btn_install_all = tk.Button(row1_frame, text="📱 安装App", + self.btn_patch_52 = tk.Button(row1_frame, text=self.t('btn_patch_52'), + command=self.push_patch_52, + bg=self.colors['warning'], + **btn_params) + self.btn_patch_52.pack(side=tk.LEFT, padx=4) + + self.btn_install_all = tk.Button(row1_frame, text=self.t('btn_install'), command=self.install_apps, bg=self.colors['accent'], **btn_params) self.btn_install_all.pack(side=tk.LEFT, padx=4) - self.btn_language = tk.Button(row1_frame, text="🌐 语言设置", + self.btn_language = tk.Button(row1_frame, text=self.t('btn_language'), command=self.open_language_quick_set, bg=self.colors['accent'], **btn_params) @@ -285,25 +621,25 @@ class ADKAPKGUI: row2_frame = tk.Frame(button_frame, bg=self.colors['bg_light']) row2_frame.pack(pady=(4, 8)) - self.btn_timezone = tk.Button(row2_frame, text="⏰ 时区设置", + self.btn_timezone = tk.Button(row2_frame, text=self.t('btn_timezone'), command=self.open_timezone_settings, bg=self.colors['accent'], **btn_params) self.btn_timezone.pack(side=tk.LEFT, padx=4) - self.btn_settings = tk.Button(row2_frame, text="⚙️ 安卓设置", + self.btn_settings = tk.Button(row2_frame, text=self.t('btn_settings'), command=self.open_android_settings, bg=self.colors['accent'], **btn_params) self.btn_settings.pack(side=tk.LEFT, padx=4) - self.btn_reboot = tk.Button(row2_frame, text="🔄 重启设备", + self.btn_reboot = tk.Button(row2_frame, text=self.t('btn_reboot'), command=self.reboot_device, bg=self.colors['warning'], **btn_params) self.btn_reboot.pack(side=tk.LEFT, padx=4) - self.btn_exit = tk.Button(row2_frame, text="❌ 禁用升级", + self.btn_exit = tk.Button(row2_frame, text=self.t('btn_disable_upgrade'), command=self.on_disable_upgrade, bg=self.colors['error'], **btn_params) @@ -315,82 +651,85 @@ class ADKAPKGUI: # 状态指示器 status_indicator_frame = tk.Frame(status_bar_frame, bg=self.colors['bg_light']) - status_indicator_frame.pack(side=tk.LEFT, padx=10, pady=5) + status_indicator_frame.pack(side=tk.LEFT, padx=(8, 4), 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_label = tk.Label(status_indicator_frame, text=self.t('device_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.device_label.pack(side=tk.LEFT, padx=(5, 3)) - self.device_status_label = tk.Label(status_indicator_frame, text="未检测", + self.device_status_label = tk.Label(status_indicator_frame, text=self.t('status_detecting'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light'], - anchor='w', width=4) + anchor='w', width=7) 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="未获取", + vin_frame.pack(side=tk.LEFT, padx=(8, 4), pady=5) + self.vin_label_title = tk.Label(vin_frame, text=self.t('vin_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.vin_label_title.pack(side=tk.LEFT) + self.vin_label = tk.Label(vin_frame, text=self.t('vin_none'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light'], - anchor='w', width=17) + anchor='w', width=14) 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="未验证", + auth_frame.pack(side=tk.LEFT, padx=(8, 4), pady=5) + self.auth_label_title = tk.Label(auth_frame, text=self.t('auth_label'), + font=('Microsoft YaHei', 9), + fg=self.colors['text'], + bg=self.colors['bg_light']) + self.auth_label_title.pack(side=tk.LEFT) + self.auth_label = tk.Label(auth_frame, text=self.t('auth_none'), font=('Microsoft YaHei', 9, 'bold'), fg='#636e72', bg=self.colors['bg_light'], - anchor='w', width=4) + anchor='w', width=10) self.auth_label.pack(side=tk.LEFT, padx=(5, 0)) # 刷新按钮 - refresh_btn = tk.Button(status_bar_frame, text="🔄 检查", + self.btn_refresh = tk.Button(status_bar_frame, text=self.t('btn_refresh'), 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) + cursor='hand2', + width=8) + self.btn_refresh.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. 部分语言需要重启后生效,可以一切工作完成以后再重启。", - ] + tips = self.t('hint_lines') + self.tips_labels = [] 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) + tip_label = tk.Label(tip_row, text=tip, + font=('Microsoft YaHei', 9), + fg=self.colors['warning'], + bg=self.colors['bg_light'], + wraplength=600, + justify=tk.LEFT) + tip_label.pack(side=tk.LEFT) + self.tips_labels.append(tip_label) # 解压进度条框架 progress_frame = tk.Frame(main_frame, bg=self.colors['bg_dark']) @@ -422,12 +761,13 @@ class ADKAPKGUI: 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.log_title_label = tk.Label(log_title_frame, text=self.t('log_title'), + font=('Microsoft YaHei', 10, 'bold'), + fg=self.colors['accent'], + bg=self.colors['bg_dark']) + self.log_title_label.pack(side=tk.LEFT, padx=10) - self.btn_clear = tk.Button(log_title_frame, text="🗑 清空日志", + self.btn_clear = tk.Button(log_title_frame, text=self.t('btn_clear_log'), command=self.clear_log, font=('Microsoft YaHei', 8), fg=self.colors['text_secondary'], @@ -463,28 +803,20 @@ class ADKAPKGUI: bottom_status.pack(fill=tk.X, pady=(5, 0)) bottom_status.pack_propagate(False) - self.status_text = tk.Label(bottom_status, text="就绪", + self.status_text = tk.Label(bottom_status, text=self.t('status_ready'), font=('Microsoft YaHei', 8), fg=self.colors['text_secondary'], bg=self.colors['bg_light']) self.status_text.pack(side=tk.LEFT, padx=10) # 主题和语言切换按钮 - self.btn_theme_switch = tk.Button(bottom_status, text="🌙 暗色", + self.btn_theme_switch = tk.Button(bottom_status, text=self.t('theme_light'), command=self.toggle_theme, font=('Microsoft YaHei', 8), fg=self.colors['accent'], bg=self.colors['bg_light'], relief=tk.FLAT, cursor='hand2') self.btn_theme_switch.pack(side=tk.RIGHT, padx=5) - self.btn_lang_switch = tk.Button(bottom_status, text="EN", - command=self.toggle_lang, - font=('Microsoft YaHei', 8, 'bold'), - fg=self.colors['accent'], - bg=self.colors['bg_light'], - relief=tk.FLAT, cursor='hand2') - self.btn_lang_switch.pack(side=tk.RIGHT, padx=5) - # 调试模式快捷键 self.root.bind('', self._toggle_debug) self.root.bind('', self._debug_test_extract) @@ -494,7 +826,7 @@ class ADKAPKGUI: def bind_hover_effects(self): """绑定按钮悬停效果""" - buttons = [self.btn_push, self.btn_install_all, + buttons = [self.btn_push, self.btn_patch_52, self.btn_install_all, self.btn_language, self.btn_timezone, self.btn_settings, self.btn_reboot, self.btn_clear, self.btn_exit] @@ -539,17 +871,13 @@ class ADKAPKGUI: minute_tens = now.minute // 10 # 如 58 → 5 return f"{minute_tens}0{hour:02d}" - def _update_fac_pwd_display(self): + def _update_fac_pwd_display(self, schedule=True): """更新工程密码显示,每30秒刷新一次""" pwd = self._calc_fac_pwd() - text = ( - f"工程模式:拨号 *#*#888 ," - f"工程密码:{pwd}," - f"调试密码:3821" - ) - self.eng_tip_label.config(text=text) + self.eng_tip_label.config(text=self.tf('hint_factory_dynamic', password=pwd)) # 每30秒刷新(密码每分钟可能变化) - self.root.after(30000, self._update_fac_pwd_display) + if schedule: + self.root.after(30000, self._update_fac_pwd_display) def run_on_ui_thread(self, func, *args, **kwargs): """将函数调度到主线程执行,确保线程安全""" @@ -561,11 +889,17 @@ class ADKAPKGUI: def t(self, key): return self.T.get(self.lang, self.T['zh']).get(key, key) + def tf(self, key, **kwargs): + try: + return self.t(key).format(**kwargs) + except Exception: + return self.t(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") + self.log(self.t('log_lang_changed'), "INFO") def toggle_theme(self): if self.theme == 'dark': @@ -596,13 +930,21 @@ class ADKAPKGUI: self.log_text.configure(bg='#ffffff', fg='#2d3436') else: self.log_text.configure(bg='#2d2d3d', fg='#e0e0e0') + if getattr(self, 'btn_lang_switch', None): + self.btn_lang_switch.configure( + fg='white', + bg=c['accent'], + activebackground=c['accent_hover'], + activeforeground='white' + ) def _refresh_ui_texts(self): t = self.t + self.root.title(t('title')) widgets = [ (getattr(self, 'title_label', None), 'title', None), - (getattr(self, 'subtitle_label', None), 'about_company', None), (getattr(self, 'btn_push', None), 'btn_push', None), + (getattr(self, 'btn_patch_52', None), 'btn_patch_52', 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), @@ -618,15 +960,50 @@ class ADKAPKGUI: (getattr(self, 'btn_refresh', None), 'btn_refresh', None), ] for w, key, _ in widgets: - if w: w.config(text=t(key)) + if not w: + continue + text = t(key) + w.config(text=text) self.btn_theme_switch.config(text=t('theme_light') if self.theme == 'dark' else t('theme_dark')) self.btn_lang_switch.config(text=t('lang_en') if self.lang == 'zh' else t('lang_zh')) - if self.vin: + if getattr(self, 'status_text', None): + self.status_text.config(text=t('status_debug') if self.debug_mode else t('status_ready')) + if getattr(self, 'eng_tip_label', None): + self._update_fac_pwd_display(schedule=False) + for label, tip in zip(getattr(self, 'tips_labels', []), t('hint_lines')): + label.config(text=tip) + if getattr(self, 'device_status_label', None): self._update_device_status_impl(self.device_connected, self.vin, getattr(self, '_last_authorized', False)) + def _sanitize_user_log_message(self, message): + """Hide low-level commands, paths, package names, and APK names in normal logs.""" + text = str(message) + if re.match(r'^[✓✗]\s+[\w.\-]+\.apk$', text): + return text + replacements = [ + (r'com\.[\w.\-]+', '相关应用'), + (r'cn\.[\w.\-]+', '相关应用'), + (r'[\w.\-]+\.apk', '文件'), + (r'package\.bin', '资源文件'), + (r'7za(?:\.exe)?', '资源工具'), + (r'adb(?:\.exe)?', '设备连接工具'), + (r'pm\s+\S+', '系统操作'), + (r'\broot\b', '权限'), + (r'\bremount\b', '挂载'), + (r'(? {out[:300]}", "CMD") + self.log(f"STDOUT:\n{out}", "CMD") if err: - self.log(f" !! {err[:300]}", "ERROR") + self.log(f"STDERR:\n{err}", "CMD") if result.returncode == 0: return True, result.stdout.strip() else: @@ -1127,7 +1578,7 @@ class ADKAPKGUI: "com.changan.oushangCos1", ] - self.log("正在执行刷入后应用清理...", "INFO") + self.log(self.t('log_cleanup_start'), "INFO") failed_count = 0 for pkg in cleanup_packages: disable_ok, disable_err = self.run_adb_command( @@ -1136,65 +1587,154 @@ class ADKAPKGUI: f'adb -d shell pm uninstall --user 0 {pkg}') if disable_ok and uninstall_ok: - self.log(f"已禁用并卸载: {pkg}", "SUCCESS") + if self.debug_mode: + self.log(f"已禁用并卸载: {pkg}", "SUCCESS") else: failed_count += 1 detail = uninstall_err or disable_err or "应用可能不存在或已处理" - self.log(f"清理未完全成功: {pkg} ({detail})", "WARNING") + if self.debug_mode: + self.log(f"清理未完全成功: {pkg} ({detail})", "WARNING") if failed_count: - self.log(f"应用清理完成,{failed_count} 个应用未完全成功", "WARNING") + self.log(self.t('log_cleanup_partial'), "WARNING") else: - self.log("刷入后应用清理完成", "SUCCESS") + self.log(self.t('log_cleanup_done'), "SUCCESS") + + def _get_patch_52_apk(self): + """B561_Navi_5.2.apk lives next to the extracted app directory.""" + if not self.apps_dir or not self.apps_dir.exists(): + return None + patch_apk = self.apps_dir.parent / "B561_Navi_5.2.apk" + if patch_apk.exists(): + return patch_apk + # Compatibility fallback in case a resource package places it inside app/. + patch_apk = self.apps_dir / "B561_Navi_5.2.apk" + if patch_apk.exists(): + return patch_apk + return None + + def push_patch_52(self): + """刷入 B561_Navi 5.2 补丁 APK。""" + if not self.check_device_connection(): + return + if not self.vin: + messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_need_vin')) + return + + def do_patch(): + if not self.check_authorization(self.vin): + self.run_on_ui_thread(messagebox.showerror, self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized')) + return + if not self.extract_password: + if not self.fetch_package_password(): + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.t('msg_data_prepare_failed')) + 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(messagebox.showerror, self.t('msg_error_title'), self.t('msg_data_prepare_failed')) + return + self.show_progress(False, is_push=False) + + patch_apk = self._get_patch_52_apk() + if not patch_apk: + if self.debug_mode: + self.log("未找到 5.2 补丁文件: B561_Navi_5.2.apk", "ERROR") + else: + self.log(self.t('log_patch_missing'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.t('msg_patch_missing')) + return + + self.show_progress(True, is_push=True) + try: + self.update_progress(10, 100, self.t('progress_patch_prepare'), is_push=True) + ok, err = self.run_adb_command('adb -d root') + if not ok: + if self.debug_mode: + self.log(f"adb root 失败: {err}", "WARNING") + else: + time.sleep(2) + ok, err = self.run_adb_command('adb -d remount') + if not ok: + if self.debug_mode: + self.log(f"adb remount 失败: {err}", "WARNING") + + self.update_progress(40, 100, self.t('progress_patch_push'), is_push=True) + ok, err = self.run_adb_command(f'adb -d push "{patch_apk}" /data/local/tmp/B561_Navi_5.2.apk') + if not ok: + if self.debug_mode: + self.log(f"5.2 补丁推送失败: {err}", "ERROR") + else: + self.log(self.t('log_patch_push_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.t('msg_patch_push_failed')) + return + + self.update_progress(80, 100, self.t('progress_patch_apply'), is_push=True) + ok, err = self.run_adb_command( + 'adb -d shell cp -f /data/local/tmp/B561_Navi_5.2.apk /system/app/B561_Navi/B561_Navi.apk') + if not ok: + if self.debug_mode: + self.log(f"5.2 补丁刷入失败: {err}", "ERROR") + else: + self.log(self.t('log_patch_apply_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.t('msg_patch_apply_failed')) + return + + self.update_progress(100, 100, self.t('progress_patch_done'), is_push=True) + self.log(self.t('log_patch_done'), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('msg_done_title'), self.t('msg_patch_done')) + finally: + self.show_progress(False, is_push=True) + + threading.Thread(target=do_patch, daemon=True).start() def push_all_apks(self): """推送APK到系统分区(支持app和priv-app)""" if not self.check_device_connection(): return if not self.vin: - messagebox.showwarning("警告", "请先刷新设备状态并获取VIN码") + messagebox.showwarning(self.t('msg_warn_title'), self.t('msg_need_vin')) return - messagebox.showwarning("⚠️ 重要提示", - "刷入过程中请勿:\n" - " ● 重启车机\n" - " ● 退出本程序\n" - " ● 关闭电脑\n\n" - "否则可能导致车机系统损坏!") + messagebox.showwarning(self.t('msg_flash_warning_title'), self.t('msg_flash_warning')) def do_push_all(): if not self.check_authorization(self.vin): - self.run_on_ui_thread(lambda: messagebox.showerror("授权失败", "设备未授权")) + self.run_on_ui_thread(messagebox.showerror, self.t('msg_auth_failed_title'), self.t('msg_device_unauthorized')) return if not self.extract_password: if not self.fetch_package_password(): - self.run_on_ui_thread(lambda: messagebox.showerror("错误", "资源准备失败!")) + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.t('msg_data_prepare_failed')) 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("错误", "资源准备失败!")) + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.t('msg_data_prepare_failed')) 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("错误", "资源目录未找到")) + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.t('msg_resource_dir_missing')) return self.show_progress(True, is_push=True) + self.log(self.t('log_flash_start_notice'), "SUCCESS") # 推送系统分区前先获取 root 权限并重新挂载 ok, err = self.run_adb_command('adb -d root') if not ok: - self.log(f"adb root 失败: {err}", "WARNING") + if self.debug_mode: + self.log(f"adb root 失败: {err}", "WARNING") else: # adbd 重启后需要短暂等待 time.sleep(2) ok, err = self.run_adb_command('adb -d remount') if not ok: - self.log(f"adb remount 失败: {err}", "WARNING") + if self.debug_mode: + self.log(f"adb remount 失败: {err}", "WARNING") self.run_adb_command('adb -d shell mkdir -p /data/local/tmp') @@ -1212,7 +1752,7 @@ class ADKAPKGUI: 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.log(self.t('log_no_language_files'), "WARNING") self.show_progress(False, is_push=True) return # 重新收集 @@ -1224,7 +1764,7 @@ class ADKAPKGUI: for apk in self.priv_apps_dir.glob("*.apk"): all_apks.append((apk, "priv-app")) if not all_apks: - self.log("未找到语言包文件", "WARNING") + self.log(self.t('log_no_language_files'), "WARNING") self.show_progress(False, is_push=True) return @@ -1235,12 +1775,12 @@ class ADKAPKGUI: 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(i, total, self.t('progress_flashing'), is_push=True) - self.update_progress(total, total, "刷入完成", is_push=True) + self.update_progress(total, total, self.t('progress_flash_done'), is_push=True) # 删除旧应用 - self.log("正在清理旧应用...", "INFO") + self.log(self.t('log_cleanup_start'), "INFO") for pkg in [ "/system/app/B561_OSWeather", "/system/app/B561_WeChatSendCar", @@ -1253,7 +1793,6 @@ class ADKAPKGUI: # 推送字体文件 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') @@ -1262,11 +1801,11 @@ class ADKAPKGUI: self.cleanup_after_language_push() if success_count == total: - self.log(f"刷入完成,共 {total} 个语言包", "SUCCESS") - self.log("语言包已刷入完成,重启设备后生效,您可在适当时候重启", "WARNING") + self.log(self.tf('log_flash_done', total=total), "SUCCESS") + self.log(self.t('log_flash_effective'), "WARNING") elif success_count > 0: - self.log(f"部分刷入成功({success_count}/{total})", "WARNING") - self.log("语言包已刷入完成,重启设备后生效,您可在适当时候重启", "WARNING") + self.log(self.tf('log_flash_partial', success=success_count, total=total), "WARNING") + self.log(self.t('log_flash_effective'), "WARNING") self.show_progress(False, is_push=True) @@ -1277,48 +1816,56 @@ class ADKAPKGUI: if not self.check_device_connection(): return - apk_dir = filedialog.askdirectory(title="选择包含APK文件的文件夹") + apk_dir = filedialog.askdirectory(title=self.t('file_select_folder_title')) if not apk_dir: return apk_files = list(Path(apk_dir).glob("*.apk")) if not apk_files: - messagebox.showerror("错误", "所选文件夹中没有APK文件!") + messagebox.showerror(self.t('msg_error_title'), self.t('msg_no_apks_in_folder')) return - result = messagebox.askyesno("确认安装", - f"找到 {len(apk_files)} 个APK文件\n\n是否开始批量安装?") + result = messagebox.askyesno( + self.t('msg_install_confirm_title'), + self.tf('msg_install_confirm_folder', count=len(apk_files))) if not result: return def install(): self.show_progress(True, is_push=True) total = len(apk_files) - self.log(f"开始批量安装 {total} 个APK...", "INFO") + self.log(self.tf('log_batch_install_start', count=total), "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) + self.update_progress(i, total, self.t('progress_installing'), 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) + self.update_progress(total, total, self.t('progress_install_done'), is_push=True) if success_count == total: - self.log(f"安装完成:全部 {total} 个成功", "SUCCESS") - self.run_on_ui_thread(messagebox.showinfo, "安装完成", f"成功安装 {total} 个APK!") + self.log(self.tf('log_install_done_all', count=total), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('msg_install_done_title'), + self.tf('msg_install_done_all', count=total)) 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}") + self.log(self.tf('log_install_done_partial', success=success_count, count=total), "WARNING") + self.run_on_ui_thread(messagebox.showwarning, self.t('msg_install_partial_title'), + self.tf('msg_install_partial', success=success_count, failed=total - success_count)) else: - self.log("安装失败", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "安装失败", "所有APK安装失败!") + self.log(self.t('log_install_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_install_failed_title'), + self.t('msg_install_failed_all')) except Exception as e: - self.log(f"安装过程异常: {str(e)}", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "安装失败", f"安装过程异常:{str(e)}") + if self.debug_mode: + self.log(f"安装过程异常: {str(e)}", "ERROR") + else: + self.log(self.t('log_install_exception'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_install_failed_title'), + self.t('msg_install_exception')) finally: self.run_adb_command('adb -d shell setprop vecentek.model 0') self.show_progress(False, is_push=True) @@ -1332,8 +1879,8 @@ class ADKAPKGUI: return file_path = filedialog.askopenfilename( - title="选择APK文件", - filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + title=self.t('file_select_apk_title'), + filetypes=[(self.t('filetype_apk'), "*.apk"), (self.t('filetype_all'), "*.*")] ) if not file_path: @@ -1341,17 +1888,20 @@ class ADKAPKGUI: def install(): self.show_progress(True, is_push=True) - self.update_progress(50, 100, f"安装中", is_push=True) + self.update_progress(50, 100, self.t('progress_installing'), 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) + self.update_progress(100, 100, self.t('progress_done'), is_push=True) if success: - self.log("✓ 安装成功", "SUCCESS") + self.log(self.t('log_install_success'), "SUCCESS") else: - self.log("✗ 安装失败", "ERROR") + self.log(self.t('log_install_failed'), "ERROR") except Exception as e: - self.log(f"安装过程异常: {str(e)}", "ERROR") + if self.debug_mode: + self.log(f"安装过程异常: {str(e)}", "ERROR") + else: + self.log(self.t('log_install_exception'), "ERROR") finally: self.run_adb_command('adb -d shell setprop vecentek.model 0') self.show_progress(False, is_push=True) @@ -1372,7 +1922,7 @@ class ADKAPKGUI: # 创建弹窗 popup = tk.Toplevel(self.root) - popup.title("快捷语言设置") + popup.title(self.t('quick_lang_title')) popup.geometry("520x320") popup.configure(bg=self.colors['bg_dark']) popup.resizable(False, False) @@ -1386,29 +1936,23 @@ class ADKAPKGUI: popup.grab_set() # 标题 - header = tk.Label(popup, text="选择目标语言", + header = tk.Label(popup, text=self.t('quick_lang_header'), font=('Microsoft YaHei', 13, 'bold'), fg=self.colors['accent'], bg=self.colors['bg_dark']) header.pack(pady=(15, 10)) - hint = tk.Label(popup, text="点击按钮即可将系统语言切换为对应语言,重启后生效", + hint = tk.Label(popup, text=self.t('quick_lang_hint'), font=('Microsoft YaHei', 9), fg=self.colors['text_secondary'], bg=self.colors['bg_dark']) hint.pack(pady=(0, 12)) # 语言列表:(显示名, locale_code) - languages = [ - ("🇨🇳 中文", "zh-CN"), - ("英 English", "en-EN"), - ("俄 Русский", "ru-RU"), - ("法 Français", "fr-FR"), - ("西 Español", "es-ES"), - ("葡 Português", "pt-BR"), - ("意 Italiano", "it-IT"), - ("阿 العربية", "ar-SA"), - ] + languages = list(zip(self.t('quick_lang_names'), [ + "zh-CN", "en-EN", "ru-RU", "fr-FR", + "es-ES", "pt-BR", "it-IT", "ar-SA", + ])) # 创建按钮容器 btn_frame = tk.Frame(popup, bg=self.colors['bg_dark']) @@ -1442,7 +1986,7 @@ class ADKAPKGUI: 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="⚙️ 打开系统语言设置(手动选择)", + sys_btn = tk.Button(popup, text=self.t('quick_lang_system'), command=lambda: self._open_sys_and_close(popup), font=('Microsoft YaHei', 9), fg=self.colors['text_secondary'], @@ -1456,21 +2000,24 @@ class ADKAPKGUI: popup.destroy() def do_set(): - self.log(f"正在设置系统语言为: {language_name} ({locale_code})", "INFO") + self.log(self.tf('log_quick_lang_setting', language=language_name, locale=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.log(self.tf('log_quick_lang_success', language=language_name), "SUCCESS") self.run_on_ui_thread( messagebox.showinfo, - "设置成功", - f"系统语言已设置为 {language_name}\n\n⚠️ 请重启设备使其生效。" + self.t('quick_lang_success_title'), + self.tf('quick_lang_success', language=language_name) ) else: - self.log(f"✗ 语言设置失败: {output}", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "设置失败", f"语言设置失败!\n\n{output}") + if self.debug_mode: + self.log(f"✗ 语言设置失败: {output}", "ERROR") + else: + self.log(self.t('log_quick_lang_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('quick_lang_failed_title'), self.t('quick_lang_failed')) threading.Thread(target=do_set, daemon=True).start() @@ -1495,10 +2042,10 @@ class ADKAPKGUI: """重启设备""" if not self.check_device_connection(): return - if messagebox.askyesno("确认重启", "确定要重启设备吗?"): + if messagebox.askyesno(self.t('msg_reboot_title'), self.t('msg_reboot_confirm')): subprocess.Popen(f'{self._adb_cmd()} -d shell reboot', shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - self.log("设备正在重启...", "INFO") + self.log(self.t('log_rebooting'), "INFO") self.update_device_status(False) def on_disable_upgrade(self): @@ -1509,15 +2056,12 @@ class ADKAPKGUI: # 弹窗确认 result = messagebox.askyesno( - "确认禁用升级", - "⚠️ 警告:禁用系统升级后,将无法接收系统更新!\n\n" - "是否确定要禁用系统升级应用?\n\n" - "禁用命令:\n" - "adb -d shell pm disable-user --user 0 com.incall.apps.softmanager" + self.t('msg_disable_ota_title'), + self.t('msg_disable_ota_confirm') ) if not result: - self.log("已取消禁用升级操作", "INFO") + self.log(self.t('log_disable_ota_cancelled'), "INFO") return def disable(): @@ -1525,11 +2069,14 @@ class ADKAPKGUI: 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, "成功", "系统升级已成功禁用!") + self.log(self.t('log_disable_ota_success'), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('msg_success_title'), self.t('msg_disable_ota_success')) else: - self.log("禁用系统升级失败", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "错误", f"禁用失败:{output}") + if self.debug_mode: + self.log(f"禁用系统升级失败: {output}", "ERROR") + else: + self.log(self.t('log_disable_ota_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_error_title'), self.t('msg_disable_ota_failed')) self.show_progress(False, is_push=False) threading.Thread(target=disable, daemon=True).start() @@ -1538,19 +2085,43 @@ class ADKAPKGUI: """切换调试模式(隐藏入口,Ctrl+Shift+D)""" if self.debug_mode: self.debug_mode = False - self.log("调试模式已关闭", "WARNING") - self.status_text.config(text="就绪") + self.log(self.t('log_debug_off'), "WARNING") + self.status_text.config(text=self.t('status_ready')) 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("错误", "密码错误") + pwd = simpledialog.askstring(self.t('debug_title'), self.t('debug_prompt'), show='*', parent=self.root) + if not pwd: + return + + self.log(self.t('debug_password_verifying'), "WARNING") + + def verify(): + valid, message = self.verify_debug_mode_password(pwd) + if valid: + def enable_debug(): + self.debug_mode = True + self.update_device_status(True, "", True) + self.log(self.t('log_debug_on'), "WARNING") + self.status_text.config(text=self.t('status_debug')) + self.run_on_ui_thread(enable_debug) + else: + def show_failed(): + msg = message or self.t('msg_debug_wrong_password') + self.log(self.tf('debug_verify_failed', message=msg), "WARNING") + messagebox.showwarning(self.t('msg_error_title'), msg) + self.run_on_ui_thread(show_failed) + + threading.Thread(target=verify, daemon=True).start() + + def verify_debug_mode_password(self, password): + try: + data = self._post_json(self.debug_password_api_url, {"password": password}) + if data.get('success') is True and data.get('valid') is True: + return True, data.get('message', '') + return False, data.get('message') or self.t('msg_debug_wrong_password') + except Exception as e: + return False, str(e) def install_apps(self): """安装App — 支持单选或多选APK文件""" @@ -1558,27 +2129,28 @@ class ADKAPKGUI: return file_paths = filedialog.askopenfilenames( - title="选择APK文件", - filetypes=[("APK文件", "*.apk"), ("所有文件", "*.*")] + title=self.t('file_select_apk_title'), + filetypes=[(self.t('filetype_apk'), "*.apk"), (self.t('filetype_all'), "*.*")] ) if not file_paths: return count = len(file_paths) - result = messagebox.askyesno("确认安装", f"已选择 {count} 个APK文件\n\n是否开始安装?") + result = messagebox.askyesno(self.t('msg_install_confirm_title'), + self.tf('msg_install_confirm_many', count=count)) if not result: return def install(): self.show_progress(True, is_push=True) - self.log(f"开始安装 {count} 个APK...", "INFO") + self.log(self.tf('log_install_many_start', count=count), "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) + self.update_progress(i, count, self.tf('progress_installing_name', name=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") @@ -1586,20 +2158,27 @@ class ADKAPKGUI: else: self.log(f"✗ {apk_name}.apk", "ERROR") - self.update_progress(count, count, "安装完成", is_push=True) + self.update_progress(count, count, self.t('progress_install_done'), is_push=True) if success_count == count: - self.log(f"安装完成:全部 {count} 个成功", "SUCCESS") - self.run_on_ui_thread(messagebox.showinfo, "安装完成", f"成功安装 {count} 个APK!") + self.log(self.tf('log_install_done_all', count=count), "SUCCESS") + self.run_on_ui_thread(messagebox.showinfo, self.t('msg_install_done_title'), + self.tf('msg_install_done_all', count=count)) 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}") + self.log(self.tf('log_install_done_partial', success=success_count, count=count), "WARNING") + self.run_on_ui_thread(messagebox.showwarning, self.t('msg_install_partial_title'), + self.tf('msg_install_partial', success=success_count, failed=count - success_count)) else: - self.log("安装失败", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "安装失败", "所有APK安装失败!") + self.log(self.t('log_install_failed'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_install_failed_title'), + self.t('msg_install_failed_all')) except Exception as e: - self.log(f"安装过程异常: {str(e)}", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "安装失败", f"安装过程异常:{str(e)}") + if self.debug_mode: + self.log(f"安装过程异常: {str(e)}", "ERROR") + else: + self.log(self.t('log_install_exception'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('msg_install_failed_title'), + self.t('msg_install_exception')) finally: self.run_adb_command('adb -d shell setprop vecentek.model 0') self.show_progress(False, is_push=True) @@ -1609,10 +2188,11 @@ class ADKAPKGUI: def _debug_test_extract(self, event=None): """Debug-only package extraction test.""" if not getattr(self, 'debug_mode', False): - messagebox.showwarning("Debug mode", "Press Ctrl+Shift+D to enable debug mode first") + messagebox.showwarning(self.t('debug_title'), self.t('debug_need_enable')) return - pwd = simpledialog.askstring("Test extraction", "Enter package.bin password:", show='*', parent=self.root) + pwd = simpledialog.askstring(self.t('debug_extract_title'), self.t('debug_extract_prompt'), + show='*', parent=self.root) if not pwd: return @@ -1622,15 +2202,16 @@ class ADKAPKGUI: try: self.show_progress(True, is_push=False) if self.extract_package_silent(): - self.log("Test extraction succeeded", "SUCCESS") + self.log(self.t('debug_extract_success_title'), "SUCCESS") self.run_on_ui_thread( messagebox.showinfo, - "Test extraction succeeded", - f"Resources extracted to:\n{self.temp_dir}" + self.t('debug_extract_success_title'), + self.tf('debug_extract_success', path=self.temp_dir) ) else: - self.log("Test extraction failed", "ERROR") - self.run_on_ui_thread(messagebox.showerror, "Test extraction failed", "Check the 7za output in logs") + self.log(self.t('debug_extract_failed_title'), "ERROR") + self.run_on_ui_thread(messagebox.showerror, self.t('debug_extract_failed_title'), + self.t('debug_extract_failed')) finally: self.extract_password = old_password self.show_progress(False, is_push=False) @@ -1644,17 +2225,17 @@ class ADKAPKGUI: def main(): """主函数""" if sys.version_info < (3, 6): - print("错误:需要Python 3.6或更高版本") + print("错误:需要Python 3.6或更高版本 / Error: Python 3.6 or later is required") sys.exit(1) try: app = ADKAPKGUI() app.run() except Exception as e: - print(f"启动失败: {e}") + print(f"启动失败 / Startup failed: {e}") import traceback traceback.print_exc() - messagebox.showerror("错误", f"程序启动失败: {e}") + messagebox.showerror("Error", f"程序启动失败 / Startup failed: {e}") if __name__ == "__main__": main() diff --git a/X5plus/app.ico b/X5plus/app.ico new file mode 100644 index 0000000..dd3b20e Binary files /dev/null and b/X5plus/app.ico differ diff --git a/X5plus/apps/FZLTHPro_GB18030.ttf b/X5plus/apps/FZLTHPro_GB18030.ttf new file mode 100644 index 0000000..500b104 Binary files /dev/null and b/X5plus/apps/FZLTHPro_GB18030.ttf differ diff --git a/X5plus/pack_x5plus.bat b/X5plus/pack_x5plus.bat index 5ae85d1..8ae0199 100644 --- a/X5plus/pack_x5plus.bat +++ b/X5plus/pack_x5plus.bat @@ -1,96 +1,157 @@ -@echo off -chcp 65001 >nul -cd /d "%~dp0" -set "ROOT=%~dp0.." -set "TOOLS=%ROOT%\tools" -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 "%TOOLS%\adb.exe" . >nul -copy "%TOOLS%\AdbWinApi.dll" . >nul -copy "%TOOLS%\AdbWinUsbApi.dll" . >nul -copy "%TOOLS%\7za.exe" . >nul -if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul - -echo [5/6] PyInstaller... -%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 -echo [INFO] Normal PyInstaller... -%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 +@echo off +chcp 65001 >nul +setlocal +cd /d "%~dp0" + +set "ROOT=%~dp0.." +set "TOOLS=%ROOT%\tools" +set "ICON=%~dp0app.ico" +set "NAME=Changan_X5Plus-Installer" +set "SRC=X5plusTool.py" +title %NAME% - Build + +echo ============================================================ +echo %NAME% - Cython Build +echo ============================================================ +echo. + +set "PY=" +where python >nul 2>&1 +if not errorlevel 1 ( + for /f "delims=" %%i in ('where python') do ( + if not defined PY set "PY=%%i" + ) +) +if not defined PY ( + where py >nul 2>&1 + if not errorlevel 1 set "PY=py" +) +if not defined PY ( + if exist "%LOCALAPPDATA%\Microsoft\WindowsApps\python.exe" set "PY=%LOCALAPPDATA%\Microsoft\WindowsApps\python.exe" +) +if not defined PY ( + echo [ERROR] Python not found + pause + exit /b 1 +) +echo Python: %PY% + +if not exist "%SRC%" ( + echo [ERROR] Source not found: %SRC% + pause + exit /b 1 +) + +if not exist "%ICON%" ( + echo [ERROR] app.ico not found: %ICON% + pause + exit /b 1 +) + +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 + if errorlevel 1 ( + echo [ERROR] Dependency installation failed + pause + exit /b 1 + ) +) + +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 +if errorlevel 1 ( + echo [ERROR] Copy source failed + pause + exit /b 1 +) + +"%PY%" -c "open('dist_cy/setup_cython.py','w',encoding='utf-8').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')" +if errorlevel 1 ( + echo [ERROR] Generate Cython setup failed + pause + exit /b 1 +) + +cd dist_cy +"%PY%" setup_cython.py build_ext --inplace +if errorlevel 1 ( + cd .. + echo [ERROR] Cython compile failed + pause + exit /b 1 +) + +set "PYD=" +for %%f in (_core*.pyd) do set "PYD=%%f" +if "%PYD%"=="" ( + cd .. + echo [ERROR] No Cython pyd generated + pause + exit /b 1 +) +echo PYD: %PYD% +copy "%PYD%" "_core.pyd" >nul +if errorlevel 1 ( + cd .. + echo [ERROR] Copy Cython pyd failed + pause + exit /b 1 +) + +"%PY%" -c "open('launcher.py','w',encoding='utf-8').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')" +if errorlevel 1 ( + cd .. + echo [ERROR] Generate launcher failed + pause + exit /b 1 +) + +echo [4/6] Copy resources... +copy "%TOOLS%\adb.exe" . >nul +if errorlevel 1 ( + cd .. + echo [ERROR] adb.exe not found in %TOOLS% + pause + exit /b 1 +) +copy "%TOOLS%\AdbWinApi.dll" . >nul +copy "%TOOLS%\AdbWinUsbApi.dll" . >nul +copy "%TOOLS%\7za.exe" . >nul +copy "%ICON%" . >nul +if errorlevel 1 ( + cd .. + echo [ERROR] Copy resources failed + pause + exit /b 1 +) + +echo [5/6] PyInstaller... +"%PY%" -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ICON%" --add-data "%ICON%;." --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 1 +) + +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 .. + +echo. +echo Done. +if exist "dist_cy\dist\%NAME%.exe" ( + echo Output: dist_cy\dist\%NAME%.exe +) else ( + echo [ERROR] Build output not found +) +pause diff --git a/Yidong/Changan_Yidong-Language_Installer.exe.id0 b/Yidong/Changan_Yidong-Language_Installer.exe.id0 new file mode 100644 index 0000000..bffc9f3 Binary files /dev/null and b/Yidong/Changan_Yidong-Language_Installer.exe.id0 differ diff --git a/Yidong/Changan_Yidong-Language_Installer.exe.id1 b/Yidong/Changan_Yidong-Language_Installer.exe.id1 new file mode 100644 index 0000000..a44d43e Binary files /dev/null and b/Yidong/Changan_Yidong-Language_Installer.exe.id1 differ diff --git a/Yidong/Changan_Yidong-Language_Installer.exe.id2 b/Yidong/Changan_Yidong-Language_Installer.exe.id2 new file mode 100644 index 0000000..522c30d Binary files /dev/null and b/Yidong/Changan_Yidong-Language_Installer.exe.id2 differ diff --git a/Yidong/Changan_Yidong-Language_Installer.exe.nam b/Yidong/Changan_Yidong-Language_Installer.exe.nam new file mode 100644 index 0000000..dee0a69 Binary files /dev/null and b/Yidong/Changan_Yidong-Language_Installer.exe.nam differ diff --git a/Yidong/Changan_Yidong-Language_Installer.exe.til b/Yidong/Changan_Yidong-Language_Installer.exe.til new file mode 100644 index 0000000..9b8bc62 Binary files /dev/null and b/Yidong/Changan_Yidong-Language_Installer.exe.til differ diff --git a/Yidong/pack_common.bat b/Yidong/pack_common.bat deleted file mode 100644 index 7c36664..0000000 --- a/Yidong/pack_common.bat +++ /dev/null @@ -1,96 +0,0 @@ -@echo off -chcp 65001 >nul -cd /d "%~dp0" -set "ROOT=%~dp0.." -set "TOOLS=%ROOT%\tools" -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 "%TOOLS%\adb.exe" . >nul -copy "%TOOLS%\AdbWinApi.dll" . >nul -copy "%TOOLS%\AdbWinUsbApi.dll" . >nul -copy "%TOOLS%\7za.exe" . >nul -if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul - -echo [5/6] PyInstaller... -%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 -echo [INFO] Normal PyInstaller... -%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\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 diff --git a/tools/encrypt_q05_lidar_resource.py b/tools/encrypt_q05_lidar_resource.py index 21e14a5..ea22127 100644 --- a/tools/encrypt_q05_lidar_resource.py +++ b/tools/encrypt_q05_lidar_resource.py @@ -10,16 +10,35 @@ import struct import zlib from pathlib import Path -AAD = b"Q05-Lidar init_boot resource v1" +DEFAULT_AAD = "Q05-Lidar init_boot resource v1" +FORMAT_MAGIC = { + "q05": (b"Q05R2\x00", "q05-lidar-resource-v2", DEFAULT_AAD), + "ez60": (b"EZ60R2\x00", "ez60-resource-v2", "Mazda-EZ60 init_boot resource v1"), +} def b64u(data): return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=") +def decode_key_material(key_text): + text = str(key_text).strip() + if text.startswith("raw:"): + key = text.split(":", 1)[1].encode("utf-8") + elif len(text) == 64 and all(c in "0123456789abcdefABCDEF" for c in text): + key = bytes.fromhex(text) + elif len(text.encode("utf-8")) == 32: + key = text.encode("utf-8") + else: + key = base64.urlsafe_b64decode(text + "=" * (-len(text) % 4)) + if len(key) != 32: + raise ValueError(f"key must decode to 32 bytes, got {len(key)} bytes") + return key + + def main(): parser = argparse.ArgumentParser( - description="Encrypt a Q05-Lidar init_boot image into resource.dat" + description="Encrypt a vehicle init_boot image into a resource dat file" ) parser.add_argument("input_img", help="Magisk-patched init_boot image") parser.add_argument( @@ -27,9 +46,19 @@ def main(): default="resource.dat", help="Output encrypted resource file, default: resource.dat", ) + parser.add_argument( + "--vehicle", + choices=sorted(FORMAT_MAGIC.keys()), + default="q05", + help="Resource header/profile to write. Default: q05.", + ) + parser.add_argument( + "--aad", + help="Optional AES-GCM AAD text. Defaults to the selected vehicle profile.", + ) parser.add_argument( "--key", - help="Optional AES-256 key as base64url or 64-char hex. Omit to generate a random key.", + help="Optional AES-256 key as base64url, 64-char hex, raw:TEXT, or 32-byte UTF-8 text. Omit to generate a random key.", ) parser.add_argument( "--no-compress", @@ -49,29 +78,28 @@ def main(): input_path = Path(args.input_img) plain = input_path.read_bytes() sha256 = hashlib.sha256(plain).hexdigest() + magic, resource_format, default_aad = FORMAT_MAGIC[args.vehicle] + aad = (args.aad or default_aad).encode("utf-8") if args.key: - text = args.key.strip() - if len(text) == 64 and all(c in "0123456789abcdefABCDEF" for c in text): - key = bytes.fromhex(text) - else: - key = base64.urlsafe_b64decode(text + "=" * (-len(text) % 4)) - if len(key) != 32: - raise SystemExit("key must decode to 32 bytes") + try: + key = decode_key_material(args.key) + except Exception as e: + raise SystemExit(str(e)) else: key = os.urandom(32) compression = "none" if args.no_compress else "zlib" body = plain if args.no_compress else zlib.compress(plain, level=9) nonce = os.urandom(12) - ciphertext = AESGCM(key).encrypt(nonce, body, AAD) + ciphertext = AESGCM(key).encrypt(nonce, body, aad) header = { - "format": "q05-lidar-resource-v2", + "format": resource_format, "cipher": "AES-256-GCM", "kdf": "none", "compression": compression, - "aad": AAD.decode("utf-8"), + "aad": aad.decode("utf-8"), "nonce": b64u(nonce), "sha256": sha256, "plainSize": len(plain), @@ -80,7 +108,7 @@ def main(): output_path = Path(args.output) header_bytes = json.dumps(header, ensure_ascii=False, separators=(",", ":")).encode("utf-8") - output_path.write_bytes(b"Q05R2\x00" + struct.pack(">I", len(header_bytes)) + header_bytes + ciphertext) + output_path.write_bytes(magic + struct.pack(">I", len(header_bytes)) + header_bytes + ciphertext) print("resource.dat created") print(f"input: {input_path}") diff --git a/tools/usb_driver/android_winusb.inf b/tools/usb_driver/android_winusb.inf new file mode 100644 index 0000000..dcf4c28 --- /dev/null +++ b/tools/usb_driver/android_winusb.inf @@ -0,0 +1,195 @@ +; +; Android WinUsb driver installation. +; +[Version] +Signature = "$Windows NT$" +Class = AndroidUsbDeviceClass +ClassGuid = {3F966BD9-FA04-4ec5-991C-D326973B5128} +Provider = %ProviderName% +DriverVer = 08/28/2014,11.0.0000.00000 +CatalogFile.NTx86 = androidwinusb86.cat +CatalogFile.NTamd64 = androidwinusba64.cat + +[ClassInstall32] +Addreg = AndroidWinUsbClassReg + +[AndroidWinUsbClassReg] +HKR,,,0,%ClassName% +HKR,,Icon,,-1 + + +[Manufacturer] +%ProviderName% = Google, NTx86, NTamd64 + + +[Google.NTx86] + +;Google Nexus One +%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_0D02 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_0D02&MI_01 +%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_4E11 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4E12&MI_01 + +;Google Nexus S +%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_4E21 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4E22&MI_01 +%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_4E23 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4E24&MI_01 + +;Google Nexus 7 +%SingleBootLoaderInterface% = USB_Install, USB\VID_18D1&PID_4E40 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4E42&MI_01 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4E44&MI_01 + +;Google Nexus Q +%SingleBootLoaderInterface% = USB_Install, USB\VID_18D1&PID_2C10 +%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_2C11 + +;Google Nexus (generic) +%SingleBootLoaderInterface% = USB_Install, USB\VID_18D1&PID_4EE0 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4EE2&MI_01 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4EE4&MI_02 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4EE6&MI_01 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4EE7 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_D001 + +;Google Glass +%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_9001 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_9001&MI_01 + +;Google Glass EE1 + +%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_9003 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_9003&MI_01 +%SingleBootLoaderInterface% = USB_Install, USB\VID_18D1&PID_9004 + +;Google Glass EE2 + +%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_9005 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_9005&MI_00 +%SingleBootLoaderInterface% = USB_Install, USB\VID_18D1&PID_9006 + +;Project Tango (generic) +%SingleBootLoaderInterface% = USB_Install, USB\VID_18D1&PID_4D00 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4D02&MI_01 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4D04&MI_02 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4D06&MI_01 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4D07 + + +[Google.NTamd64] + +;Google Nexus One +%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_0D02 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_0D02&MI_01 +%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_4E11 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4E12&MI_01 + +;Google Nexus S +%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_4E21 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4E22&MI_01 +%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_4E23 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4E24&MI_01 + +;Google Nexus 7 +%SingleBootLoaderInterface% = USB_Install, USB\VID_18D1&PID_4E40 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4E42&MI_01 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4E44&MI_01 + +;Google Nexus Q +%SingleBootLoaderInterface% = USB_Install, USB\VID_18D1&PID_2C10 +%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_2C11 + +;Google Nexus (generic) +%SingleBootLoaderInterface% = USB_Install, USB\VID_18D1&PID_4EE0 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4EE2&MI_01 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4EE4&MI_02 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4EE6&MI_01 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4EE7 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_D001 + +;Google Glass +%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_9001 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_9001&MI_01 + +;Google Glass EE1 + +%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_9003 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_9003&MI_01 +%SingleBootLoaderInterface% = USB_Install, USB\VID_18D1&PID_9004 + +;Google Glass EE2 + +%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_9005 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_9005&MI_00 +%SingleBootLoaderInterface% = USB_Install, USB\VID_18D1&PID_9006 + +;Project Tango (generic) +%SingleBootLoaderInterface% = USB_Install, USB\VID_18D1&PID_4D00 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4D02&MI_01 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4D04&MI_02 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4D06&MI_01 +%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4D07 + + +[USB_Install] +Include = winusb.inf +Needs = WINUSB.NT + +[USB_Install.Services] +Include = winusb.inf +AddService = WinUSB,0x00000002,WinUSB_ServiceInstall + +[WinUSB_ServiceInstall] +DisplayName = %WinUSB_SvcDesc% +ServiceType = 1 +StartType = 3 +ErrorControl = 1 +ServiceBinary = %12%\WinUSB.sys + +[USB_Install.Wdf] +KmdfService = WINUSB, WinUSB_Install + +[WinUSB_Install] +KmdfLibraryVersion = 1.9 + +[USB_Install.HW] +AddReg = Dev_AddReg + +[Dev_AddReg] +HKR,,DeviceInterfaceGUIDs,0x10000,"{F72FE0D4-CBCB-407d-8814-9ED673D0DD6B}" + +[USB_Install.CoInstallers] +AddReg = CoInstallers_AddReg +CopyFiles = CoInstallers_CopyFiles + +[CoInstallers_AddReg] +HKR,,CoInstallers32,0x00010000,"WdfCoInstaller01009.dll,WdfCoInstaller","WinUSBCoInstaller2.dll" + +[CoInstallers_CopyFiles] +WinUSBCoInstaller2.dll +WdfCoInstaller01009.dll + +[DestinationDirs] +CoInstallers_CopyFiles=11 + +[SourceDisksNames] +1 = %DISK_NAME%,,,\i386 +2 = %DISK_NAME%,,,\amd64 + +[SourceDisksFiles.x86] +WinUSBCoInstaller2.dll = 1 +WdfCoInstaller01009.dll = 1 + +[SourceDisksFiles.amd64] +WinUSBCoInstaller2.dll = 2 +WdfCoInstaller01009.dll = 2 + +[Strings] +ProviderName = "Google, Inc." +SingleAdbInterface = "Android ADB Interface" +CompositeAdbInterface = "Android Composite ADB Interface" +SingleBootLoaderInterface = "Android Bootloader Interface" +WinUSB_SvcDesc = "Android USB Driver" +DISK_NAME = "Android WinUsb installation disk" +ClassName = "Android Device" diff --git a/tools/usb_driver/androidwinusb86.cat b/tools/usb_driver/androidwinusb86.cat new file mode 100644 index 0000000..3aa4b92 Binary files /dev/null and b/tools/usb_driver/androidwinusb86.cat differ diff --git a/tools/usb_driver/androidwinusba64.cat b/tools/usb_driver/androidwinusba64.cat new file mode 100644 index 0000000..40636d9 Binary files /dev/null and b/tools/usb_driver/androidwinusba64.cat differ diff --git a/tools/usb_driver/source.properties b/tools/usb_driver/source.properties new file mode 100644 index 0000000..fdb9336 --- /dev/null +++ b/tools/usb_driver/source.properties @@ -0,0 +1,3 @@ +Pkg.Revision=13 +Archive.HostOs=WINDOWS +Extra.Path=usb_driver