diff --git a/LICENSE b/LICENSE index 8546d22..56ef93d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License -Copyright (c) 2026 MeiYongAI +Copyright (c) 2026 krolchonok +Copyright (c) 2026 MeiYongAI (Toki modifications) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index f57bc0b..28a3468 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,200 @@ -
- Toki icon -

Toki

-

More control over your TikTok experience.

-

English · 简体中文

-

- Download · - Telegram · - Development · - Support -

-
+# Changelog -Toki is an LSPosed module for TikTok, with a Material 3 interface and 57 language options. +## 1.0.0 — 2026-09-30 -> **AI-generated project.** This module is developed with AI-generated code. It may contain errors; review changes and test carefully before relying on it. +### English -## Features +- New module identity: `com.toki.lsposed`, version code `1`, with a new release signing certificate. This installs separately from `com.seepd.toki`; disable the 0.x module in LSPosed before enabling 1.0.0. Settings are not migrated automatically. +- TikTok 47.0.3 and 46.8.3 adaptation, with method discovery and persistent results keyed to the host code and rules. +- Feed filters for topic and creator recommendation cards, keywords, duration, counts, offline insertion and AI-labeled videos or photos. Ad filtering also covers creator-profile video lists; other content rules remain limited to For You. +- Playback and clean-mode controls, progress-bar handling, translation and original/translated comment copying. +- Material 3 interface with 57 language options, feature status, configuration import/export and donation-image saving. +- Local validation: 252 unit tests passed; lint reported 0 errors and 28 warnings. Release builds use R8 and resource shrinking. The latest profile-ad changes still need device verification. -- **Feed filters** — ads across video feeds, including creator profiles; recommendation filters for LIVE, photos, AI-labeled videos and photos, topic/creator cards, keywords, duration, views and likes. Block offline video insertion. -- **Playback controls** — custom speeds, auto-hide controls, fullscreen playback, progress bar options and auto-scroll unlocking. -- **Media & tools** — watermark-free downloads, custom save folders, audio restriction handling, translation options and comment text copying. -- **Region settings** — region/SIM, language, time zone and location spoofing within TikTok; creator region display. -- **Easy management** — feature status, settings import/export and automatic method discovery. +### 中文 -## Get started +- 新模块包名为 `com.toki.lsposed`,版本号 `1`,使用新的正式签名。与 `com.seepd.toki` 分开安装;请先在 LSPosed 中停用 0.x 模块,再启用 1.0.0,配置不会自动迁移。 +- 适配 TikTok 47.0.3、46.8.3,支持方法查找,并按宿主代码与规则标识持久保存结果。 +- 信息流过滤涵盖话题及创作者推荐卡片、关键词、时长、数量范围、离线插入,以及标记为 AI 生成的视频和照片。广告过滤扩展到作者主页的视频列表,其他内容规则仍只作用于推荐页。 +- 完善播放清屏、进度条处理、翻译与评论原文/译文复制。 +- Material 3 界面提供 57 种语言选项、功能状态查看、配置导入导出及赞助图片保存。 +- 本地验证:252 项单元测试通过,Lint 0 错误、28 警告;Release 启用 R8 与资源压缩。最新作者主页广告过滤调整仍待实机验证。 -**Requires:** Android 9 or newer, a working LSPosed installation supporting **libxposed API 102**, and TikTok. Toki does not work as a standalone app without the framework. -**Adapted TikTok versions:** `47.0.3` · `46.8.3`. Builds from different stores may behave differently; other versions are not guaranteed to work. +[English](#changelog) | [中文](#中文) -1. Install Toki from [Releases](https://github.com/MeiYongAI/Toki/releases). -2. Enable it in LSPosed and select your installed TikTok in the module scope. -3. Open Toki and choose the features you want. -4. Open TikTok. If a method-discovery dialog appears, let it finish. TikTok closes when discovery succeeds; open it again to apply the results. +## 0.4.23 -Supported package names: `com.zhiliaoapp.musically` and `com.ss.android.ugc.trill`. +- Added a Home dashboard as the default destination with LSPosed service status and the installed TikTok version. +- Moved the Root-based TikTok restart action to Home and added Root-only cache clearing that preserves accounts, settings, drafts, and app data. +- Added manual interface language switching between Follow System, English, and Chinese, plus an action to reset all Toki settings. +- Added direct GitHub, Telegram, and issue-report links and expanded navigation to Home, General, Feed, and Downloads. +- Added an independent page-purification control for game-related entrances. -**Moving from 0.x:** Toki 1.0.0 uses a new module package, `io.github.meiyongai.toki`, and a new release signing key. It installs separately from `com.seepd.toki`; disable the previous module in LSPosed before enabling this one. Settings are not migrated automatically. Keep any needed configuration before uninstalling. See the [release notes](CHANGELOG.md). +## 0.4.22 -## Community +- Added an option to keep the video progress bar visible on standard videos shorter than 30 seconds; the existing hide-progress-bar option takes priority. +- Added optional author-location display with the matching country flag and region code beside the author name. +- Split author-avatar and author-information purification into independent controls. +- Updated TikTok 46.4.3 purification paths for the LIVE entry, search entry, top and bottom navigation, music title, and video feedback surveys. +- Added grouped purification controls for commercial and promotion labels, creative tools and templates, movie and anime entrances, sharing and creator incentives, and activity safety warnings. +- Moved feed payload, label, anchor, survey, and warning gates into a dedicated hook module and removed stale decompiler aliases and obsolete warning paths. -Join the [Telegram group](https://t.me/toki_lsposed) for discussion and feedback. When reporting a problem, include your Toki/TikTok versions, TikTok download source, steps to reproduce and relevant LSPosed logs. Remove personal information before sharing logs. +## 0.4.21 -## Development +- Added an optional page-purification control to hide the system status bar on TikTok's main video pages. +- Applied the status-bar control only to TikTok 46.4.3's `MainActivity` and kept it active across layout updates. +- Used Android's `WindowInsetsController` on Android 11+ with a legacy system-UI fallback for Android 8–10. -### Requirements +## 0.4.20 -- Android Studio with JDK 21 to run Gradle (Java source/target: 17) -- Android SDK 37 -- Android Gradle Plugin 9.4.0 and Gradle 9.7.1 (provided by the wrapper) +- Set official TikTok 46.4.3 as the sole implementation, testing, and maintenance target. +- Other TikTok versions are outside the support scope and receive no version-specific compatibility work. +- Removed 46.3.x download-location and comment-translation compatibility code. +- Split hook installation into isolated feature classes while keeping one module APK and entry point. +- Extended TikTok 46.4.3 For You filtering to feed dispatch and cache-backed list insertion paths. +- Preserved the exact text entered in view-count and like-count range fields while keeping numeric filtering unchanged. +- Added an option to disable offline cold-cache use when a network is available. -The app targets Android 35 and supports Android 9 (API 28) and newer. Dependencies are resolved from Google Maven and Maven Central. +## 0.4.19 -### Build and test +- Added compatibility for the comment-translation control and playback-completion handling on official TikTok 46.4.3. +- Fixed custom video, image, and GIF save locations on TikTok 46.3.2 and 46.4.3 by intercepting TikTok's MediaStore insertion bridge. +- Replaced the Android system directory picker with a built-in relative shared-storage path editor. +- Extended trending-topic and promotional-overlay purification for TikTok 46.4.3. +- Known issue: view-count and like-count filtering remains unreliable on TikTok 46.4.3 and may let some out-of-range videos through. -```bash -./gradlew :app:assembleDebug -./gradlew :app:testDebugUnitTest -./gradlew :app:lintRelease -./gradlew :app:assembleRelease -``` +## 0.4.18 -On Windows, use `gradlew.bat`. The main source sets are `app/src/main/java`, `app/src/main/res` and `app/src/test/java`. Release builds enable R8 and resource shrinking. With the local signing configuration present, the signed APK is written to `app/build/outputs/apk/release/app-release.apk`. +- Reworked the Material 3 settings screen and organized features into General, Feed, and Downloads sections. +- Added page purification with optional controls for author details, descriptions, music, action buttons, search, Tako, translation controls, and navigation bars. +- Added filters for AI-generated content, trending-topic bars, and content-rating prompts. +- Added independent GPS, system-language, and system-time-zone spoofing that follows the selected target region. +- Added an option to skip the startup login guide by dismissing skippable prompts only; it does not bypass login or verification. +- Improved view-count and like-count filters with support for full numbers and `K`/`M`/`B` suffixes. +- Removed the failed anti-burn-in feature; the standard default playback-speed option is now displayed as `1.0x`. +- Clarified download wording to state that all videos prefer watermark-free URLs. -For your own signed build, copy [keystore.properties.example](keystore/keystore.properties.example) to `keystore/keystore.properties` and supply your own keystore and credentials. Without that file, the release APK is unsigned. Set `JAVA_HOME` or Android Studio's Gradle JDK locally; no machine-specific JDK path is committed. +## 0.4.17 -The release keystore is intentionally kept outside version control. Keep `keystore/toki-release.jks` and `keystore/keystore.properties` private and backed up; losing this key prevents future updates from being installed over the same app. The current public certificate fingerprint is: +- Added support for official TikTok 46.3.2 and 46.3.3. +- Fixed the comment translation button not executing translation on TikTok 46.3.2. +- Adapted the anti-burn-in status Toast entry for TikTok 46.3.2. -`SHA-256 74:09:5F:B8:C2:88:80:15:FC:DD:B9:3E:3C:14:F6:F6:3D:2A:EA:33:40:AB:5E:F9:6D:60:B0:C0:14:E1:6C:07` +## 0.4.16 -Please run the unit tests and lint before submitting changes. Do not commit generated files from `app/build`. +- Improved anti-burn-in clear-screen state retention and restoration across videos and photo posts. +- Confirmed compatibility with official TikTok 46.3.3. +- Updated the launcher icon with a solid-color background. -## License +## 0.4.15 -[MIT](LICENSE). Dependency credits and licenses are listed in [Third-party notices](THIRD_PARTY_NOTICES.md). +- Limited support to the official TikTok client and removed compatibility code for modified clients. +- Removed grayscale mode and forced unmute settings that depended on third-party client bridges. +- Improved loop disabling so playback enters TikTok's native paused state and shows the replay frame. +- Fixed the need for two taps to replay a video and the feature becoming inactive after switching videos. +- Stopped forcing progress-bar synchronization to reduce reliance on high-frequency callbacks. -## Support +## 0.4.14 -If Toki is useful to you, you can support development through [Ko-fi](https://ko-fi.com/meiyongai) or [Alipay](app/src/main/res/drawable-nodpi/alipay.jpg). These options are also available in Toki under **Support Toki**. Donations are optional. +- Added default playback speeds: 1.0x, 1.25x, 1.5x, 1.75x, and 2.0x. +- Applied the selected speed to each new video after its first render without overriding manual speed changes. +- Adapted to the official TikTok 46.3.3 player interface. -
-USDT · TRC20 / Tron +## 0.4.13 -`TXoTeZLpbQdn4wZF51858bC3zCwS822HbB` +- Switched to libxposed API 102 and fixed the TikTok scope. +- Reworked the Material 3 settings UI, region selection, and media-directory selection. +- Added comment-translation state retention and scrolling-list synchronization. +- Added the two-finger long-press anti-burn-in clear-screen mode. +- Fixed the need for two taps to replay a video after loop disabling. +- Added the Root-based Restart TikTok action. -Use the TRC20 (Tron) network only. Verify the address and network before sending. +## 中文 -
+### 0.4.23 -## Disclaimer +- 新增首页并设为默认页面,可查看 LSPosed 服务状态与已安装的 TikTok 版本。 +- 将基于 Root 的 TikTok 重启操作移至首页,并新增仅清除缓存的 Root 操作;账号、设置、草稿和应用数据均会保留。 +- 新增界面语言手动切换,可选择跟随系统、English 或中文,并可一键重置全部 Toki 配置。 +- 新增 GitHub、Telegram 与问题反馈直达入口,导航扩展为首页、常规、信息流和下载。 +- 新增独立的游戏相关入口页面净化选项。 -Toki is an independent project, not affiliated with or endorsed by TikTok or ByteDance. It modifies app behavior and is provided **as is**, without warranties of compatibility, reliability or account safety. Use it at your own risk and follow applicable laws and platform terms. Respect creators' rights; download or reuse content only with permission. +### 0.4.22 + +- 新增“总是显示视频进度条”,移除普通视频短于 30 秒时隐藏进度条的限制;与隐藏进度条同时开启时,以隐藏为准。 +- 新增作者位置显示,可在作者昵称旁显示对应国家/地区旗帜和地区代码。 +- 将作者头像与作者信息拆分为两个独立的页面净化选项。 +- 更新 TikTok 46.4.3 的直播入口、搜索入口、顶部与底部导航栏、音乐标题和视频评价问卷净化路径。 +- 新增商业合作与推广、创作工具与模板、影视与动漫、分享与创作者激励、活动伤害警告等分类型净化选项。 +- 将 Feed 数据、标识、锚点、问卷和警告 Hook 整理到独立模块,并清除反编译伪包名与失效的旧警告路径。 + +### 0.4.21 + +- 新增页面净化选项,可隐藏 TikTok 主视频页面的手机系统状态栏。 +- 状态栏功能仅作用于 TikTok 46.4.3 的 `MainActivity`,并在页面布局更新后保持生效。 +- Android 11 及以上使用 `WindowInsetsController`,Android 8–10 使用兼容性的系统 UI 标记。 + +### 0.4.20 + +- 将官方 TikTok 46.4.3 设为唯一的实现、测试与维护目标。 +- 其他 TikTok 版本不在支持范围内,不再提供针对版本的专门适配。 +- 移除 46.3.x 保存位置和评论翻译兼容代码。 +- 将 Hook 按功能拆分为相互隔离的代码类,同时保持单一模块 APK 和入口。 +- 将 TikTok 46.4.3 推荐页过滤扩展到信息流消息分发和缓存列表插入路径。 +- 保留播放量与点赞数范围输入的原始文本,过滤计算仍使用解析后的数值。 +- 增加联网时禁用离线冷缓存的选项。 + +### 0.4.19 + +- 适配官方 TikTok 46.4.3 的评论翻译控件与播放完成处理。 +- 通过拦截 TikTok 的 MediaStore 写入桥,修复 TikTok 46.3.2 与 46.4.3 的视频、图片和 GIF 自定义保存位置。 +- 移除 Android 系统目录选择器,改用内置的共享存储相对路径编辑框。 +- 扩展 TikTok 46.4.3 的热点话题与推广浮层净化兼容。 +- 已知问题:TikTok 46.4.3 的播放量与点赞数筛选仍不可靠,少数范围外视频可能漏过过滤。 + +### 0.4.18 + +- 重构 Material 3 设置页,按常规、信息流和下载分类组织功能。 +- 新增页面净化,可选择隐藏作者信息、文案、音乐、互动按钮、搜索入口、Tako、翻译控件和导航栏。 +- 新增屏蔽 AI 生成内容、热点话题条和内容评级提示。 +- 新增 GPS、系统语言和系统时区伪装,均可独立开关并跟随目标地区。 +- 新增跳过启动登录引导,仅关闭启动时可跳过的登录提示,不绕过登录或验证。 +- 优化播放量与点赞量筛选,支持输入完整数字及 `K/M/B` 数量后缀。 +- 移除失败的防烧屏功能;默认倍速的标准项统一显示为 `1.0x`。 +- 优化下载说明,明确所有视频优先使用无水印地址。 + +### 0.4.17 + +- 支持官方 TikTok 46.3.2 与 46.3.3。 +- 修复 TikTok 46.3.2 评论页翻译按钮无法执行翻译的问题。 +- 适配 TikTok 46.3.2 的防烧屏状态提示 Toast 入口。 + +### 0.4.16 + +- 改进防烧屏清屏模式在视频与图集中的状态保持和恢复,增强页面切换后的稳定性。 +- 明确适配官方 TikTok 46.3.3。 +- 更新启动器图标,使用纯色背景。 + +### 0.4.15 + +- 明确仅支持官方 TikTok,移除第三方修改客户端专用兼容代码。 +- 移除仅依赖第三方客户端桥接、在官方 TikTok 中无效的灰度模式和强制取消静音设置。 +- 优化禁止循环播放:播放结束后进入 TikTok 原生暂停状态,并显示重播首帧。 +- 修复播放结束后需要点击两次才能重新播放,以及切换视频后功能失效的问题。 +- 不再强制同步播放进度条,减少对 TikTok 高频进度回调的依赖。 + +### 0.4.14 + +- 新增默认播放速度:1.0x、1.25x、1.5x、1.75x 和 2.0x。 +- 每条新视频首次渲染后自动应用所选速度,不覆盖当前视频内手动选择的倍速。 +- 适配官方 TikTok 46.3.3 播放器接口。 + +### 0.4.13 + +- 使用 libxposed API 102,并固定 TikTok 作用域。 +- 重构 Material 3 设置界面、地区选择和媒体保存目录选择。 +- 增加评论翻译状态保持及滚动列表同步。 +- 增加双指长按防烧屏清屏模式。 +- 修复禁止循环播放后需要点击两次才能重新播放的问题。 +- 增加 Root 重启 TikTok 操作。 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 45b2387..d860839 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,7 +17,7 @@ android { compileSdk = 37 defaultConfig { - applicationId = "io.github.meiyongai.toki" + applicationId = "com.toki.lsposed" minSdk = 28 targetSdk = 35 versionCode = 1 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6c4c354..a390e64 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,9 +1,9 @@ - - + @@ -40,7 +40,7 @@ diff --git a/app/src/main/java/com/toki/lsposed/TokiApplication.kt b/app/src/main/java/com/toki/lsposed/TokiApplication.kt index 3fe2572..d702bce 100644 --- a/app/src/main/java/com/toki/lsposed/TokiApplication.kt +++ b/app/src/main/java/com/toki/lsposed/TokiApplication.kt @@ -3,6 +3,7 @@ package com.toki.lsposed import android.app.Application import com.toki.lsposed.provider.ConfigClient import com.toki.lsposed.util.LSPosedStatusHelper +import com.toki.lsposed.ui.TikTokRootLogMonitor /** 管理端进程入口,在界面创建前准备本地设置和框架发布通道。 */ class TokiApplication : Application() { @@ -15,5 +16,7 @@ class TokiApplication : Application() { super.onCreate() ConfigClient.init(this) LSPosedStatusHelper.init() + // Root 诊断在 Toki 管理端独立运行,不向 TikTok 注入监控线程。 + TikTokRootLogMonitor.ensureStarted() } } diff --git a/app/src/main/java/com/toki/lsposed/hook/AuthorLocationHook.kt b/app/src/main/java/com/toki/lsposed/hook/AuthorLocationHook.kt index 8247d13..ee4b083 100644 --- a/app/src/main/java/com/toki/lsposed/hook/AuthorLocationHook.kt +++ b/app/src/main/java/com/toki/lsposed/hook/AuthorLocationHook.kt @@ -45,7 +45,7 @@ object AuthorLocationHook { * Unit: 无返回值。 * * Callers: - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 宿主应用启动与前台初始化时同步。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 宿主应用启动与前台初始化时同步。 */ fun refreshConfig(context: Context) { ConfigClient.init(context) diff --git a/app/src/main/java/com/toki/lsposed/hook/AutoScrollHook.kt b/app/src/main/java/com/toki/lsposed/hook/AutoScrollHook.kt index 44dc2c5..244702b 100644 --- a/app/src/main/java/com/toki/lsposed/hook/AutoScrollHook.kt +++ b/app/src/main/java/com/toki/lsposed/hook/AutoScrollHook.kt @@ -65,7 +65,7 @@ object AutoScrollHook { * Unit: 无返回值。 * * Callers: - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 宿主冷启动完成时分发。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 宿主冷启动完成时分发。 */ fun refreshConfig(context: Context) { // 配置由 ConfigClient 统一管理并缓存 diff --git a/app/src/main/java/com/toki/lsposed/hook/CommentCopyHook.kt b/app/src/main/java/com/toki/lsposed/hook/CommentCopyHook.kt index 54d0ad2..f3d2f2d 100644 --- a/app/src/main/java/com/toki/lsposed/hook/CommentCopyHook.kt +++ b/app/src/main/java/com/toki/lsposed/hook/CommentCopyHook.kt @@ -17,7 +17,7 @@ object CommentCopyHook { * 初始化共享配置;每次复制直接读取最新快照,不另存功能开关。 * @param context 已附加的宿主上下文。 * @return Unit。 - * Callers: TokiModule.hookApplication。 + * Callers: TokiModule.hookApplicationIfNeeded。 */ fun refreshConfig(context: Context) = ConfigClient.init(context) diff --git a/app/src/main/java/com/toki/lsposed/hook/CommentTranslateHook.kt b/app/src/main/java/com/toki/lsposed/hook/CommentTranslateHook.kt index 923fd86..65c8dad 100644 --- a/app/src/main/java/com/toki/lsposed/hook/CommentTranslateHook.kt +++ b/app/src/main/java/com/toki/lsposed/hook/CommentTranslateHook.kt @@ -30,7 +30,7 @@ import java.lang.reflect.Method * * Callers: * - `com.toki.lsposed.hook.TokiModule.onPackageLoaded`: 宿主目标包加载就绪后初始化挂载。 - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 目标应用初始化时同步开关配置。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 目标应用初始化时同步开关配置。 */ object CommentTranslateHook { @@ -58,7 +58,7 @@ object CommentTranslateHook { * Unit: 无返回值。 * * Callers: - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 应用启动阶段同步配置。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 应用启动阶段同步配置。 */ fun refreshConfig(context: Context) { isEnabled = ConfigClient.getBoolean(context, KEY_COMMENT_TRANSLATE_ENABLED) diff --git a/app/src/main/java/com/toki/lsposed/hook/DownloadHook.kt b/app/src/main/java/com/toki/lsposed/hook/DownloadHook.kt index 77709d5..c0868f8 100644 --- a/app/src/main/java/com/toki/lsposed/hook/DownloadHook.kt +++ b/app/src/main/java/com/toki/lsposed/hook/DownloadHook.kt @@ -115,7 +115,7 @@ object DownloadHook { * Unit: 无返回值。 * * Callers: - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 宿主冷启动完成时分发。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 宿主冷启动完成时分发。 */ fun refreshConfig(context: Context) { // 配置由 ConfigClient 统一管理并缓存 diff --git a/app/src/main/java/com/toki/lsposed/hook/FeedFilterHook.kt b/app/src/main/java/com/toki/lsposed/hook/FeedFilterHook.kt index fd87917..4661dea 100644 --- a/app/src/main/java/com/toki/lsposed/hook/FeedFilterHook.kt +++ b/app/src/main/java/com/toki/lsposed/hook/FeedFilterHook.kt @@ -117,7 +117,7 @@ object FeedFilterHook { * 确保配置中心已初始化。 * @param context 已附加的宿主上下文。 * @return Unit。 - * Callers: TokiModule.hookApplication。 + * Callers: TokiModule.hookApplicationIfNeeded。 */ fun refreshConfig(context: Context) = ConfigClient.init(context) diff --git a/app/src/main/java/com/toki/lsposed/hook/GpsHook.kt b/app/src/main/java/com/toki/lsposed/hook/GpsHook.kt index 19ca3a7..00c7bbc 100644 --- a/app/src/main/java/com/toki/lsposed/hook/GpsHook.kt +++ b/app/src/main/java/com/toki/lsposed/hook/GpsHook.kt @@ -42,7 +42,7 @@ object GpsHook { * @param context 宿主目标应用的上下文对象。 * * Callers: - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 宿主启动时触发配置加载。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 宿主启动时触发配置加载。 */ fun refreshConfig(context: Context) { isEnabled = ConfigClient.getBoolean(context, "gps_spoof_enabled") diff --git a/app/src/main/java/com/toki/lsposed/hook/HookRuntime.kt b/app/src/main/java/com/toki/lsposed/hook/HookRuntime.kt index ccfbeda..797377e 100644 --- a/app/src/main/java/com/toki/lsposed/hook/HookRuntime.kt +++ b/app/src/main/java/com/toki/lsposed/hook/HookRuntime.kt @@ -34,7 +34,7 @@ internal object HookRuntime { * 注册仅响应管理端查询的诊断接收器,不创建定时发布任务。 * @param value 已附加的宿主上下文。 * @return Unit。 - * Callers: TokiModule.hookApplication。 + * Callers: TokiModule.hookApplicationIfNeeded。 */ @Synchronized fun attach(value: Context) { if (receiver != null || process != value.packageName) return @@ -163,7 +163,7 @@ internal object HookRuntime { * @param feature 已注册的功能标识。 * @param configure 初始化或热更新操作。 * @return Unit。 - * Callers: TokiModule.refreshConfiguration。 + * Callers: TokiModule.refreshEnabledConfiguration。 */ fun configure(feature: String, configure: () -> Unit) { if (synchronized(this) { handles[feature].isNullOrEmpty() }) return diff --git a/app/src/main/java/com/toki/lsposed/hook/HostScanController.kt b/app/src/main/java/com/toki/lsposed/hook/HostScanController.kt index cef8b18..edabd89 100644 --- a/app/src/main/java/com/toki/lsposed/hook/HostScanController.kt +++ b/app/src/main/java/com/toki/lsposed/hook/HostScanController.kt @@ -20,7 +20,7 @@ internal class HostScanController private constructor(private val app: Applicati * 在宿主主进程订阅活动生命周期;进程级控制器独立调度退出,窗口随前台活动释放。 * @param app 当前宿主Application。 * @return Unit。 - * Callers: TokiModule.hookApplication。 + * Callers: TokiModule.hookApplicationIfNeeded。 */ fun attach(app: Application) { if (controller != null) return @@ -39,6 +39,8 @@ internal class HostScanController private constructor(private val app: Applicati private val main = Handler(Looper.getMainLooper()) private var foreground = WeakReference(null) private var dialog: HostScanDialog? = null + /** 扫描 UI 创建失败时只禁用 UI,不得影响宿主扫描及业务 Hook。 */ + private var dialogCreationFailed = false private val refresh = object : Runnable { /** 更新可见窗口;无前台活动时停止刷新。@return Unit。Callers: 主线程Handler。 */ override fun run() { @@ -58,10 +60,27 @@ internal class HostScanController private constructor(private val app: Applicati scheduleClose() val state = session.status if (state.phase == HostScanPhase.IDLE) { dismissDialog(); return } - if (dialog == null) { - dialog = HostScanDialog(activity) { session.dismissFailure(); dismissDialog() }.also { it.show() } + if (dialog == null && !dialogCreationFailed) { + runCatching { + HostScanDialog(activity) { session.dismissFailure(); dismissDialog() }.also { it.show() } + }.onSuccess { created -> + dialog = created + HookRuntime.event("TokiHostScan", "扫描窗口已创建 pid=${Process.myPid()}") + }.onFailure { error -> + // 扫描窗口是诊断/适配 UI,不是宿主功能依赖。任何 UI/资源异常都必须被吞掉, + // 否则异常发生在主线程会直接终止 TikTok。后台 DEX 扫描继续运行。 + dialogCreationFailed = true + Log.e("TokiHostScan", "扫描窗口创建失败,继续后台扫描,不影响 TikTok", error) + HookRuntime.failure("HostScanDialog", error, "扫描窗口不可用,已降级为后台扫描") + } + } + + runCatching { dialog?.render(state) }.onFailure { error -> + dialogCreationFailed = true + Log.e("TokiHostScan", "扫描窗口刷新失败,继续后台扫描,不影响 TikTok", error) + HookRuntime.failure("HostScanDialog", error, "扫描窗口刷新失败,已降级为后台扫描") + dismissDialog() } - dialog!!.render(state) } /** @@ -105,6 +124,7 @@ internal class HostScanController private constructor(private val app: Applicati /** 活动进入前台后恢复窗口。@param activity 宿主活动。@return Unit。Callers: Android Application。 */ override fun onActivityResumed(activity: Activity) { dismissDialog() + dialogCreationFailed = false foreground = WeakReference(activity) main.post(refresh) } diff --git a/app/src/main/java/com/toki/lsposed/hook/HostScanDialog.kt b/app/src/main/java/com/toki/lsposed/hook/HostScanDialog.kt index dfc2985..404278a 100644 --- a/app/src/main/java/com/toki/lsposed/hook/HostScanDialog.kt +++ b/app/src/main/java/com/toki/lsposed/hook/HostScanDialog.kt @@ -1,8 +1,6 @@ package com.toki.lsposed.hook import android.app.Activity -import android.content.ContextWrapper -import android.content.res.Resources import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.view.Window @@ -46,7 +44,7 @@ import com.toki.lsposed.ui.tokiColorScheme * Callers: HostScanController.render。 */ internal class HostScanDialog(activity: Activity, onFailureDismiss: () -> Unit) : - ComponentDialog(ScanContext(activity), android.R.style.Theme_Material_Light_Dialog_NoActionBar) { + ComponentDialog(activity, android.R.style.Theme_Material_Light_Dialog_NoActionBar) { private var status by mutableStateOf(HostScanStatus()) private val composeView = ComposeView(context).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) @@ -93,27 +91,6 @@ internal class HostScanDialog(activity: Activity, onFailureDismiss: () -> Unit) fun render(state: HostScanStatus) { status = state } } -/** - * 保留宿主活动的窗口服务,隔离 Compose 读取的模块资源和主题。 - * @param activity 宿主前台活动。 - * Callers: HostScanDialog 构造函数。 - */ -private class ScanContext(activity: Activity) : ContextWrapper(activity) { - private val moduleContext = activity.createPackageContext("io.github.meiyongai.toki", 0) - .createConfigurationContext(activity.resources.configuration) - private val moduleTheme = moduleContext.resources.newTheme().apply { - applyStyle(android.R.style.Theme_Material_Light_Dialog_NoActionBar, true) - } - /** 提供模块资源。@return Toki 资源集合。Callers: Compose、ComponentDialog。 */ - override fun getResources(): Resources = moduleContext.resources - /** 提供模块资产。@return Toki 资产管理器。Callers: Android 字体与资源加载。 */ - override fun getAssets(): android.content.res.AssetManager = moduleContext.assets - /** 提供与模块资源一致的主题。@return 窗口主题。Callers: ComponentDialog。 */ - override fun getTheme(): Resources.Theme = moduleTheme - /** 解析模块 Compose 类型。@return 模块加载器。Callers: Android 视图系统。 */ - override fun getClassLoader(): ClassLoader = checkNotNull(HostScanDialog::class.java.classLoader) -} - /** * 渲染紧凑 Material 3 窗口;说明和方法区域内部滚动,进度与状态固定在底部。 * @param state 当前阶段和工作量。 diff --git a/app/src/main/java/com/toki/lsposed/hook/ImmersiveFullScreenHook.kt b/app/src/main/java/com/toki/lsposed/hook/ImmersiveFullScreenHook.kt index 6116b14..f0e222a 100644 --- a/app/src/main/java/com/toki/lsposed/hook/ImmersiveFullScreenHook.kt +++ b/app/src/main/java/com/toki/lsposed/hook/ImmersiveFullScreenHook.kt @@ -92,7 +92,7 @@ object ImmersiveFullScreenHook { * Unit: 无返回值。 * * Callers: - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 宿主冷启动完成时分发。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 宿主冷启动完成时分发。 */ fun refreshConfig(context: Context) { // 配置由 ConfigClient 统一管理并缓存 diff --git a/app/src/main/java/com/toki/lsposed/hook/LocaleHook.kt b/app/src/main/java/com/toki/lsposed/hook/LocaleHook.kt index 5d51d66..12cfd61 100644 --- a/app/src/main/java/com/toki/lsposed/hook/LocaleHook.kt +++ b/app/src/main/java/com/toki/lsposed/hook/LocaleHook.kt @@ -29,7 +29,7 @@ import java.util.Locale * * Callers: * - `com.toki.lsposed.hook.TokiModule.onPackageLoaded`: 宿主目标包加载就绪后初始化拦截器。 - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 宿主应用启动时刷新并应用配置。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 宿主应用启动时刷新并应用配置。 */ object LocaleHook { @@ -88,7 +88,7 @@ object LocaleHook { * Unit: 无返回值。 * * Callers: - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 宿主启动时触发配置加载。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 宿主启动时触发配置加载。 */ fun refreshConfig(context: Context) { isEnabled = ConfigClient.getBoolean(context, "language_spoof_enabled") @@ -126,7 +126,7 @@ object LocaleHook { * Unit: 无返回值。 * * Callers: - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 宿主 Application 创建时调用。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 宿主 Application 创建时调用。 */ fun applyToApplication(app: Application) { if (!isEnabled) { diff --git a/app/src/main/java/com/toki/lsposed/hook/MusicUnlockHook.kt b/app/src/main/java/com/toki/lsposed/hook/MusicUnlockHook.kt index a843a40..4ce3ce5 100644 --- a/app/src/main/java/com/toki/lsposed/hook/MusicUnlockHook.kt +++ b/app/src/main/java/com/toki/lsposed/hook/MusicUnlockHook.kt @@ -80,7 +80,7 @@ object MusicUnlockHook { * Unit: 无返回值。 * * Callers: - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 宿主冷启动完成时分发。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 宿主冷启动完成时分发。 */ fun refreshConfig(context: Context) { // 配置由 ConfigClient 统一管理并缓存 diff --git a/app/src/main/java/com/toki/lsposed/hook/PlaybackSpeedHook.kt b/app/src/main/java/com/toki/lsposed/hook/PlaybackSpeedHook.kt index 724690f..4364969 100644 --- a/app/src/main/java/com/toki/lsposed/hook/PlaybackSpeedHook.kt +++ b/app/src/main/java/com/toki/lsposed/hook/PlaybackSpeedHook.kt @@ -132,7 +132,7 @@ object PlaybackSpeedHook { * Unit: 无返回值。 * * Callers: - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 宿主进程初始化时同步。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 宿主进程初始化时同步。 */ fun refreshConfig(context: Context) { if (speedMemory == null) speedMemory = PlaybackSpeedMemory( diff --git a/app/src/main/java/com/toki/lsposed/hook/SimHook.kt b/app/src/main/java/com/toki/lsposed/hook/SimHook.kt index 288f390..78a77c7 100644 --- a/app/src/main/java/com/toki/lsposed/hook/SimHook.kt +++ b/app/src/main/java/com/toki/lsposed/hook/SimHook.kt @@ -90,7 +90,7 @@ object SimHook { * @param context 宿主目标应用的上下文对象。 * * Callers: - * - `com.toki.lsposed.hook.SimHook.hookApplication`: 宿主 Application 创建时触发同步。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 宿主 Application 创建时触发同步。 */ fun refreshConfig(context: Context) { isEnabled = ConfigClient.getBoolean(context, "sim_spoof_enabled") diff --git a/app/src/main/java/com/toki/lsposed/hook/StatusBarHook.kt b/app/src/main/java/com/toki/lsposed/hook/StatusBarHook.kt index 77825d6..91487ec 100644 --- a/app/src/main/java/com/toki/lsposed/hook/StatusBarHook.kt +++ b/app/src/main/java/com/toki/lsposed/hook/StatusBarHook.kt @@ -485,7 +485,7 @@ object StatusBarHook { * Unit: 无返回值。 * * Callers: - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 宿主冷启动完成时分发。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 宿主冷启动完成时分发。 */ fun refreshConfig(context: Context) { // 配置由 ConfigClient 统一管理与跨进程分发 diff --git a/app/src/main/java/com/toki/lsposed/hook/TimeZoneHook.kt b/app/src/main/java/com/toki/lsposed/hook/TimeZoneHook.kt index 32461f0..b9e27d0 100644 --- a/app/src/main/java/com/toki/lsposed/hook/TimeZoneHook.kt +++ b/app/src/main/java/com/toki/lsposed/hook/TimeZoneHook.kt @@ -49,7 +49,7 @@ object TimeZoneHook { * @param context 宿主目标应用的上下文对象。 * * Callers: - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 宿主启动时触发配置加载。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 宿主启动时触发配置加载。 */ fun refreshConfig(context: Context) { isEnabled = ConfigClient.getBoolean(context, "timezone_spoof_enabled") @@ -78,7 +78,7 @@ object TimeZoneHook { * @param app 宿主 Application 实例。 * * Callers: - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 宿主 Application 创建时注入生效。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 宿主 Application 创建时注入生效。 */ fun applyToApplication(app: Application) { if (!isEnabled) { diff --git a/app/src/main/java/com/toki/lsposed/hook/TokiModule.kt b/app/src/main/java/com/toki/lsposed/hook/TokiModule.kt index 43672cf..a6b7cf1 100644 --- a/app/src/main/java/com/toki/lsposed/hook/TokiModule.kt +++ b/app/src/main/java/com/toki/lsposed/hook/TokiModule.kt @@ -1,9 +1,10 @@ package com.toki.lsposed.hook import android.app.Application -import android.app.Instrumentation import android.util.Log import com.toki.lsposed.provider.ConfigClient +import com.toki.lsposed.provider.ConfigSchema +import com.toki.lsposed.provider.TikTokCrashDiagnostics import io.github.libxposed.api.XposedInterface import io.github.libxposed.api.XposedModule import io.github.libxposed.api.XposedModuleInterface.ModuleLoadedParam @@ -47,6 +48,7 @@ class TokiModule : XposedModule() { override fun onModuleLoaded(param: ModuleLoadedParam) { HookRuntime.start(param.processName) { tag, message -> log(Log.INFO, tag, message) } processName = param.processName + TikTokCrashDiagnostics.install(this, param.processName) configurationAvailableAtLoad = ConfigClient.initHost({ getRemotePreferences(ConfigClient.FRAMEWORK_GROUP) }) { val error = ConfigClient.syncError if (error != null) HookRuntime.failure("ConfigClient", error, "配置未就绪,功能暂停") @@ -57,52 +59,97 @@ class TokiModule : XposedModule() { } /** - * 在宿主 onCreate 前应用已验证配置;诊断和方法查找独立于配置连接状态。 + * 仅在确实存在已开启功能时监听 Application 创建。 * - * @return Unit。 - * - * Callers: - * - `com.toki.lsposed.hook.TokiModule.onPackageLoaded`: 目标包加载时触发。 + * 所有功能关闭时不注册任何宿主 Application Hook,从源头消除模块对 TikTok + * 启动早期 Instrumentation 调用链的改动。 */ - private fun hookApplication() { - val onCreateMethod = Instrumentation::class.java.getMethod("callApplicationOnCreate", Application::class.java) - hook(onCreateMethod).setExceptionMode(XposedInterface.ExceptionMode.PASSTHROUGH).intercept { chain -> - val app = chain.args[0] as Application - HookRuntime.attach(app) - if (processName == app.packageName) HostScanController.attach(app) - if (configurationAvailableAtLoad && ConfigClient.isReady) { - refreshConfiguration(app) - } - chain.proceed() + private fun hookApplicationIfNeeded() { + if (!needsApplicationLifecycle()) return + + val onCreateMethod = runCatching { + Class.forName( + "android.app.Instrumentation", + false, + Application::class.java.classLoader + ).getMethod("callApplicationOnCreate", Application::class.java) + }.getOrElse { error -> + HookRuntime.failure("ApplicationLifecycle", error, "Application Hook 不可用,宿主不被阻断") + return } - log(Log.INFO, TAG, "Application 配置应用与独立诊断入口已注册") + + hook(onCreateMethod) + .setExceptionMode(XposedInterface.ExceptionMode.PASSTHROUGH) + .intercept { chain -> + // 诊断/扫描 UI 均不得成为宿主启动失败源。 + runCatching { + val app = chain.args[0] as? Application ?: return@runCatching + HookRuntime.attach(app) + if (processName == app.packageName) HostScanController.attach(app) + refreshEnabledConfiguration(app) + }.onFailure { error -> + HookRuntime.failure( + "ApplicationLifecycle", + error, + "宿主启动辅助逻辑失败,已放行原调用" + ) + } + chain.proceed() + } + HookRuntime.event("TokiModule", "仅因存在已开启功能才注册 Application 生命周期 Hook") } /** - * 逐功能应用完整配置,各功能错误明确报告且不影响其它功能或宿主启动。 - * @param app 宿主应用实例。 - * @return Unit。 - * Callers: hookApplication;运行期更新由各功能的配置监听负责。 + * 判断是否有功能需要 Application 上下文或宿主扫描窗口。 */ - private fun refreshConfiguration(app: Application) { - HookRuntime.configure("SimHook") { SimHook.refreshConfig(app) } - HookRuntime.configure("LocaleHook") { LocaleHook.refreshConfig(app); LocaleHook.applyToApplication(app) } - HookRuntime.configure("TimeZoneHook") { TimeZoneHook.refreshConfig(app); TimeZoneHook.applyToApplication(app) } - HookRuntime.configure("GpsHook") { GpsHook.refreshConfig(app) } - HookRuntime.configure("PlaybackSpeedHook") { PlaybackSpeedHook.refreshConfig(app) } - HookRuntime.configure("CommentTranslateHook") { CommentTranslateHook.refreshConfig(app) } - HookRuntime.configure("VideoTranslateHook") { VideoTranslateHook.refreshConfig(app) } - HookRuntime.configure("CommentCopyHook") { CommentCopyHook.refreshConfig(app) } - HookRuntime.configure("AuthorLocationHook") { AuthorLocationHook.refreshConfig(app) } - HookRuntime.configure("ImmersiveFullScreenHook") { ImmersiveFullScreenHook.refreshConfig(app) } - HookRuntime.configure("AutoScrollHook") { AutoScrollHook.refreshConfig(app) } - HookRuntime.configure("FeedFilterHook") { FeedFilterHook.refreshConfig(app) } - HookRuntime.configure("DownloadHook") { DownloadHook.refreshConfig(app) } - HookRuntime.configure("MusicUnlockHook") { MusicUnlockHook.refreshConfig(app) } - HookRuntime.configure("StatusBarHook") { StatusBarHook.refreshConfig(app) } - HookRuntime.configure("VideoDurationAlertHook") { VideoDurationAlertHook.refreshConfig(app) } + private fun needsApplicationLifecycle(): Boolean = + APPLICATION_LIFECYCLE_FEATURES.any(::featureEnabled) || HOST_SYMBOL_FEATURES.any(::featureEnabled) + + /** + * 判断指定功能是否至少有一个真实开关处于开启。 + */ + private fun featureEnabled(feature: String): Boolean = + ConfigSchema.featureSwitches[feature]?.any { key -> ConfigClient.getBoolean(key) } == true + + /** + * 应用配置仅覆盖已注册且已启用的功能;关闭的功能不会触发初始化读取。 + */ + private fun refreshEnabledConfiguration(app: Application) { + HookRuntime.configure("SimHook") { if (featureEnabled("SimHook")) SimHook.refreshConfig(app) } + HookRuntime.configure("LocaleHook") { if (featureEnabled("LocaleHook")) { LocaleHook.refreshConfig(app); LocaleHook.applyToApplication(app) } } + HookRuntime.configure("TimeZoneHook") { if (featureEnabled("TimeZoneHook")) { TimeZoneHook.refreshConfig(app); TimeZoneHook.applyToApplication(app) } } + HookRuntime.configure("GpsHook") { if (featureEnabled("GpsHook")) GpsHook.refreshConfig(app) } + HookRuntime.configure("PlaybackSpeedHook") { if (featureEnabled("PlaybackSpeedHook")) PlaybackSpeedHook.refreshConfig(app) } + HookRuntime.configure("CommentTranslateHook") { if (featureEnabled("CommentTranslateHook")) CommentTranslateHook.refreshConfig(app) } + HookRuntime.configure("VideoTranslateHook") { if (featureEnabled("VideoTranslateHook")) VideoTranslateHook.refreshConfig(app) } + HookRuntime.configure("CommentCopyHook") { if (featureEnabled("CommentCopyHook")) CommentCopyHook.refreshConfig(app) } + HookRuntime.configure("AuthorLocationHook") { if (featureEnabled("AuthorLocationHook")) AuthorLocationHook.refreshConfig(app) } + HookRuntime.configure("ImmersiveFullScreenHook") { if (featureEnabled("ImmersiveFullScreenHook")) ImmersiveFullScreenHook.refreshConfig(app) } + HookRuntime.configure("AutoScrollHook") { if (featureEnabled("AutoScrollHook")) AutoScrollHook.refreshConfig(app) } + HookRuntime.configure("FeedFilterHook") { if (featureEnabled("FeedFilterHook")) FeedFilterHook.refreshConfig(app) } + HookRuntime.configure("DownloadHook") { if (featureEnabled("DownloadHook")) DownloadHook.refreshConfig(app) } + HookRuntime.configure("MusicUnlockHook") { if (featureEnabled("MusicUnlockHook")) MusicUnlockHook.refreshConfig(app) } + HookRuntime.configure("StatusBarHook") { if (featureEnabled("StatusBarHook")) StatusBarHook.refreshConfig(app) } + HookRuntime.configure("VideoDurationAlertHook") { if (featureEnabled("VideoDurationAlertHook")) VideoDurationAlertHook.refreshConfig(app) } } + /** + * 有 Application 上下文需求的功能。其它功能通过 ConfigClient 快照实时读取。 + */ + private val APPLICATION_LIFECYCLE_FEATURES = setOf( + "SimHook", "LocaleHook", "TimeZoneHook", "GpsHook", "PlaybackSpeedHook" + ) + + /** + * 会访问 HostSymbols 的功能。没有这些功能开启时绝不触发 DEX 扫描。 + */ + private val HOST_SYMBOL_FEATURES = setOf( + "PlaybackSpeedHook", "CommentTranslateHook", "VideoTranslateHook", + "CommentCopyHook", "AuthorLocationHook", "ProgressBarHook", + "AutoCleanModeHook", "ImmersiveFullScreenHook", "AutoScrollHook", + "FeedFilterHook", "MusicUnlockHook" + ) + /** * 目标应用包完成加载时的生命周期回调。 * @@ -125,22 +172,16 @@ class TokiModule : XposedModule() { "目标应用包已加载 -> ${param.packageName} (isFirstPackage=${param.isFirstPackage})" ) - // 优先注册宿主应用 Application 生命周期监听,确保跨进程配置中枢即刻就绪 - hookApplication() - if (!configurationAvailableAtLoad) return - // 注册 SIM 卡与国家代码伪装 Hook - HookRuntime.install("SimHook") { SimHook.init(this) } - - // 注册系统语言与 Locale 伪装 Hook - HookRuntime.install("LocaleHook") { LocaleHook.init(this) } + // 只有明确开启的系统级功能才修改 Android/Java 框架方法。 + installIfEnabled("SimHook") { SimHook.init(this) } + installIfEnabled("LocaleHook") { LocaleHook.init(this) } + installIfEnabled("TimeZoneHook") { TimeZoneHook.init(this) } + installIfEnabled("GpsHook") { GpsHook.init(this) } - // 注册系统时区伪装 Hook - HookRuntime.install("TimeZoneHook") { TimeZoneHook.init(this) } - - // 注册系统 GPS 定位伪装 Hook - HookRuntime.install("GpsHook") { GpsHook.init(this) } + // Application Hook 也只在必要时注册;全部关闭时目标进程保持零业务注入。 + hookApplicationIfNeeded() } @@ -152,46 +193,61 @@ class TokiModule : XposedModule() { */ override fun onPackageReady(param: PackageReadyParam) { if (param.packageName !in SUPPORTED_PACKAGES || !param.isFirstPackage) return - if (!HostSymbols.initialize(param.applicationInfo, processName == param.packageName)) return if (!configurationAvailableAtLoad) return - HookRuntime.install("FeedFilterHook") { FeedFilterHook.init(this, param.classLoader) } + + // 没有需要符号解析的功能时,连 DEX 扫描都不要触发。 + if (HOST_SYMBOL_FEATURES.any(::featureEnabled) && + !HostSymbols.initialize(param.applicationInfo, processName == param.packageName) + ) return + installIfEnabled("FeedFilterHook") { FeedFilterHook.init(this, param.classLoader) } // 注册固定播放倍速 Hook - HookRuntime.install("PlaybackSpeedHook") { PlaybackSpeedHook.init(this, param.classLoader) } + installIfEnabled("PlaybackSpeedHook") { PlaybackSpeedHook.init(this, param.classLoader) } // 注册评论区一键翻译 Hook - HookRuntime.install("CommentTranslateHook") { CommentTranslateHook.init(this, param.classLoader) } + installIfEnabled("CommentTranslateHook") { CommentTranslateHook.init(this, param.classLoader) } // 注册视频正文描述原生翻译 Hook - HookRuntime.install("VideoTranslateHook") { VideoTranslateHook.init(this, param.classLoader) } + installIfEnabled("VideoTranslateHook") { VideoTranslateHook.init(this, param.classLoader) } // 注册评论复制仅复制正文 Hook - HookRuntime.install("CommentCopyHook") { CommentCopyHook.init(this, param.classLoader) } + installIfEnabled("CommentCopyHook") { CommentCopyHook.init(this, param.classLoader) } // 注册作者地理位置显示 Hook - HookRuntime.install("AuthorLocationHook") { AuthorLocationHook.init(this, param.classLoader) } + installIfEnabled("AuthorLocationHook") { AuthorLocationHook.init(this, param.classLoader) } // 注册视频进度条常显 Hook - HookRuntime.install("ProgressBarHook") { ProgressBarHook.init(this, param.classLoader) } + installIfEnabled("ProgressBarHook") { ProgressBarHook.init(this, param.classLoader) } // 注册首页 Feed 自动清屏 Hook - HookRuntime.install("AutoCleanModeHook") { AutoCleanModeHook.init(this, param.classLoader) } + installIfEnabled("AutoCleanModeHook") { AutoCleanModeHook.init(this, param.classLoader) } // 注册全屏沉浸播放与视口贯通 Hook - HookRuntime.install("ImmersiveFullScreenHook") { ImmersiveFullScreenHook.init(this, param.classLoader) } + installIfEnabled("ImmersiveFullScreenHook") { ImmersiveFullScreenHook.init(this, param.classLoader) } // 注册 For You 流自动滚动地区解锁 Hook - HookRuntime.install("AutoScrollHook") { AutoScrollHook.init(this, param.classLoader) } + installIfEnabled("AutoScrollHook") { AutoScrollHook.init(this, param.classLoader) } // 注册视频保存增强(下载解锁 / 无水印 / 自定义路径)Hook - HookRuntime.install("DownloadHook") { DownloadHook.init(this, param.classLoader) } + installIfEnabled("DownloadHook") { DownloadHook.init(this, param.classLoader) } // 注册音频限制解锁(音乐级限制 / 视频级静音)Hook - HookRuntime.install("MusicUnlockHook") { MusicUnlockHook.init(this, param.classLoader) } + installIfEnabled("MusicUnlockHook") { MusicUnlockHook.init(this, param.classLoader) } // 注册系统状态栏隐藏 Hook(播放页特征:视频画布可见即隐藏) - HookRuntime.install("StatusBarHook") { StatusBarHook.init(this, param.classLoader) } + installIfEnabled("StatusBarHook") { StatusBarHook.init(this, param.classLoader) } // 注册长视频播放时长 Toast 提示 Hook - HookRuntime.install("VideoDurationAlertHook") { VideoDurationAlertHook.init(this, param.classLoader) } + installIfEnabled("VideoDurationAlertHook") { VideoDurationAlertHook.init(this, param.classLoader) } + } + + /** + * 配置关闭等同于“不注册”。这比在拦截器内部再判断开关更安全,避免关闭功能仍修改宿主关键启动/框架调用链。 + */ + private fun installIfEnabled(feature: String, install: () -> Unit) { + if (featureEnabled(feature)) { + HookRuntime.install(feature, install) + } else { + HookRuntime.state(feature, "已关闭,未注册") + } } } diff --git a/app/src/main/java/com/toki/lsposed/hook/VideoDurationAlertHook.kt b/app/src/main/java/com/toki/lsposed/hook/VideoDurationAlertHook.kt index b780bf7..d88b022 100644 --- a/app/src/main/java/com/toki/lsposed/hook/VideoDurationAlertHook.kt +++ b/app/src/main/java/com/toki/lsposed/hook/VideoDurationAlertHook.kt @@ -174,7 +174,7 @@ object VideoDurationAlertHook { * Unit: 无返回值。 * * Callers: - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 宿主冷启动完成时分发。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 宿主冷启动完成时分发。 */ fun refreshConfig(context: Context) { appContext = context.applicationContext diff --git a/app/src/main/java/com/toki/lsposed/hook/VideoTranslateHook.kt b/app/src/main/java/com/toki/lsposed/hook/VideoTranslateHook.kt index 6e91fa0..b89d522 100644 --- a/app/src/main/java/com/toki/lsposed/hook/VideoTranslateHook.kt +++ b/app/src/main/java/com/toki/lsposed/hook/VideoTranslateHook.kt @@ -31,7 +31,7 @@ import java.lang.reflect.Method * * Callers: * - `com.toki.lsposed.hook.TokiModule.onPackageLoaded`: 宿主目标包加载就绪后初始化挂载。 - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 目标应用初始化时同步开关配置。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 目标应用初始化时同步开关配置。 */ object VideoTranslateHook { @@ -66,7 +66,7 @@ object VideoTranslateHook { * Unit: 无返回值。 * * Callers: - * - `com.toki.lsposed.hook.TokiModule.hookApplication`: 应用启动阶段同步配置。 + * - `com.toki.lsposed.hook.TokiModule.hookApplicationIfNeeded`: 应用启动阶段同步配置。 */ fun refreshConfig(context: Context) { isEnabled = ConfigClient.getBoolean(context, KEY_VIDEO_TRANSLATE_ENABLED) diff --git a/app/src/main/java/com/toki/lsposed/provider/ConfigClient.kt b/app/src/main/java/com/toki/lsposed/provider/ConfigClient.kt index 56a63d2..2e7f43e 100644 --- a/app/src/main/java/com/toki/lsposed/provider/ConfigClient.kt +++ b/app/src/main/java/com/toki/lsposed/provider/ConfigClient.kt @@ -37,7 +37,7 @@ object ConfigClient { */ @Synchronized fun init(context: Context) { if (initialized) return - check(!host && context.packageName == "io.github.meiyongai.toki") { "宿主必须通过框架初始化配置" } + check(!host && context.packageName == "com.toki.lsposed") { "宿主必须通过框架初始化配置" } store.attachLocal(context.getSharedPreferences(ConfigStore.LOCAL_NAME, Context.MODE_PRIVATE)) initialized = true } diff --git a/app/src/main/java/com/toki/lsposed/provider/HookDiagnostics.kt b/app/src/main/java/com/toki/lsposed/provider/HookDiagnostics.kt index 010fea9..42824a8 100644 --- a/app/src/main/java/com/toki/lsposed/provider/HookDiagnostics.kt +++ b/app/src/main/java/com/toki/lsposed/provider/HookDiagnostics.kt @@ -11,8 +11,8 @@ import kotlin.coroutines.resume /** 仅由管理端主动查询的诊断协议;不启动宿主,也不要求宿主唤起管理端服务。 */ internal object HookDiagnostics { - const val ACTION = "io.github.meiyongai.toki.REQUEST_DIAGNOSTICS" - const val PERMISSION = "io.github.meiyongai.toki.permission.READ_DIAGNOSTICS" + const val ACTION = "com.toki.lsposed.REQUEST_DIAGNOSTICS" + const val PERMISSION = "com.toki.lsposed.permission.READ_DIAGNOSTICS" val hosts = setOf("com.zhiliaoapp.musically", "com.ss.android.ugc.trill") /** diff --git a/app/src/main/java/com/toki/lsposed/provider/TikTokCrashDiagnostics.kt b/app/src/main/java/com/toki/lsposed/provider/TikTokCrashDiagnostics.kt new file mode 100644 index 0000000..11bd8e0 --- /dev/null +++ b/app/src/main/java/com/toki/lsposed/provider/TikTokCrashDiagnostics.kt @@ -0,0 +1,165 @@ +package com.toki.lsposed.provider + +import android.app.Application +import android.content.ContentValues +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import android.util.Log +import com.toki.lsposed.hook.HookRuntime +import io.github.libxposed.api.XposedInterface +import io.github.libxposed.api.XposedModule +import java.io.File +import java.io.StringWriter +import java.io.PrintWriter +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.concurrent.atomic.AtomicBoolean + +/** + * TikTok 崩溃诊断,仅挂 RuntimeInit 的最终未捕获异常处理器,不修改 TikTok 业务方法。 + * Android 10+ 通过 MediaStore 直接把完整 Java 崩溃栈写入公共 Download/Toki/Crash。 + */ +internal object TikTokCrashDiagnostics { + private const val TAG = "TokiCrashDiagnostics" + private const val GLOBAL_PACKAGE = "com.zhiliaoapp.musically" + private const val TRILL_PACKAGE = "com.ss.android.ugc.trill" + private val installedProcesses = mutableSetOf() + private val writeGuard = AtomicBoolean(false) + + /** + * 只在 TikTok 自身进程注册诊断 Hook;其它 LSPosed 作用域进程直接忽略。 + */ + @Synchronized + fun install(module: XposedModule, processName: String) { + if (!isTikTokProcess(processName) || !installedProcesses.add(processName)) return + + val method = runCatching { + Class.forName("com.android.internal.os.RuntimeInit\$KillApplicationHandler", false, null) + .getDeclaredMethod("uncaughtException", Thread::class.java, Throwable::class.java) + .apply { isAccessible = true } + }.getOrElse { error -> + HookRuntime.failure("CrashDiagnostics", error, "Runtime 崩溃处理器不可用,交由 Root 日志监听") + return + } + + runCatching { + module.hook(method) + .setExceptionMode(XposedInterface.ExceptionMode.PASSTHROUGH) + .intercept { chain -> + try { + val throwable = chain.args.getOrNull(1) as? Throwable + if (throwable != null) writeCrashReport(processName, throwable) + } catch (error: Throwable) { + // 诊断失败不能阻断宿主原始崩溃处理链。 + Log.e(TAG, "写入 TikTok 崩溃报告失败", error) + } + chain.proceed() + } + HookRuntime.state("CrashDiagnostics", "已挂载 Runtime 崩溃监听") + HookRuntime.event("TokiCrashDiagnostics", "已为 $processName 注册 Runtime 未捕获异常监听") + }.onFailure { error -> + HookRuntime.failure("CrashDiagnostics", error, "崩溃监听注册失败,交由 Root 日志监听") + } + } + + fun isTikTokProcess(processName: String): Boolean = + processName == GLOBAL_PACKAGE || processName.startsWith("$GLOBAL_PACKAGE:") || + processName == TRILL_PACKAGE || processName.startsWith("$TRILL_PACKAGE:") + + private fun currentApplication(): Application? = runCatching { + Class.forName("android.app.ActivityThread", false, null) + .getDeclaredMethod("currentApplication") + .apply { isAccessible = true } + .invoke(null) as? Application + }.getOrNull() + + private fun writeCrashReport(processName: String, throwable: Throwable) { + if (!writeGuard.compareAndSet(false, true)) return + try { + val app = currentApplication() ?: return + val now = System.currentTimeMillis() + val stamp = SimpleDateFormat("yyyyMMdd_HHmmss_SSS", Locale.US).format(Date(now)) + val fileName = "Toki_TikTok_Crash_${stamp}_${android.os.Process.myPid()}.txt" + val report = buildReport(app, processName, now, throwable) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + writeWithMediaStore(app, fileName, report) + } else { + writeLegacyDownload(fileName, report) + } + } finally { + writeGuard.set(false) + } + } + + private fun buildReport( + app: Application, + processName: String, + time: Long, + throwable: Throwable + ): String = buildString { + appendLine("Toki TikTok Crash Report") + appendLine("time=$time") + appendLine("package=${app.packageName}") + appendLine("process=$processName") + appendLine("pid=${android.os.Process.myPid()}") + appendLine("android=${Build.VERSION.RELEASE} (api ${Build.VERSION.SDK_INT})") + runCatching { + val info = app.packageManager.getPackageInfo(app.packageName, 0) + appendLine("versionName=${info.versionName}") + @Suppress("DEPRECATION") + val versionCode = if (Build.VERSION.SDK_INT >= 28) info.longVersionCode else info.versionCode.toLong() + appendLine("versionCode=$versionCode") + } + appendLine("thread=${Thread.currentThread().name}") + appendLine() + appendLine("--- uncaught exception ---") + val stack = StringWriter() + PrintWriter(stack).use { writer -> throwable.printStackTrace(writer) } + append(stack.toString()) + } + + private fun writeWithMediaStore(app: Application, fileName: String, report: String) { + val values = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) + put(MediaStore.MediaColumns.MIME_TYPE, "text/plain") + put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/Toki/Crash") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + put(MediaStore.MediaColumns.IS_PENDING, 1) + } + } + val uri = app.contentResolver.insert( + MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), + values + ) ?: error("MediaStore insert returned null") + + try { + app.contentResolver.openOutputStream(uri, "w").use { output -> + requireNotNull(output) { "无法打开崩溃报告输出流" } + output.write(report.toByteArray(Charsets.UTF_8)) + output.flush() + } + ContentValues().apply { + put(MediaStore.MediaColumns.IS_PENDING, 0) + }.also { app.contentResolver.update(uri, it, null, null) } + } catch (error: Throwable) { + runCatching { app.contentResolver.delete(uri, null, null) } + throw error + } + Log.e(TAG, "TikTok 崩溃报告已写入 Download/Toki/Crash/$fileName") + } + + @Suppress("DEPRECATION") + private fun writeLegacyDownload(fileName: String, report: String) { + val directory = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + "Toki/Crash" + ) + if (!directory.exists() && !directory.mkdirs()) { + error("无法创建 Download/Toki/Crash") + } + File(directory, fileName).writeText(report, Charsets.UTF_8) + } +} diff --git a/app/src/main/java/com/toki/lsposed/ui/LauncherIconController.kt b/app/src/main/java/com/toki/lsposed/ui/LauncherIconController.kt index e59afd5..d0746b0 100644 --- a/app/src/main/java/com/toki/lsposed/ui/LauncherIconController.kt +++ b/app/src/main/java/com/toki/lsposed/ui/LauncherIconController.kt @@ -19,7 +19,7 @@ object LauncherIconController { private const val TAG = "TokiLauncherIcon" /** 桌面启动别名组件名(与 Manifest 中 activity-alias 一致) */ - private const val ALIAS_CLASS = "io.github.meiyongai.toki.LauncherAlias" + private const val ALIAS_CLASS = "com.toki.lsposed.LauncherAlias" /** * 查询桌面图标当前是否处于隐藏状态。 @@ -76,5 +76,5 @@ object LauncherIconController { * - `com.toki.lsposed.ui.LauncherIconController.setIconHidden`: 状态切换。 */ private fun aliasComponent(): ComponentName = - ComponentName("io.github.meiyongai.toki", ALIAS_CLASS) + ComponentName("com.toki.lsposed", ALIAS_CLASS) } diff --git a/app/src/main/java/com/toki/lsposed/ui/TikTokRootLogMonitor.kt b/app/src/main/java/com/toki/lsposed/ui/TikTokRootLogMonitor.kt new file mode 100644 index 0000000..97d16e0 --- /dev/null +++ b/app/src/main/java/com/toki/lsposed/ui/TikTokRootLogMonitor.kt @@ -0,0 +1,107 @@ +package com.toki.lsposed.ui + +import android.util.Log +import java.io.IOException +import java.util.concurrent.Executors + +/** + * Root 侧独立诊断监听。 + * + * 监听 Android crash/events/system 日志中的 Java crash、native fatal signal 与 ANR 事件, + * 不向 TikTok 进程注入额外代码。Root shell 脱离 Toki 管理端后持续写入公共 Download。 + */ +internal object TikTokRootLogMonitor { + private const val TAG = "TokiRootLogMonitor" + private const val LOG_FILE = "/sdcard/Download/Toki/TikTok_Runtime.log" + private const val PID_FILE = "/sdcard/Download/Toki/.tiktok_monitor.pid" + private val executor = Executors.newSingleThreadExecutor { runnable -> + Thread(runnable, "TokiTikTokLogMonitor").apply { isDaemon = true } + } + + /** 异步尝试启动 Root 监听;无 Root 时安静失败,不影响 Toki 或 TikTok。 */ + fun ensureStarted() { + executor.execute { + runCatching { start() } + .onFailure { Log.i(TAG, "Root TikTok 诊断监听未启动:${it.message}") } + } + } + + /** 停止已有 Root 监听,供后续诊断控制使用。 */ + fun stopAsync() { + executor.execute { + runCatching { runSu(stopCommand()) } + .onFailure { Log.w(TAG, "停止 Root TikTok 诊断监听失败", it) } + } + } + + private fun start() { + runSu(startCommand()) + } + + private fun startCommand(): String { + val d = '$' + val script = """ + mkdir -p '$LOG_DIR' + if [ -f '$PID_FILE' ]; then + old_pid=${d}(cat '$PID_FILE' 2>/dev/null) + if [ -n "${d}old_pid" ] && kill -0 "${d}old_pid" 2>/dev/null; then + exit 0 + fi + rm -f '$PID_FILE' + fi + ( + printf '\n=== Toki TikTok crash/ANR monitor started %s ===\n' "${d}(date '+%Y-%m-%d %H:%M:%S')" >> '$LOG_FILE' + capture=0 + buffer='$LOG_FILE.capture.'${d}${d} + rm -f "${d}buffer" + trap 'rm -f "${d}buffer"' EXIT HUP INT TERM + logcat -v threadtime -b crash -b events -b system 2>/dev/null | while IFS= read -r line; do + case "${d}line" in + *"FATAL EXCEPTION"*|*"Fatal signal"*|*"SIGSEGV"*|*"SIGABRT"*|*"SIGBUS"*|*"am_crash"*|*"am_anr"*|*"ANR in"*|*"Application Not Responding"*|*"not responding"*) + : > "${d}buffer" + capture=80 + ;; + esac + if [ "${d}capture" -gt 0 ]; then + printf '%s\n' "${d}line" >> "${d}buffer" + capture=${d}((capture - 1)) + if [ "${d}capture" -eq 0 ]; then + if grep -Eq 'com\.zhiliaoapp\.musically|com\.ss\.android\.ugc\.trill' "${d}buffer" 2>/dev/null; then + printf '%s\n' "" >> '$LOG_FILE' + printf '=== TikTok crash/ANR trigger ===\n' >> '$LOG_FILE' + cat "${d}buffer" >> '$LOG_FILE' + fi + : > "${d}buffer" + fi + fi + done + ) & + echo ${d}! > '$PID_FILE' + """.trimIndent() + return "nohup sh -c ${shellQuote(script)} >/dev/null 2>&1 &" + } + + private const val LOG_DIR = "/sdcard/Download/Toki" + + private fun stopCommand(): String = + "if [ -f '$PID_FILE' ]; then p=\$(cat '$PID_FILE' 2>/dev/null); [ -n \"\$p\" ] && kill \"\$p\" 2>/dev/null; rm -f '$PID_FILE'; fi" + + private fun runSu(command: String) { + val process = try { + ProcessBuilder("su", "-c", command) + .redirectErrorStream(true) + .start() + } catch (error: IOException) { + throw IllegalStateException("无法启动 su", error) + } + val output = process.inputStream.bufferedReader().use { it.readText() } + val code = process.waitFor() + if (code != 0) { + throw IllegalStateException("su exit=$code output=$output") + } + } + + /** 单引号 Shell 字符串转义,脚本本身不包含任何用户输入。 */ + private fun shellQuote(value: String): String = + "'" + value.replace("'", "'\"'\"'") + "'" +} diff --git a/app/src/test/java/com/toki/lsposed/provider/TikTokCrashDiagnosticsTest.kt b/app/src/test/java/com/toki/lsposed/provider/TikTokCrashDiagnosticsTest.kt new file mode 100644 index 0000000..c5cd802 --- /dev/null +++ b/app/src/test/java/com/toki/lsposed/provider/TikTokCrashDiagnosticsTest.kt @@ -0,0 +1,21 @@ +package com.toki.lsposed.provider + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** 诊断监听只允许 TikTok 主进程及其子进程,避免把 Runtime 崩溃 Hook 扩散到其它进程。 */ +class TikTokCrashDiagnosticsTest { + @Test fun acceptsTikTokMainAndChildProcesses() { + assertTrue(TikTokCrashDiagnostics.isTikTokProcess("com.zhiliaoapp.musically")) + assertTrue(TikTokCrashDiagnostics.isTikTokProcess("com.zhiliaoapp.musically:push")) + assertTrue(TikTokCrashDiagnostics.isTikTokProcess("com.ss.android.ugc.trill")) + assertTrue(TikTokCrashDiagnostics.isTikTokProcess("com.ss.android.ugc.trill:tools")) + } + + @Test fun rejectsLookalikeOrOtherPackages() { + assertFalse(TikTokCrashDiagnostics.isTikTokProcess("com.zhiliaoapp.musically.fake")) + assertFalse(TikTokCrashDiagnostics.isTikTokProcess("com.toki.lsposed")) + assertFalse(TikTokCrashDiagnostics.isTikTokProcess("android")) + } +} diff --git a/app/src/test/java/com/toki/lsposed/util/LocalizedResourceContextTest.kt b/app/src/test/java/com/toki/lsposed/util/LocalizedResourceContextTest.kt index b298621..2039c45 100644 --- a/app/src/test/java/com/toki/lsposed/util/LocalizedResourceContextTest.kt +++ b/app/src/test/java/com/toki/lsposed/util/LocalizedResourceContextTest.kt @@ -55,7 +55,7 @@ class LocalizedResourceContextTest { for (index in 0 until nodes.length) { val element = nodes.item(index) as Element val name = element.getAttribute("name") - val resourceId = localized.resources.getIdentifier(name, "string", localized.packageName) + val resourceId = localized.resources.getIdentifier(name, "string", "com.toki.lsposed") assertTrue("Unknown resource: $name", resourceId != 0) val expected = decode(element.textContent) assertEquals("${language.languageTag}/$name", expected, localized.getString(resourceId)) diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 diff --git a/gradlew.bat b/gradlew.bat index 9d21a21..9b42019 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,94 +1,94 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega