diff --git a/.gitignore b/.gitignore index a169a39695..07a6d3b0b7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ build/ /app/foss/release /app/premium/release /captures +/.kotlin/ # Ignore Gradle GUI config gradle-app.setting @@ -62,5 +63,9 @@ google-services.json # logs *.log +# Python bytecode +__pycache__/ +*.pyc + # MacOS .DS_Store diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..4ae4b233db --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,56 @@ +# AGENTS.md — 开发向导 + +> 本文件是 ClashMetaForAndroid 的开发向导。`CLAUDE.md` 只指向这里。 + +ClashMetaX 是 [mihomo](https://github.com/MetaCubeX/mihomo) 的 Android 图形界面。本仓库是 TIIEHenry fork,在上游应用上保留应用分流策略、访问控制应用集合和 WebDAV 同步。 + +> **新会话必读顺序:** +> 1. 本文件 +> 2. [docs/DOCUMENTATION.md](docs/DOCUMENTATION.md) — 文档维护规则 +> 3. [dev/progress/status.md](dev/progress/status.md) — 当前快照 + +编码前再看相关的 [docs/systems/](docs/systems/) 或 [docs/modules/](docs/modules/)。构建、签名和内核子模块的约束见 [构建指南](docs/guides/getting-started/build.md)。 + +## 文档导航 + +| 文档 | 说明 | +|:-----|:-----| +| [文档索引](docs/INDEX.md) | 全局入口 | +| [产品需求](docs/product/INDEX.md) | 用户可观察的承诺 | +| [高层设计](DESIGN.md) | 进程与模块边界 | +| [架构概览](docs/architecture/overview.md) | 模块依赖 | +| [文档规范](docs/DOCS-SPEC.md) | 目录与归属 | +| [维护规则](docs/DOCUMENTATION.md) | frontmatter、提交前文档门禁 | +| [术语表](docs/glossary.md) | 本仓库用词 | + +## 模块 + +Gradle 模块由 `settings.gradle.kts` 决定:`app`、`core`、`service`、`design`、`common`、`hideapi`。导航在 [docs/modules/](docs/modules/)。 + +依赖方向:`app` 使用 `core`、`service`、`design`、`common`,并 `compileOnly` `hideapi`。`service` 与 `design` 使用 `core` 和 `common`。`common` `compileOnly` `hideapi`。 + +## 构建 + +```bash +git submodule update --init --recursive +./gradlew :app:assembleMetaRelease +``` + +默认 flavor 是 `alpha`。已安装到手机上的包使用 `meta` flavor,包名 `com.github.metacubex.clash.meta`。JDK 目标是 21。没有 `signing.properties` 时 release 使用 debug 签名。细节与内核 pin 见 [构建指南](docs/guides/getting-started/build.md)。 + +## 开发流程 + +1. 行为或契约变更先写进 `dev/plans/` 或对应 `docs/systems/`,需要取舍时写 ADR。 +2. 编码。 +3. 提交前按 [规则 3a](docs/DOCUMENTATION.md#规则-3a提交前文档门禁) 更新行动层;用户可见行为再更新知识层。 +4. 只 `git add` 本次主题的文件。工作区里其他未提交改动留在原地。 + +单行且无行为变化的修改可以只提交代码。 + +## 约束 + +- 过时文档移入 `dev/archive/`,不删除。 +- `status.md` 保持 200 行以内的当前快照。溢出后注用 `bash scripts/archive-status-overflow.sh` 滚入归档。 +- 结构变更后运行 `python3 scripts/check-docs-health.py`。 +- 不要把设备序列号、签名指纹、密钥口令写进仓库文档。WebDAV 默认常量以 `service` 模块源码为准,文档只链接,不另抄一套。 +- 根 `README.md` 保持上游英文介绍。Fork 的架构与构建说明写在 `docs/`。 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..045bb6fc7e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +# CLAUDE.md + +本文件是指针。开发向导以 [AGENTS.md](AGENTS.md) 为准。 + +文档维护规则见 [docs/DOCUMENTATION.md](docs/DOCUMENTATION.md)。当前快照见 [dev/progress/status.md](dev/progress/status.md)。 diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000000..ba85bf54ab --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,41 @@ +--- +title: "ClashMetaX 高层设计" +type: architecture +status: accepted +phase: N/A +updated: 2026-10-15 +summary: "UI 进程与 :background 进程的边界,以及六个 Gradle 模块的职责" +--- + +# 高层设计 + +本应用把 mihomo 跑在 Android VPN 服务里,并用一套配置界面管理配置文件、代理选择和按应用分流。 + +## 进程 + +界面跑在默认进程。`ClashService`、`TunService`、`RemoteService`、配置处理与状态 Provider 声明在 `:background`。界面通过 `service` 模块的远程接口读写配置和隧道状态,不在 UI 进程里加载 `libclash.so` 的业务循环。 + +`TunService` 持有 `android.net.VpnService`。流量是否进入隧道,由当前配置、访问控制名单,以及可选的应用分流策略决定。 + +## 模块 + +| 模块 | 职责 | +|:-----|:-----| +| app | Activity、外部控制入口、日志界面 | +| design | 界面布局、列表与设置控件 | +| service | `:background` 服务、配置库、WebDAV、访问控制与分流策略的存储 | +| core | JNI 与 Go 桥,链到 mihomo,产出 `libclash.so` | +| common | 跨模块常量、Store、兼容工具 | +| hideapi | 隐藏 API 的编译期存根 | + +模块依赖与阅读入口见 [架构概览](docs/architecture/overview.md)。 + +## 内核 + +mihomo 以 Git 子模块放在 `core/src/foss/golang/clash`。`core/src/main/golang` 用 `replace` 指向它,再由 golang-android 插件编进 native 库。子模块的检出约束见 [构建指南](docs/guides/getting-started/build.md)。 + +## Fork 行为 + +相对上游界面,本 fork 增加三类用户可见行为:命名的应用分流策略、访问控制里的系统/国内应用集合、配置与策略的 WebDAV 同步。契约分别见 [访问控制](docs/systems/access-control/INDEX.md) 与 [WebDAV](docs/systems/webdav/INDEX.md)。 + +产品承诺见 [docs/product/](docs/product/INDEX.md)。这里不写验收步骤。 diff --git a/NOTICE b/NOTICE index 6853d8820c..a98e7206a2 100644 --- a/NOTICE +++ b/NOTICE @@ -1,5 +1,14 @@ 3th-party software licenses + * AgenticUILibrary +========================================================================== +design/src/main/java/nota/ui and design/src/main/java/universe/agentic/ui/search +contain source copied from AgenticUILibrary under the Apache License 2.0. +Adapted for this AndroidX Compose module: Material 3 theme calls were +removed, dark mode follows UiStore through LocalShellIosDark, and the +switch thumb is a round rect because the iOS spec is a circle. +The upstream tree did not ship a NOTICE file. + * Clash ========================================================================== GNU GENERAL PUBLIC LICENSE diff --git a/README.md b/README.md index 03757eca76..a495b949cb 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ A Graphical user interface of [Clash.Meta](https://github.com/MetaCubeX/Clash.Meta) for Android +### Documentation + +Architecture, build constraints, and product behavior for this fork are in the documentation system. Start at [docs/INDEX.md](docs/INDEX.md). The agent guide is [AGENTS.md](AGENTS.md). + ### Feature Feature of [Clash.Meta](https://github.com/MetaCubeX/Clash.Meta) diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000000..d612b2d6c1 --- /dev/null +++ b/app/README.md @@ -0,0 +1,16 @@ +--- +title: "app 模块" +type: overview +status: accepted +phase: N/A +updated: 2026-10-15 +summary: "Android 应用模块:Activity、外部控制和日志" +--- + +# app + +应用进程里的界面入口。屏幕布局在 `design`,隧道和配置存储在 `service`。 + +导航:[docs/modules/app/INDEX.md](../docs/modules/app/INDEX.md) + +模块设计:[app/docs/INDEX.md](docs/INDEX.md) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 27a8f5b26c..e2667a1553 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -46,8 +46,11 @@ task("downloadGeoFiles") { doLast { geoFilesUrls.forEach { (downloadUrl, outputFileName) -> - val url = URL(downloadUrl) val outputPath = file("$geoFilesDownloadDir/$outputFileName") + if (outputPath.exists()) { + return@forEach + } + val url = URL(downloadUrl) outputPath.parentFile.mkdirs() url.openStream().use { input -> Files.copy(input, outputPath.toPath(), StandardCopyOption.REPLACE_EXISTING) diff --git a/app/docs/INDEX.md b/app/docs/INDEX.md new file mode 100644 index 0000000000..ba63b70f65 --- /dev/null +++ b/app/docs/INDEX.md @@ -0,0 +1,31 @@ +--- +title: "app 模块设计" +type: index +status: accepted +phase: N/A +updated: 2026-10-15 +summary: "应用进程的 Activity、对 :background 的调用,以及日志捕获" +--- + +# app 模块设计 + +`app` 是默认进程里的界面入口。屏幕在 `design`,隧道、配置库和 WebDAV 实现在 `service` 的 `:background` 进程。本目录只写这个模块自己怎么把两边接起来。 + +| 文档 | 写什么 | +|:-----|:-------| +| [Activity](activities.md) | `BaseActivity` 如何把界面交给 `design`,各 Activity 的职责,外部控制与主界面的差别 | +| [远程调用](remote.md) | 应用进程怎样绑定 `:background`,以及状态、文件、偏好和启停各走哪条路 | +| [日志捕获](logcat.md) | `LogcatService` 到 `LogcatReader` 的实际链路,以及崩溃页用的系统 logcat | + +模块导航在 [docs/modules/app/INDEX.md](../../docs/modules/app/INDEX.md)。 + +跨模块行为以这些文档为准,这里不重写: + +| 主题 | 文档 | +|:-----|:-----| +| 界面分层 | [界面](../../docs/systems/ui/INDEX.md) | +| 隧道与外部控制动作 | [运行时](../../docs/systems/runtime/INDEX.md) | +| 配置 | [配置](../../docs/systems/profiles/INDEX.md) | +| 访问控制与分流策略 | [访问控制](../../docs/systems/access-control/INDEX.md) | +| WebDAV | [WebDAV](../../docs/systems/webdav/INDEX.md) | +| 用户可观察的承诺 | [产品需求](../../docs/product/requirements/INDEX.md) | diff --git a/app/docs/activities.md b/app/docs/activities.md new file mode 100644 index 0000000000..8908ec2b8e --- /dev/null +++ b/app/docs/activities.md @@ -0,0 +1,138 @@ +--- +title: "app 的 Activity" +type: architecture +status: accepted +phase: N/A +updated: 2026-10-19 +summary: "BaseActivity 把 Design 设为内容视图。从策略进入的名单标题是策略名称" +--- + +# app 的 Activity + +Activity 在应用进程。布局、列表和设置控件在 `design` 的 `*Design`。对 `:background` 的调用方式见 [远程调用](remote.md)。分层说明见 [界面](../../docs/systems/ui/INDEX.md)。 + +## BaseActivity + +`BaseActivity>` 继承 `AppCompatActivity`,协程作用域是 `MainScope()`,并实现 `Broadcasts.Observer`。 + +`onCreate` 先按 `UiStore.darkMode` 套用 `AppThemeLight` 或 `AppThemeDark`,并把状态栏、导航栏设成当前主题的颜色。接着把 `uiStore.hideFromRecents` 写到本应用每一个 `appTask`。然后 `launch { main() }`。子类实现挂起函数 `main()`。 + +把界面交给 `design` 的顺序: + +1. 子类构造对应的 `*Design`。`Design` 持有 `root` 视图,以及一个无限容量的 `requests` 通道。 +2. `setContentDesign` 在 `window.decorView` 上投递一帧之后才赋值。 +3. `design` 非空时 `setContentView(design.root)`;置空时放进一个空 `View`。 +4. 子类在 `select` 里收取 `design.requests`,自己去调远程接口或打开下一个 Activity,再调用 Design 上的方法把状态写回去。 +5. `onDestroy` 调用 `design.cancel()`,并取消 Activity 协程。 + +另一路输入是 `events`。`onStart` 把 Activity 登记为 `Remote.broadcasts` 的观察者,并发出 `ActivityStart`。`onStop` 移除观察者,并发出 `ActivityStop`。服务重建、隧道起停、配置加载、配置变更、配置更新成功或失败,都转成 `Event`。隧道停止且带了原因、同时 Activity 处于 started 时,用这个原因构造 `ClashException`,在当前 Design 上弹 toast。 + +`clashRunning` 读的是 `Remote.broadcasts.clashRunning`,不是当场查询状态 Provider。 + +`finish()` 先跑一次 `defer` 挂起块,再在不可取消的上下文里调用 `super.finish()`。重复进入 `finish()` 时,如果 `defer` 还在跑,直接返回。日夜模式与当前配置不一致时,`ApplicationObserver.createdActivities` 里已创建的 Activity 全部 `recreate()`。`onSupportNavigateUp` 转到 `onBackPressed()`。 + +`startActivityForResult` 用 `util/Activity.kt` 的 `ActivityResultLifecycle` 登记一次性合约,把结果恢复成挂起函数的返回值。 + +## 主界面 + +`MainActivity` 使用 `MainDesign`。`launchMode` 是 `singleTask`。清单里它响应 `MAIN`、`APPLICATION_PREFERENCES` 和 `QS_TILE_PREFERENCES`,但启动器图标挂在 `MainActivityAlias` 上,别名再指向它。 + +事件 `ActivityStart`、服务重建、隧道起停、配置加载或变更时调用 `fetch()`:写入 `clashRunning`、会话覆盖里的模式(没有则用隧道状态里的模式)、是否存在 provider、当前节点、当前配置名,以及 `ServiceStore` 里生效分流策略的名字。隧道在跑时,`fetch()` 会立刻取一次流量,之后每秒再取。流量来自 `queryTrafficNow()` 和 `queryTrafficTotal()`。 + +当前节点只在隧道运行时查询。模式是 `Direct` 则不查。否则 `queryProxyGroupNames(true)`:`Global` 取名称忽略大小写等于 `GLOBAL` 的组,没有则取第一个组;其他模式取第一个名称不是 `GLOBAL` 的组,没有也退回第一个组。再以 `ProxySort.Default` 读该组的 `now`,空白则不显示。 + +`MainDesign.Request` 的去向: + +| 请求 | 行为 | +|:-----|:-----| +| `ToggleStatus` | 在跑则 `stopClashService()`,否则走下面的启动 | +| `OpenProxy` | 打开 `ProxyActivity`。组名非空时写入 `EXTRA_PROXY_GROUP`。这不是导出的 `clash://` 契约 | +| `OpenProfiles` / `OpenProviders` / `OpenAppsStrategy` / `OpenSettings` / `OpenHelp` | 打开对应 Activity | +| `OpenLogs` | `LogcatService.running` 为真时打开 `LogcatActivity`,否则打开 `LogsActivity` | +| `OpenAbout` | 显示包的 `versionName`,以及 `Bridge.nativeCoreVersion()` | +| `SetMode` | 先提示模式切换,再把 `Clash.OverrideSlot.Session` 的 `mode` 写回去并 `fetch()` | + +启动前先 `withProfile { queryActive() }`。没有当前配置,或当前配置尚未导入时,toast 提示并给出打开 `ProfilesActivity` 的动作,不启动服务。否则调用 `startClashService()`。返回的 VPN 授权 Intent 非空时,用 `StartActivityForResult` 拉起系统授权;`RESULT_OK` 后再调一次 `startClashService()`。这一步抛异常则 toast 无法启动 VPN。 + +图标未隐藏时,`onCreate` 注册三个动态快捷方式:切换、启动、停止。Intent 的类名是 `ExternalControlActivity`,动作是 `Intents` 里的对应常量,并带 `NEW_TASK`、`EXCLUDE_FROM_RECENTS`、`NO_ANIMATION`。Android 13 及以上若还没有通知权限,会请求 `POST_NOTIFICATIONS`;授权结果的回调是空的。 + +## 外部控制与主界面 + +`ExternalControlActivity` 不继承 `BaseActivity`,不创建 Design。清单把它标成导出、透明、`noHistory`、`excludeFromRecents`。它是给其他应用、链接和快捷方式用的入口,不是主界面上的一屏。用户能观察到的承诺见 [PRD-006](../../docs/product/requirements/PRD-006.md)。动作字符串与 meta 包名的对应关系见 [运行时](../../docs/systems/runtime/INDEX.md)。 + +`onCreate` 去掉过渡动画。除了 `ACTION_VIEW` 会先返回、在协程里收尾,其余分支处理完就 `finish()`。 + +`ACTION_VIEW` 要求链接上有 `url`。`type` 为 `file` 时创建 `Profile.Type.File`,为 `url` 或其他值时创建 `Profile.Type.Url`。名称缺省是 `R.string.new_profile`。`update-interval` 按分钟解析:大于 0 时至少 15 分钟,否则为 0,再换成毫秒。`withProfile` 里 `create` 再 `patch`,然后打开带这个 UUID 的 `PropertiesActivity` 并 `finish()`。导入之后的配置行为见 [配置](../../docs/systems/profiles/INDEX.md)。 + +启停不打开主界面,也不走 `MainActivity.startClash()`: + +| 动作 | 行为 | +|:-----|:-----| +| `ACTION_TOGGLE_CLASH` | `StatusClient.currentProfile()` 非空则停止,否则启动 | +| `ACTION_START_CLASH` | 已在跑只 toast;否则启动 | +| `ACTION_STOP_CLASH` | 停止并 toast | + +这里的「在跑」是状态 Provider 返回了当前配置名,不是 `Remote.broadcasts.clashRunning`。启动前先 `withProfile { queryActive() }`。没有当前配置,或当前配置尚未导入时,toast `R.string.no_profile_selected`,不调用 `startClashService()`,也不 toast 已启动。`onCreate` 在查询返回前就 `finish()`,所以这次查询和 toast 用 `applicationContext`,协程不在 `onDestroy` 里取消。已有导入配置时调用 `startClashService()`。若返回 VPN 授权 Intent,只 toast 无法启动 VPN,不拉起授权界面。主界面的开关会拉起授权。两边最后都落到 `util/Clash.kt` 的 `startClashService` / `stopClashService`。 + +`TileService` 是另一条控制路径,也不经过 `ExternalControlActivity` 或 `MainDesign`。点击时磁贴为 inactive 就 `startClashService()`,为 active 就 `stopClashService()`,返回的授权 Intent 被忽略。开始监听时用 `StatusClient` 取当前配置名,并注册隧道起停、配置加载和服务重建广播;磁贴标签是配置名,空则用启动名称。 + +`RestartReceiver` 在清单里导出,但 `android:enabled` 默认是 `false`。`AppSettingsActivity.autoRestart` 用 `PackageManager.setComponentEnabledSetting` 开关这个组件。收到 `BOOT_COMPLETED` 或 `MY_PACKAGE_REPLACED` 时,若 `StatusProvider.shouldStartClashOnBoot` 为真,调用 `startClashService()`。 + +`DialerReceiver` 导出,匹配 `android_secret_code` 主机 `252746382`,收到后以 `NEW_TASK` 打开 `MainActivity`。 + +## 配置、代理与文件 + +`ProfilesActivity` 用 `ProfilesDesign`。进入前台或配置变更时 `withProfile { queryAll() }` 刷新列表。请求包括新建、更新单个、更新全部已导入且类型不是 `File` 的配置、删除、编辑、复制和设为当前。未导入的配置不能直接设为当前,改为让 Design 要求保存。更新完成或失败时按 UUID 查出名称再 toast;失败 toast 可以打开对应的 `PropertiesActivity`。前台时每分钟让 Design 更新经过时间。 + +`NewProfileActivity` 先列出文件、URL、二维码,再加上能响应 `Intents.ACTION_PROVIDE_URL` 的外部 Activity。文件和 URL 用 `withProfile { create }` 后打开 `PropertiesActivity`,对方 `RESULT_OK` 时本页结束。二维码走 `ScanQRCode`,成功则按 URL 类型创建并把扫到的字符串当作 source。外部提供者用 `startActivityForResult` 取回 `data` URI 和可选名称,类型为 `Profile.Type.External`。 + +`PropertiesActivity` 从 Intent 取 UUID,`queryByUUID` 失败就结束。离开时 `defer` 调用 `release`。`ActivityStop` 且用户没有取消、字段相对进入时有变化,则 `patch` 名称、来源、间隔和 `ageSecretKey`。提交时再校验名称和非文件类型的来源,然后 `patch` 加 `commit`。浏览文件打开带同一 UUID 的 `FilesActivity`。服务重建则结束本页。返回键在有未保存修改时先问是否放弃。 + +`FilesActivity` 用 `FilesClient` 列 `:background` 里文件 Provider 的文档。根文档 id 是配置 UUID。`configurationEditable` 是 `profile.type != Url`,只决定根目录上已有文件的导入。新建只在子目录出现,根上不发 `ImportFile(null)`。从子目录返回且栈变空时,立刻把界面标成根目录,新建随之消失,然后才 `fetch`。栈空时收到 `ImportFile(null)` 直接返回,不调用 `stack.last()`。栈顶目录用 `list`;根目录若存在空的 `config.yaml`,列表只保留这一项。请求包括进入目录、查看、删除、重命名、导入和导出。返回键发送 `PopStack`,栈空才结束。 + +`ProxyActivity` 读取代理组名(是否排除不可选由 `uiStore.proxyExcludeNotSelectable` 决定)。组名列表为空时不发送 `Reload`。否则初始下标依次看旋转保存的组、`EXTRA_PROXY_GROUP`、`uiStore.proxyLastGroup`,再退回 0。只有用户在组列表里点选才写 `proxyLastGroup`,并改写 Intent 里的 extra。重新打开时带上当时显示的组名,这一步不写 `proxyLastGroup`。每个组有重载序号和点选序号,都放在 Activity 里。收到该组的 `Reload` 时重载序号加一;查询返回时序号已变,就丢掉整段结果,不调用 `updateGroup`,也不改 `now`。点选序号只在 `Select` 时加一。查询返回时重载序号仍有效、但点选序号已变,则列表照常画上,`now` 保留这次点选。`Select` 仍调用 `patchSelector`,把本地 `now` 设成点中的名字,再 `requestRedrawVisible`。重载用许可数为 10 的信号量调用 `queryProxyGroup`。延迟测试调用 `healthCheck` 后直接开始该组的下一次查询,不经过通道再发一条 `Reload`。只有这次查询真正画上才能清掉测速进度。被丢掉的旧结果不清进度,也不消费这张条子。打开、排序和切组的重载保持进度。配置加载后组名变了,就重新打开本 Activity 并结束当前实例。界面不再发送 `PatchMode` 或 `ReloadAll`。 + +`ProvidersActivity` 用 `queryProviders()` 的排序结果建 `ProvidersDesign`。更新请求调用 `updateProvider`。配置加载后列表变了,同样重新打开自身。前台时每分钟更新经过时间。 + +## 设置 + +`SettingsActivity` 只做转发:应用、网络、覆盖、Meta 功能、WebDAV。 + +`AppSettingsActivity` 把 `UiStore`、`ServiceStore`、当前是否在跑,以及隐藏图标的回调交给 `AppSettingsDesign`。隧道起停或服务重建时 `recreate()`。Design 发出请求时,已创建的 Activity 全部 `recreate()`。隐藏图标切换 `MainActivityAlias` 的组件启用状态。开机重启即上一节的 `RestartReceiver`。 + +`NetworkSettingsActivity` 打开不带 UUID 的 `AccessControlActivity`(全局名单)。访问控制项变化时记下自动同步,并在满足条件时 `withProfile { webdavPerform(ACTION_SYNC, ...) }`。条件与名单语义见 [访问控制](../../docs/systems/access-control/INDEX.md) 和 [WebDAV](../../docs/systems/webdav/INDEX.md)。 + +`OverrideSettingsActivity` 读出 `OverrideSlot.Persist`,`defer` 在离开时 `patchOverride` 写回。确认重置后,把 `defer` 换成 `clearOverride` 再结束。 + +`MetaFeatureSettingsActivity` 同样读写 `OverrideSlot.Persist`。另外可以用 `GetContent` 导入地理文件。扩展名必须是 `.metadb`、`.db`、`.dat` 或 `.mmdb`,否则对话框拒绝。接受的文件按类型写成 `clashDir` 下的 `geoip`、`geosite`、`country` 或 `ASN` 再加原扩展名。`clashDir` 是 `filesDir/clash`。 + +`WebdavSettingsActivity` 用 `WebdavSettingsStore` 和 `WebdavLocalState.lastMessage()` 建界面。测试、同步、拉取、推送分别对应 `WebdavSync` 的四个动作,经 `withProfile { webdavPerform }` 执行。进行中再次点击会被 `busy` 丢掉。同步或测试进行中拨开自动同步时,开关和 `store.autoSync` 都保持关。不忙时从关到开仍写上并发送 `Sync`,不确认。端点、合并和默认常量见 [WebDAV](../../docs/systems/webdav/INDEX.md),本文不抄那些字面量。 + +`AppsStrategyActivity` 直接读写 `ServiceStore` 的策略列表和生效 UUID:新建、改名、改模式、删除、设为生效。点选一份策略会打开带该 UUID 的 `AccessControlActivity`。变更后按 [WebDAV](../../docs/systems/webdav/INDEX.md) 的自动同步条件调用 `webdavPerform(ACTION_SYNC)`。保存位置和谁在驱动隧道见 [访问控制](../../docs/systems/access-control/INDEX.md)。 + +`AccessControlActivity` 的返回键直接 `finish()`。`defer` 把当前选中包名写入策略或全局名单,再按该文档所述决定是否在返回之后重载隧道和同步。从策略进入且名称非空时,窗口标题设成该策略的名称。列表加载调用 `loadAccessControlApps`,国内包名来自 `DomesticAppCatalog`。点选规则不在这里重复。 + +## 日志、帮助与异常页 + +`LogsActivity` 和 `LogcatActivity` 的捕获、读文件和导出见 [日志捕获](logcat.md)。 + +`HelpActivity` 把 Design 给出的 URI 用 `ACTION_VIEW` 打开。它只消费 `events`,不处理 Design 请求。 + +`ApkBrokenActivity` 由 `Remote.verifyApp` 在安装包里找不到匹配 ABI 的 `libclash.so` 时拉起。Design 请求里的 URL 用 `ACTION_VIEW` 打开。 + +`AppCrashedActivity` 是 `singleTask`。`Remote` 在 `RemoteService` 短时间连续断开或绑定失败时,先结束已创建的 Activity,再以 `NEW_TASK` 打开它。页面用 `SystemLogcat.dumpCrash()` 填日志。这是系统 `logcat` 命令,不是 `LogcatService` 写的文件。 + +## 清单里的导出入口 + +不逐条列权限。导出的组件是: + +| 组件 | 导出时做什么 | +|:-----|:-------------| +| `MainActivity` | 应用内主界面。另有 `MAIN`,但没有启动器 category | +| `MainActivityAlias` | `LAUNCHER` 与 `LEANBACK_LAUNCHER`,目标是 `MainActivity` | +| `ExternalControlActivity` | `VIEW`(`clash` / `clashmeta` 的 `install-config`)以及清单里写死的启动、停止、切换动作 | +| `TileService` | 快捷设置磁贴,要求 `BIND_QUICK_SETTINGS_TILE` | +| `RestartReceiver` | 开机与覆盖安装;默认禁用 | +| `DialerReceiver` | 秘密码,打开主界面 | + +其余 Activity 和 `LogcatService` 的 `exported` 都是 `false`。`LogcatService` 的前台类型是 `specialUse`。 diff --git a/app/docs/logcat.md b/app/docs/logcat.md new file mode 100644 index 0000000000..efccfe2b2b --- /dev/null +++ b/app/docs/logcat.md @@ -0,0 +1,69 @@ +--- +title: "日志捕获" +type: architecture +status: accepted +phase: N/A +updated: 2026-10-15 +summary: "LogcatService 在应用进程订阅 :background 的日志并写文件;LogcatReader 再读回来" +--- + +# 日志捕获 + +Clash 运行日志和崩溃页上的系统 logcat 是两条链路。前者由 `LogcatService` 订阅 `:background` 里的 `IClashManager.setLogObserver`,写成 `cacheDir/logs` 下的文件。后者是 `AppCrashedActivity` 执行一次 `logcat -d`。 + +## 谁打开哪一屏 + +`MainActivity` 收到 `OpenLogs` 时:`LogcatService.running` 为真就打开 `LogcatActivity`,否则打开 `LogsActivity`。 + +`LogsActivity` 在 `ActivityStart` 时列出 `cacheDir/logs` 里能被 `LogFile.parseFromFileName` 认出的文件,格式是 `clash-<毫秒>.log`。删除全部会 `logsDir.deleteRecursively()`,`logsDir` 就是这个目录。打开某个文件时给 `LogcatActivity` 带上文件名。开始捕获则打开不带文件名的 `LogcatActivity` 并结束自己。 + +`LogcatActivity.main` 看 Intent 里有没有文件名。有则读本地文件;没有则走实时捕获。文件名解析失败,或读取抛异常,toast 无效日志文件,并且不再 `setContentDesign`。 + +## 实时捕获 + +`mainStreaming` 用 `LogcatDesign(streaming = true)`,然后 `startForegroundService(LogcatService)`,再 `bindService`。绑定返回的 Binder 在 `queryLocalInterface` 里忽略 descriptor,直接交回 `LogcatService` 实例,所以这个绑定只在同一进程里有用。`LogcatService` 没有单独的 `android:process`,和 Activity 同在默认进程。 + +Activity 处于 started 时,每 500 毫秒调用 `snapshot`。第一次要全量;之后 `snapshot` 在没有增删时返回 null,界面不刷新。`Close` 时 `stopService`,再打开 `LogsActivity` 并结束自己。`onDestroy` 解除这次绑定。 + +`LogcatService.onCreate` 把 `running` 设为 true,创建通知渠道 `clash_logcat_channel`,以前台服务类型 `specialUse` 显示通知,通知点击打开 `LogcatActivity`。然后它自己 `bindService(RemoteService)`,`BIND_AUTO_CREATE`。这是对 `:background` 的另一次绑定,不经过 `Remote.service`,因此界面全部不可见、`Remote` 解除自己的绑定时,正在跑的日志服务仍可以连着 `RemoteService`。 + +`onServiceConnected` 后: + +1. Binder 已死则 `stopSelf`。 +2. `unwrap` 成 `IRemoteService`,取 `clash()`。 +3. `logsDir.mkdirs()`,用 `LogcatWriter` 打开一个新文件。`LogFile.generate()` 的文件名是当前时间毫秒。 +4. `setLogObserver`。观察者的 `newItem` 对容量 128 的通道 `trySend`。通道满时 `trySend` 不等待。 +5. 循环 `receive`。每一条先 `LogcatWriter.appendMessage`,再 `LogcatCache.append`。 + +`:background` 侧 `ClashManager.setLogObserver` 在观察者非空时调用 `Clash.subscribeLogcat()`,再把收到的 `LogMessage` 交给 `observer.newItem`。`subscribeLogcat` 使用容量 32 的通道,原生回调里是 `trySend`。方向是:应用进程注册观察者,`:background` 再把条目回调进来。观察者换成 null,或注册一个新观察者时,`ClashManager` 取消上一段订阅并 `Clash.forceGc()`。 + +写文件或读取循环失败时,若 Binder 仍活着就 `setLogObserver(null)`,然后 `stopSelf`。`RemoteService` 断开也会 `stopSelf`。`onDestroy` 取消服务协程、解除到 `RemoteService` 的绑定、停止前台通知,并把 `running` 设为 false。 + +`LogcatCache` 是容量 128 的环形缓冲。放满时丢掉最旧的一条,并累计 `removed`。`snapshot(full)` 在非全量且 `removed` 和 `appended` 都是 0 时返回 null;否则交出当前列表和这两个计数,然后把计数清零。全量时,返回的 `appended` 是当前条数再加上未清零的 `appended`。 + +## 文件格式与 LogcatReader + +`LogcatWriter` 每一行是: + +```text +:: +``` + +`time.time` 是毫秒。级别名来自 `LogMessage.Level.name`。 + +`LogcatReader` 只读这种文件。它跳过空白修剪后以 `#` 开头的行,再按 `:` 分成至多三段: + +- 第一段能解析成 Long:时间用这个毫秒,级别用 `Level.valueOf`,消息是第三段。 +- 第一段不是 Long:时间沿用上一条,级别用 `Warning`,消息是整行重新用 `:` 拼回去。 + +`LogcatActivity` 读完后用 `LogcatDesign(streaming = false)`,一次 `patchMessages` 整表。删除会删掉该文件并 `finish()`。导出用 `CreateDocument("text/plain")`,由 `LogcatFilter` 写成另一种文本:第一行是 `# Capture on ...`,之后每行是对齐过的时间、级别名和消息。导出文件不是 `LogcatWriter` 的格式;`LogcatReader` 不会被用来读这个导出结果。 + +## 系统 logcat + +`SystemLogcat.dumpCrash` 执行: + +```text +logcat -d -s Go DEBUG AndroidRuntime ClashMetaForAndroid LwIP +``` + +它读完标准输出,丢掉以 `------` 开头的行。失败时返回空字符串。只有 `AppCrashedActivity` 调用它,用来填充崩溃页,不写 `logsDir`,也不经过 `LogcatService`。 diff --git a/app/docs/remote.md b/app/docs/remote.md new file mode 100644 index 0000000000..07342e1510 --- /dev/null +++ b/app/docs/remote.md @@ -0,0 +1,97 @@ +--- +title: "app 怎样调用 :background" +type: architecture +status: accepted +phase: N/A +updated: 2026-10-15 +summary: "界面进程绑定 RemoteService;启停、状态、文件和偏好各自另有一条路" +--- + +# app 怎样调用 :background + +`:background` 里有哪些服务见 [运行时](../../docs/systems/runtime/INDEX.md) 和 [架构概览](../../docs/architecture/overview.md)。本文只写 `app` 源码里的调用方向。 + +`RemoteService`、`ClashService`、`TunService`、`FilesProvider`、`StatusProvider`、`PreferenceProvider` 的 `android:process` 都是 `:background`。`app` 清单没有给 Activity、`LogcatService`、`TileService` 指定进程,它们在默认应用进程。 + +## 进程入口 + +`MainApplication.attachBaseContext` 调用 `Global.init`。`onCreate` 在区分进程之前先把地理文件解到 `filesDir/clash`:`geoip.metadb`、`geosite.dat`、`ASN.mmdb`、`BundleMRS.7z`。文件不存在,或修改时间早于本包 `lastUpdateTime` 时,从 assets 重新拷贝。 + +随后: + +- `currentProcessName == packageName`(界面进程)时调用 `Remote.launch()`。 +- 其他进程(包括 `:background`)调用 `sendServiceRecreated()`,不绑定远程服务。`Broadcasts` 收到该广播后把 `clashRunning` 置为 false,并通知观察者。接收器还没登记时,这次广播不会被 `Broadcasts` 处理。 + +## 可见时才绑定 RemoteService + +`Remote.launch()` 只做两件事:挂上 `ApplicationObserver`,并在 IO 线程跑 `verifyApp`。 + +`ApplicationObserver` 在任一 Activity `onStart` 后视为可见,全部 Activity 停止或销毁后视为不可见。可见时 `service.bind()` 且 `broadcasts.register()`;不可见时 `service.unbind()` 且 `broadcasts.unregister()`。因此界面进程持有的 `IRemoteService` 连接跟「有没有 Activity 处于 started」走,不跟隧道是否在跑走。`LogcatService` 自己另绑一次,不受这次 unbind 影响,见 [日志捕获](logcat.md)。 + +`remote/Service.kt` 的 `bind()` 对 `RemoteService` 使用 `BIND_AUTO_CREATE`。`onServiceConnected` 把 `IBinder` `unwrap` 成 `IRemoteService`,放进 `Resource`。`onServiceDisconnected` 把 `Resource` 置空。若距离上次断开不到 10 秒,则 unbind,并执行 `Remote` 传入的崩溃回调:结束已创建的 Activity,再启动 `AppCrashedActivity`。`bindService` 抛异常时同样走这个回调。 + +`Resource.get()` 在值为空时挂起,`set` 非空后唤醒等待者。`reset` 只在当前值与传入实例是同一引用时清空。 + +`verifyApp` 比较 `AppStore.updatedAt` 和包的 `lastUpdateTime`。不一致时用 `verifyApk()` 在 apk 与 split 里找 `lib//libclash.so`,ABI 必须和 `Build.SUPPORTED_ABIS` 有交集。失败则结束已创建 Activity 并打开 `ApkBrokenActivity`。成功则把 `updatedAt` 写成这次的 `lastUpdateTime`。 + +## Binder:IRemoteService + +方向是界面进程调用 `:background`,不是反过来。 + +``` +界面进程 + bindService(RemoteService) + → IRemoteService.clash() → IClashManager(ClashManager) + → IRemoteService.profile() → IProfileManager(ProfileManager) +``` + +`util/Remote.kt` 是这条 Binder 的唯一包装: + +- `withClash` 取 `Remote.service.remote`,再 `clash()`,在 `Dispatchers.IO` 上执行块。 +- `withProfile` 同样取连接,再 `profile()`。 +- 只捕获 `DeadObjectException`:打日志、`reset` 这次拿到的实例,然后无限重试。其他异常原样抛出。 + +`IClashManager` 上界面会用到的方法:`queryTunnelState`、`queryTrafficNow`、`queryTrafficTotal`、`queryProxyGroupNames`、`queryProxyGroup`、`queryProviders`、`patchSelector`、`healthCheck`、`updateProvider`、`queryOverride`、`patchOverride`、`clearOverride`、`setLogObserver`。`queryConfiguration` 在接口上,当前 `app` 源码没有调用。 + +`IProfileManager` 上的方法:`create`、`clone`、`commit`、`release`、`delete`、`patch`、`update`、`queryByUUID`、`queryAll`、`queryActive`、`setActive`、`webdavPerform`。配置和同步的语义见 [配置](../../docs/systems/profiles/INDEX.md) 与 [WebDAV](../../docs/systems/webdav/INDEX.md)。 + +调用方:主界面、代理、提供者、覆盖设置、Meta 功能设置走 `withClash`。主界面、外部导入、配置列表、配置属性、新建配置、网络设置、分流策略、WebDAV 设置、访问控制离开后的同步走 `withProfile`。`LogcatService` 不走 `withClash`,它自己 `unwrap` 之后调用 `clash().setLogObserver`。 + +## 不经过 IRemoteService 的三条路 + +这些仍然从默认进程进 `:background`,但不是 `RemoteService` 的 Binder。 + +**状态。** `StatusClient` 对 `Authorities.STATUS_PROVIDER`(`:background` 的 `StatusProvider`)做 `contentResolver.call`,方法是 `currentProfile`。返回 Bundle 里的 `name`;服务没在跑或调用失败时得到 null。`ExternalControlActivity`、`TileService` 和 `Broadcasts.register` 用「名字非空」表示隧道在跑。 + +**文件。** `FilesClient` 用 `DocumentsContract` 访问 `Authorities.FILES_PROVIDER`(`:background` 的 `FilesProvider`):列子文档、重命名、删除、以及经 `copyContentTo` 导入或导出。只有 `FilesActivity` 使用它。 + +**偏好。** `ServiceStore` 在 `BaseService` / `TunService` 上直接打开名为 `service` 的 `SharedPreferences`;其他 Context(界面进程)走 `MultiProcessPreference`,authority 是 `Authorities.SETTINGS_PROVIDER`,也就是 `:background` 的 `PreferenceProvider`。策略 JSON 写在调用方 `filesDir` 下,细节见 [访问控制](../../docs/systems/access-control/INDEX.md)。 + +`AppStore` 和 `TipsStore` 不走这个 Provider。它们用本进程 `MODE_PRIVATE` 的 `SharedPreferences`,文件名分别是 `app` 和 `tips`。`AppStore` 只保存上面的 `updated_at`。`TipsStore` 有 `request_donate` 和 `primary_version`,当前仓库没有其他引用。 + +## 启停与重载 + +`util/Clash.kt`: + +- `startClashService`:`UiStore.enableVpn` 为真时先 `VpnService.prepare`。需要授权则把系统 Intent 返回给调用方,不启动服务。已授权则前台启动 `TunService`。未开 VPN 则前台启动 `ClashService`。两者都在 `:background`。 +- `stopClashService`:`sendBroadcastSelf(ACTION_CLASH_REQUEST_STOP)`。广播 `setPackage` 为本包,并要求 `RECEIVE_SELF_BROADCASTS`。 +- `requestClashReload`:同样发送 `ACTION_CLASH_REQUEST_RELOAD`。`AccessControlActivity` 在离开页面之后使用它。 + +谁处理这两条请求在 `service`,不在 `app`。 + +## 广播::background 通知界面进程 + +`sendBroadcastSelf` 把包名设成本包。`Broadcasts` 的接收器再核对 `intent.package` 是否为本包,然后按动作分发: + +| 动作 | 界面侧 | +|:-----|:-------| +| `ACTION_SERVICE_RECREATED` | `clashRunning = false`,`onServiceRecreated` | +| `ACTION_CLASH_STARTED` | `clashRunning = true`,`onStarted` | +| `ACTION_CLASH_STOPPED` | `clashRunning = false`,`onStopped`,额外字段是停止原因 | +| `ACTION_PROFILE_CHANGED` | `onProfileChanged` | +| `ACTION_PROFILE_UPDATE_COMPLETED` / `FAILED` | 从额外字段解析 UUID;失败还有原因字符串 | +| `ACTION_PROFILE_LOADED` | `onProfileLoaded` | + +`register()` 成功之后用 `StatusClient.currentProfile() != null` 填一次 `clashRunning`。 + +`Broadcasts.registered` 初始是 false,`register` 和 `unregister` 都没有把它写成 true。因此 `register()` 不会因为「已经注册」而返回;`unregister()` 一进来就返回,不会 `unregisterReceiver`,也不会把 `clashRunning` 置回 false。可见性回调仍然会调用这两个函数。`TileService` 自己在监听期间注册和注销,并带上 `RECEIVE_SELF_BROADCASTS`;`Broadcasts` 注册时没有传这个权限参数。 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9900007e38..5293129a49 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -101,6 +101,11 @@ + + + () { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + onBackPressedDispatcher.addCallback(this) { + finish() + } + } + override suspend fun main() { + val configUuid = intent.uuid val service = ServiceStore(this) - val selected = withContext(Dispatchers.IO) { - service.accessControlPackages.toMutableSet() + val (config, selected) = withContext(Dispatchers.IO) { + if (configUuid != null) { + val cfg = service.appsStrategyConfigs.find { it.uuid == configUuid } + if (cfg != null) { + cfg to cfg.packages.toMutableSet() + } else { + null to service.accessControlPackages.toMutableSet() + } + } else { + null to service.accessControlPackages.toMutableSet() + } } defer { - withContext(Dispatchers.IO) { - val changed = selected != service.accessControlPackages - service.accessControlPackages = selected - if (clashRunning && changed) { - stopClashService() - while (clashRunning) { - delay(200) - } - startClashService() - } + val running = clashRunning + val packages = selected.toSet() + val action = withContext(Dispatchers.IO) { + persistPackages(service, config, packages, running) + } + if (action.restartVpn || action.sync) { + scheduleExitWork(action) } } - val design = AccessControlDesign(this, uiStore, selected) + if (config != null && config.name.isNotBlank()) { + title = config.name + } + + val design = AccessControlDesign(this, uiStore, selected) { members -> + applyAppSetToggle(members, selected) + } setContentDesign(design) @@ -55,7 +86,9 @@ class AccessControlActivity : BaseActivity() { design.requests.onReceive { when (it) { AccessControlDesign.Request.ReloadApps -> { - design.patchApps(loadApps(selected)) + val loaded = loadApps(selected) + design.patchApps(loaded.apps) + design.patchBuiltinSets(loaded.systemPackages, loaded.domesticPackages) } AccessControlDesign.Request.SelectAll -> { @@ -87,18 +120,44 @@ class AccessControlActivity : BaseActivity() { } AccessControlDesign.Request.Import -> { - val clipboard = getSystemService() - val data = clipboard?.primaryClip - - if (data != null && data.itemCount > 0) { - val packages = data.getItemAt(0).text.split("\n").toSet() - val all = design.apps.map(AppInfo::packageName).intersect(packages) - - selected.clear() - selected.addAll(all) + val confirmed = iosConfirm( + context = this@AccessControlActivity, + title = getString(com.github.kr328.clash.design.R.string.import_), + message = getString(com.github.kr328.clash.design.R.string.import_from_clipboard), + confirm = getString(com.github.kr328.clash.design.R.string.ok), + destructive = true, + ) + if (confirmed) { + var failed = false + try { + val text = getSystemService() + ?.primaryClip + ?.takeIf { it.itemCount > 0 } + ?.getItemAt(0) + ?.text + if (text == null) { + failed = true + } else { + val packages = text.split("\n") + .map { it.replace("\r", "").trim() } + .filter { it.isNotEmpty() } + .toSet() + val matched = design.apps.map(AppInfo::packageName).intersect(packages) + if (matched.isNotEmpty()) { + selected.clear() + selected.addAll(matched) + } + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + failed = true + } + if (failed) { + design.showToast(DesignR.string.geofile_import_failed, ToastDuration.Long) + } + design.rebindAll() } - - design.rebindAll() } AccessControlDesign.Request.Export -> { @@ -117,40 +176,109 @@ class AccessControlActivity : BaseActivity() { } } - private suspend fun loadApps(selected: Set): List = - withContext(Dispatchers.IO) { - val reverse = uiStore.accessControlReverse - val sort = uiStore.accessControlSort - val systemApp = uiStore.accessControlSystemApp - - val base = compareByDescending { it.packageName in selected } - val comparator = if (reverse) base.thenDescending(sort) else base.then(sort) - - val pm = packageManager - val packages = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS) - - packages.asSequence() - .filter { - it.packageName != packageName + private fun persistPackages( + service: ServiceStore, + config: AppsStrategyConfig?, + packages: Set, + clashRunning: Boolean, + ): ExitAction { + val packagesChanged = if (config != null) { + val changed = packages != config.packages.toSet() + if (changed) { + val updatedConfig = AppsStrategyConfig( + uuid = config.uuid, + name = config.name, + mode = config.mode, + packages = packages.toList(), + ) + service.appsStrategyConfigs = service.appsStrategyConfigs.map { + if (it.uuid == config.uuid) updatedConfig else it } - .filter { - it.applicationInfo != null - } - .filter { - it.requestedPermissions?.contains(INTERNET) == true || it.applicationInfo!!.uid < android.os.Process.FIRST_APPLICATION_UID - } - .filter { - systemApp || !it.isSystemApp + WebdavAutoSync.noteAppsStrategy(this, config.uuid) + } + changed + } else { + val changed = packages != service.accessControlPackages + if (changed) { + service.accessControlPackages = packages + WebdavAutoSync.noteAccessControl(this) + } + changed + } + if (!packagesChanged) return ExitAction(restartVpn = false, sync = false) + val activeId = service.activeAppsStrategyConfigUuid + val active = activeId?.let { id -> service.appsStrategyConfigs.find { it.uuid == id } } + val drivesVpn = if (config != null) active?.uuid == config.uuid else active == null + return ExitAction(restartVpn = clashRunning && drivesVpn, sync = true) + } + + private fun scheduleExitWork(action: ExitAction) { + val app = applicationContext + Global.launch(Dispatchers.Main) { + if (action.restartVpn) { + try { + restartClash(app) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Log.w("Restart after access control change: ${e.message}", e) } - .map { - it.toAppInfo(pm) + } + if (action.sync) { + try { + syncAccessPackages(app) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Log.w("WebDAV after access control change: ${e.message}", e) } - .sortedWith(comparator) - .toList() + } } + } - private val PackageInfo.isSystemApp: Boolean - get() { - return applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM) != 0 + private fun restartClash(app: Context) { + Log.i("Reload tun after access control change") + app.requestClashReload() + } + + private suspend fun syncAccessPackages(app: Context) { + val settings = WebdavSettingsStore(app).load() + if (!settings.autoSync || !settings.hasEndpoint() || settings.username.isBlank() || settings.password.isEmpty()) { + return + } + withTimeoutOrNull(SYNC_TIMEOUT_MS) { + withProfile { + webdavPerform( + WebdavSync.ACTION_SYNC, + settings.url, + settings.lanUrl, + settings.username, + settings.password, + settings.remotePath, + ) + } + } + } + + private data class ExitAction( + val restartVpn: Boolean, + val sync: Boolean, + ) + + private suspend fun loadApps(selected: Set): AccessControlApps = + withContext(Dispatchers.IO) { + loadAccessControlApps( + pm = packageManager, + selfPackage = packageName, + showSystem = uiStore.accessControlSystemApp, + selected = selected, + sort = uiStore.accessControlSort, + reverse = uiStore.accessControlReverse, + domesticCatalog = DomesticAppCatalog.packages(this@AccessControlActivity), + ) } + + private companion object { + const val SYNC_TIMEOUT_MS = 60_000L + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/AccessControlApps.kt b/app/src/main/java/com/github/kr328/clash/AccessControlApps.kt new file mode 100644 index 0000000000..8201e38541 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/AccessControlApps.kt @@ -0,0 +1,55 @@ +package com.github.kr328.clash + +import android.Manifest +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Process +import com.github.kr328.clash.design.model.AppInfo +import com.github.kr328.clash.design.model.AppInfoSort +import com.github.kr328.clash.design.util.toAppInfo + +internal data class AccessControlApps( + val apps: List, + val systemPackages: Set, + val domesticPackages: Set, +) + +/** + * System and domestic sets ignore the "hide system apps" filter. + * That filter only changes which rows are visible. + */ +internal fun loadAccessControlApps( + pm: PackageManager, + selfPackage: String, + showSystem: Boolean, + selected: Set, + sort: AppInfoSort, + reverse: Boolean, + domesticCatalog: Set, +): AccessControlApps { + val base = compareByDescending { it.packageName in selected } + val comparator = if (reverse) base.thenDescending(sort) else base.then(sort) + + val systemPackages = mutableSetOf() + val domesticPackages = mutableSetOf() + val visible = mutableListOf() + + for (pkg in pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)) { + if (pkg.packageName == selfPackage) continue + val info = pkg.applicationInfo ?: continue + val hasInternet = pkg.requestedPermissions?.contains(Manifest.permission.INTERNET) == true + if (!hasInternet && info.uid >= Process.FIRST_APPLICATION_UID) continue + + val system = info.flags.and(ApplicationInfo.FLAG_SYSTEM) != 0 + if (system) systemPackages.add(pkg.packageName) + if (pkg.packageName in domesticCatalog) domesticPackages.add(pkg.packageName) + if (showSystem || !system) visible.add(pkg) + } + + return AccessControlApps( + apps = visible.map { it.toAppInfo(pm) }.sortedWith(comparator), + systemPackages = systemPackages, + domesticPackages = domesticPackages, + ) +} diff --git a/app/src/main/java/com/github/kr328/clash/AppsStrategyActivity.kt b/app/src/main/java/com/github/kr328/clash/AppsStrategyActivity.kt new file mode 100644 index 0000000000..c9f62a05bf --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/AppsStrategyActivity.kt @@ -0,0 +1,206 @@ +package com.github.kr328.clash + +import com.github.kr328.clash.common.util.intent +import com.github.kr328.clash.common.util.setUUID +import com.github.kr328.clash.design.AppsStrategyDesign +import com.github.kr328.clash.design.util.showExceptionToast +import com.github.kr328.clash.design.dialog.requestModelTextInput +import com.github.kr328.clash.design.ios.iosConfirm +import com.github.kr328.clash.design.requestAppListsConfigEdit +import com.github.kr328.clash.service.model.AccessControlMode +import com.github.kr328.clash.service.model.AppsStrategyConfig +import com.github.kr328.clash.service.store.ServiceStore +import com.github.kr328.clash.service.webdav.WebdavAutoSync +import com.github.kr328.clash.service.webdav.WebdavSettingsStore +import com.github.kr328.clash.service.webdav.WebdavSync +import com.github.kr328.clash.util.withProfile +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.selects.select +import kotlinx.coroutines.withContext +import java.util.* + +class AppsStrategyActivity : BaseActivity() { + override suspend fun main() { + val design = AppsStrategyDesign(this) + + setContentDesign(design) + + design.fetch() + + while (isActive) { + select { + events.onReceive { + when (it) { + Event.ActivityStart -> { + design.fetch() + } + + else -> Unit + } + } + design.requests.onReceive { + when (it) { + is AppsStrategyDesign.Request.Choose -> { + startActivity(AccessControlActivity::class.intent.setUUID(it.config.uuid)) + } + + is AppsStrategyDesign.Request.Edit -> { + val result = requestAppListsConfigEdit( + initialName = it.config.name, + initialMode = it.config.mode, + title = getString(com.github.kr328.clash.design.R.string.edit_config_name), + hint = getString(com.github.kr328.clash.design.R.string.config_name), + showDelete = true + ) + if (result != null) { + if (result.deleted) { + val confirmed = iosConfirm( + context = this@AppsStrategyActivity, + title = getString(com.github.kr328.clash.design.R.string.delete), + message = it.config.name.ifBlank { + getString(com.github.kr328.clash.design.R.string.delete) + }, + confirm = getString(com.github.kr328.clash.design.R.string.delete), + destructive = true, + ) + if (confirmed) { + deleteConfig(it.config.uuid) + design.fetch() + } + } else if (result.name != it.config.name || result.mode != it.config.mode) { + updateConfig(it.config.uuid, result.name, result.mode) + design.fetch() + } + } + } + + is AppsStrategyDesign.Request.Active -> { + setActiveConfig(it.config.uuid) + } + + AppsStrategyDesign.Request.Create -> { + val name = requestModelTextInput( + initial = "", + title = getString(com.github.kr328.clash.design.R.string.new_config), + hint = getString(com.github.kr328.clash.design.R.string.config_name) + ) + if (name.isNotBlank()) { + createConfig(name) + } + } + } + } + } + } + } + + private suspend fun AppsStrategyDesign.fetch() { + val configs = queryConfigs() + val activeUuid = queryActiveUuid() + patchConfigs(configs, activeUuid) + } + + private suspend fun queryConfigs(): List { + return withContext(Dispatchers.IO) { + val service = ServiceStore(this@AppsStrategyActivity) + service.appsStrategyConfigs + } + } + + private suspend fun queryActiveUuid(): UUID? { + return withContext(Dispatchers.IO) { + val service = ServiceStore(this@AppsStrategyActivity) + service.activeAppsStrategyConfigUuid + } + } + + private suspend fun setActiveConfig(uuid: UUID) { + withContext(Dispatchers.IO) { + val service = ServiceStore(this@AppsStrategyActivity) + service.activeAppsStrategyConfigUuid = uuid + WebdavAutoSync.noteActiveAppsStrategy(this@AppsStrategyActivity) + design?.fetch() + syncIfEnabled() + } + } + + private suspend fun deleteConfig(uuid: UUID) { + withContext(Dispatchers.IO) { + val service = ServiceStore(this@AppsStrategyActivity) + val configs = service.appsStrategyConfigs.filter { it.uuid != uuid } + service.appsStrategyConfigs = configs + if (service.activeAppsStrategyConfigUuid == uuid) { + service.activeAppsStrategyConfigUuid = null + WebdavAutoSync.noteActiveAppsStrategy(this@AppsStrategyActivity) + } + WebdavAutoSync.noteStrategyDeleted(this@AppsStrategyActivity, uuid) + syncIfEnabled() + } + } + + private suspend fun createConfig(name: String) { + withContext(Dispatchers.IO) { + val service = ServiceStore(this@AppsStrategyActivity) + val configs = service.appsStrategyConfigs.toMutableList() + val newConfig = AppsStrategyConfig( + uuid = UUID.randomUUID(), + name = name, + mode = com.github.kr328.clash.service.model.AccessControlMode.AcceptAll, + packages = emptyList() + ) + configs.add(newConfig) + service.appsStrategyConfigs = configs + WebdavAutoSync.noteAppsStrategy(this@AppsStrategyActivity, newConfig.uuid) + design?.fetch() + syncIfEnabled() + } + } + + private suspend fun updateConfig(uuid: UUID, newName: String, newMode: AccessControlMode) { + withContext(Dispatchers.IO) { + val service = ServiceStore(this@AppsStrategyActivity) + val configs = service.appsStrategyConfigs.map { config -> + if (config.uuid == uuid) { + AppsStrategyConfig( + uuid = config.uuid, + name = newName, + mode = newMode, + packages = config.packages + ) + } else { + config + } + } + service.appsStrategyConfigs = configs + WebdavAutoSync.noteAppsStrategy(this@AppsStrategyActivity, uuid) + syncIfEnabled() + } + } + + private suspend fun syncIfEnabled() { + val settings = WebdavSettingsStore(this).load() + if (!settings.autoSync || !settings.hasEndpoint() || settings.username.isBlank() || settings.password.isEmpty()) { + return + } + try { + withProfile { + webdavPerform( + WebdavSync.ACTION_SYNC, + settings.url, + settings.lanUrl, + settings.username, + settings.password, + settings.remotePath, + ) + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + withContext(Dispatchers.Main) { + design?.showExceptionToast(e) + } + } + } +} diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index b181713e13..7973c4e42d 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -1,6 +1,8 @@ package com.github.kr328.clash import android.app.ActivityManager +import android.content.Context +import android.content.pm.PackageManager import android.content.res.Configuration import android.os.Build import android.os.Bundle @@ -8,6 +10,7 @@ import android.view.View import androidx.activity.result.contract.ActivityResultContract import androidx.appcompat.app.AppCompatActivity import androidx.core.content.getSystemService +import com.github.kr328.clash.common.AppLanguages import com.github.kr328.clash.common.compat.isAllowForceDarkCompat import com.github.kr328.clash.common.compat.isLightNavigationBarCompat import com.github.kr328.clash.common.compat.isLightStatusBarsCompat @@ -86,8 +89,13 @@ abstract class BaseActivity> : AppCompatActivity(), } } + override fun attachBaseContext(newBase: Context) { + super.attachBaseContext(AppLanguages.wrap(newBase)) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + applyLocalizedTitle() applyDayNight() // Apply excludeFromRecents setting to all app tasks. @@ -145,6 +153,15 @@ abstract class BaseActivity> : AppCompatActivity(), } } + private fun applyLocalizedTitle() { + val labelRes = try { + packageManager.getActivityInfo(componentName, 0).labelRes + } catch (_: PackageManager.NameNotFoundException) { + 0 + } + if (labelRes != 0) setTitle(getString(labelRes)) + } + open fun shouldDisplayHomeAsUpEnabled(): Boolean { return true } diff --git a/app/src/main/java/com/github/kr328/clash/DomesticAppCatalog.kt b/app/src/main/java/com/github/kr328/clash/DomesticAppCatalog.kt new file mode 100644 index 0000000000..757dfc56b6 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/DomesticAppCatalog.kt @@ -0,0 +1,30 @@ +package com.github.kr328.clash + +import android.content.Context + +/** + * Package names of apps commonly distributed on Chinese app stores. + * The built-in set is dynamic: only names that are installed on this device are members. + */ +internal object DomesticAppCatalog { + private val packageName = Regex("""^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)+$""") + + @Volatile + private var cached: Set? = null + + fun packages(context: Context): Set { + cached?.let { return it } + return synchronized(this) { + cached ?: load(context).also { cached = it } + } + } + + private fun load(context: Context): Set { + return context.resources.openRawResource(R.raw.domestic_packages).bufferedReader().use { reader -> + reader.lineSequence().mapNotNull { line -> + val pkg = line.substringBefore('#').trim() + pkg.takeIf { packageName.matches(it) } + }.toSet() + } + } +} diff --git a/app/src/main/java/com/github/kr328/clash/ExternalControlActivity.kt b/app/src/main/java/com/github/kr328/clash/ExternalControlActivity.kt index e31d3d18b2..4e2731bf48 100644 --- a/app/src/main/java/com/github/kr328/clash/ExternalControlActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ExternalControlActivity.kt @@ -60,6 +60,7 @@ class ExternalControlActivity : Activity(), CoroutineScope by MainScope() { stopClash() } else { startClash() + return } } @@ -68,6 +69,7 @@ class ExternalControlActivity : Activity(), CoroutineScope by MainScope() { Toast.makeText(this, R.string.external_control_started, Toast.LENGTH_LONG).show() } else { startClash() + return } } @@ -83,16 +85,28 @@ class ExternalControlActivity : Activity(), CoroutineScope by MainScope() { } private fun startClash() { -// if (currentProfile == null) { -// Toast.makeText(this, R.string.no_profile_selected, Toast.LENGTH_LONG).show() -// return -// } - val vpnRequest = startClashService() - if (vpnRequest != null) { - Toast.makeText(this, R.string.unable_to_start_vpn, Toast.LENGTH_LONG).show() - return + val appContext = applicationContext + launch { + try { + val active = withProfile { queryActive() } + if (active == null || !active.imported) { + Toast.makeText(appContext, R.string.no_profile_selected, Toast.LENGTH_LONG).show() + return@launch + } + val vpnRequest = appContext.startClashService() + if (vpnRequest != null) { + try { + startActivity(VpnRequestActivity.consentIntent(this@ExternalControlActivity, vpnRequest)) + } catch (e: Exception) { + Toast.makeText(appContext, R.string.unable_to_start_vpn, Toast.LENGTH_LONG).show() + } + return@launch + } + Toast.makeText(appContext, R.string.external_control_started, Toast.LENGTH_LONG).show() + } finally { + finish() + } } - Toast.makeText(this, R.string.external_control_started, Toast.LENGTH_LONG).show() } private fun stopClash() { diff --git a/app/src/main/java/com/github/kr328/clash/FilesActivity.kt b/app/src/main/java/com/github/kr328/clash/FilesActivity.kt index 797c6c4b39..8fa820b813 100644 --- a/app/src/main/java/com/github/kr328/clash/FilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/FilesActivity.kt @@ -7,6 +7,7 @@ import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Build +import androidx.activity.addCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import com.github.kr328.clash.common.util.grantPermissions @@ -30,6 +31,9 @@ class FilesActivity : BaseActivity() { val root = uuid.toString() val design = FilesDesign(this) + onBackPressedDispatcher.addCallback(this) { + design.requests.trySend(FilesDesign.Request.PopStack) + } val client = FilesClient(this) val stack = Stack() @@ -58,6 +62,7 @@ class FilesActivity : BaseActivity() { finish() } else { stack.pop() + if (stack.empty()) design.showAtRoot() } } is FilesDesign.Request.OpenDirectory -> { @@ -81,6 +86,7 @@ class FilesActivity : BaseActivity() { client.renameDocument(it.file.id, newName) } is FilesDesign.Request.ImportFile -> { + if (it.file == null && stack.empty()) return@onReceive val uri: Uri? = startActivityForResult( ActivityResultContracts.GetContent(), "*/*" @@ -122,10 +128,6 @@ class FilesActivity : BaseActivity() { } } - override fun onBackPressed() { - design?.requests?.trySend(FilesDesign.Request.PopStack) - } - private suspend fun FilesDesign.fetch(client: FilesClient, stack: Stack, root: String) { val documentId = stack.lastOrNull() ?: root val files = if (stack.empty()) { diff --git a/app/src/main/java/com/github/kr328/clash/LogcatActivity.kt b/app/src/main/java/com/github/kr328/clash/LogcatActivity.kt index 6f5d0883fb..714d7f74a5 100644 --- a/app/src/main/java/com/github/kr328/clash/LogcatActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/LogcatActivity.kt @@ -53,7 +53,7 @@ class LogcatActivity : BaseActivity() { return showInvalid() } - val design = LogcatDesign(this, false) + val design = LogcatDesign(this, streaming = false, fileName = file.fileName) setContentDesign(design) diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index ea8254cb40..c399f21ffb 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -4,10 +4,8 @@ import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Bundle -import android.os.PersistableBundle import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts.RequestPermission -import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat @@ -17,11 +15,15 @@ import com.github.kr328.clash.common.util.intent import com.github.kr328.clash.common.util.ticker import com.github.kr328.clash.design.MainDesign import com.github.kr328.clash.design.ui.ToastDuration +import com.github.kr328.clash.service.store.ServiceStore import com.github.kr328.clash.util.startClashService import com.github.kr328.clash.util.stopClashService import com.github.kr328.clash.util.withClash import com.github.kr328.clash.util.withProfile +import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.bridge.* +import com.github.kr328.clash.core.model.ProxySort +import com.github.kr328.clash.core.model.TunnelState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.selects.select @@ -58,12 +60,19 @@ class MainActivity : BaseActivity() { else design.startClash() } - MainDesign.Request.OpenProxy -> - startActivity(ProxyActivity::class.intent) + is MainDesign.Request.OpenProxy -> { + val open = ProxyActivity::class.intent + if (!it.group.isNullOrEmpty()) { + open.putExtra(ProxyActivity.EXTRA_PROXY_GROUP, it.group) + } + startActivity(open) + } MainDesign.Request.OpenProfiles -> startActivity(ProfilesActivity::class.intent) MainDesign.Request.OpenProviders -> startActivity(ProvidersActivity::class.intent) + MainDesign.Request.OpenAppsStrategy -> + startActivity(AppsStrategyActivity::class.intent) MainDesign.Request.OpenLogs -> { if (LogcatService.running) { startActivity(LogcatActivity::class.intent) @@ -77,6 +86,15 @@ class MainActivity : BaseActivity() { startActivity(HelpActivity::class.intent) MainDesign.Request.OpenAbout -> design.showAbout(queryAppVersionName()) + is MainDesign.Request.SetMode -> { + design.showToast(DesignR.string.mode_switch_tips, ToastDuration.Long) + withClash { + val override = queryOverride(Clash.OverrideSlot.Session) + override.mode = it.mode + patchOverride(Clash.OverrideSlot.Session, override) + } + design.fetch() + } } } if (clashRunning) { @@ -91,24 +109,47 @@ class MainActivity : BaseActivity() { private suspend fun MainDesign.fetch() { setClashRunning(clashRunning) - val state = withClash { - queryTunnelState() + val mode = withClash { + queryOverride(Clash.OverrideSlot.Session).mode ?: queryTunnelState().mode } val providers = withClash { queryProviders() } - setMode(state.mode) + setMode(mode) setHasProviders(providers.isNotEmpty()) + val (node, group) = if (clashRunning) queryCurrentNode(mode) else null to null + setCurrentNode(node, group) + + if (clashRunning) { + fetchTraffic() + } withProfile { setProfileName(queryActive()?.name) } + + setAppsStrategyName(queryActiveAppsStrategyName()) } private suspend fun MainDesign.fetchTraffic() { withClash { - setForwarded(queryTrafficTotal()) + setTraffic(queryTrafficNow(), queryTrafficTotal()) + } + } + + private suspend fun queryCurrentNode(mode: TunnelState.Mode): Pair { + if (mode == TunnelState.Mode.Direct) return null to null + return withClash { + val names = queryProxyGroupNames(true) + val group = when (mode) { + TunnelState.Mode.Global -> + names.firstOrNull { it.equals("GLOBAL", ignoreCase = true) } ?: names.firstOrNull() + else -> + names.firstOrNull { !it.equals("GLOBAL", ignoreCase = true) } ?: names.firstOrNull() + } ?: return@withClash null to null + val now = queryProxyGroup(group, ProxySort.Default).now.takeIf { it.isNotBlank() } + now to group } } @@ -148,6 +189,18 @@ class MainActivity : BaseActivity() { } } + private suspend fun queryActiveAppsStrategyName(): String? { + return withContext(Dispatchers.IO) { + val service = ServiceStore(this@MainActivity) + val activeUuid = service.activeAppsStrategyConfigUuid + if (activeUuid != null) { + service.appsStrategyConfigs.find { it.uuid == activeUuid }?.name + } else { + null + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 9242d72299..77773406f2 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -1,27 +1,40 @@ package com.github.kr328.clash import android.app.Application +import android.content.BroadcastReceiver import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import com.github.kr328.clash.common.AppLanguages import com.github.kr328.clash.common.Global +import com.github.kr328.clash.common.compat.registerReceiverCompat import com.github.kr328.clash.common.compat.currentProcessName +import com.github.kr328.clash.common.constants.Intents +import com.github.kr328.clash.common.constants.Permissions import com.github.kr328.clash.common.log.Log import com.github.kr328.clash.remote.Remote import com.github.kr328.clash.service.util.sendServiceRecreated import com.github.kr328.clash.util.clashDir -import java.io.File -import java.io.FileOutputStream +import com.github.kr328.clash.util.installBundledGeoFile @Suppress("unused") class MainApplication : Application() { override fun attachBaseContext(base: Context?) { - super.attachBaseContext(base) + if (base != null) AppLanguages.rememberBaseline(base) + super.attachBaseContext(base?.let(AppLanguages::wrap)) Global.init(this) } override fun onCreate() { super.onCreate() + registerReceiverCompat( + appLanguageReceiver, + IntentFilter(Intents.ACTION_APP_LANGUAGE_CHANGED), + Permissions.RECEIVE_SELF_BROADCASTS, + exported = false, + ) val processName = currentProcessName extractGeoFiles() @@ -39,48 +52,31 @@ class MainApplication : Application() { clashDir.mkdirs() val updateDate = packageManager.getPackageInfo(packageName, 0).lastUpdateTime - val geoipFile = File(clashDir, "geoip.metadb") - if (geoipFile.exists() && geoipFile.lastModified() < updateDate) { - geoipFile.delete() + installBundledGeoFile(clashDir, "geoip.metadb", updateDate, true) { + assets.open("geoip.metadb") } - if (!geoipFile.exists()) { - FileOutputStream(geoipFile).use { - assets.open("geoip.metadb").copyTo(it) - } + installBundledGeoFile(clashDir, "geosite.dat", updateDate, true) { + assets.open("geosite.dat") } - - val geositeFile = File(clashDir, "geosite.dat") - if (geositeFile.exists() && geositeFile.lastModified() < updateDate) { - geositeFile.delete() - } - if (!geositeFile.exists()) { - FileOutputStream(geositeFile).use { - assets.open("geosite.dat").copyTo(it) - } - } - - val asnFile = File(clashDir, "ASN.mmdb") - if (asnFile.exists() && asnFile.lastModified() < updateDate) { - asnFile.delete() - } - if (!asnFile.exists()) { - FileOutputStream(asnFile).use { - assets.open("ASN.mmdb").copyTo(it) - } + installBundledGeoFile(clashDir, "ASN.mmdb", updateDate, true) { + assets.open("ASN.mmdb") } - - val bundleMRSFile = File(clashDir, "BundleMRS.7z") - if (bundleMRSFile.exists() && bundleMRSFile.lastModified() < updateDate) { - bundleMRSFile.delete() - } - if (!bundleMRSFile.exists()) { - FileOutputStream(bundleMRSFile).use { - assets.open("BundleMRS.7z").copyTo(it) - } + installBundledGeoFile(clashDir, "BundleMRS.7z", updateDate, false) { + assets.open("BundleMRS.7z") } } fun finalize() { Global.destroy() } + + private val appLanguageReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent?) { + if (intent?.action != Intents.ACTION_APP_LANGUAGE_CHANGED) return + if (!intent.hasExtra(Intents.EXTRA_APP_LANGUAGE)) return + val tag = intent.getStringExtra(Intents.EXTRA_APP_LANGUAGE) + AppLanguages.write(context, tag.orEmpty()) + AppLanguages.apply(context, tag) + } + } } diff --git a/app/src/main/java/com/github/kr328/clash/MetaFeatureSettingsActivity.kt b/app/src/main/java/com/github/kr328/clash/MetaFeatureSettingsActivity.kt index 47f0c4c2aa..8410d95780 100644 --- a/app/src/main/java/com/github/kr328/clash/MetaFeatureSettingsActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MetaFeatureSettingsActivity.kt @@ -8,14 +8,15 @@ import androidx.activity.result.contract.ActivityResultContracts import com.github.kr328.clash.core.Clash import com.github.kr328.clash.design.MetaFeatureSettingsDesign import com.github.kr328.clash.util.clashDir +import com.github.kr328.clash.util.importReplacingGeoFile import com.github.kr328.clash.util.withClash import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.selects.select import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileOutputStream +import java.io.IOException import com.github.kr328.clash.design.R @@ -103,7 +104,7 @@ class MetaFeatureSettingsActivity : BaseActivity() { .setTitle(R.string.geofile_unknown_db_format) .setMessage(getString(R.string.geofile_unknown_db_format_message, validDatabaseExtensions.joinToString("/"))) - .setPositiveButton("OK") { _, _ -> } + .setPositiveButton(R.string.ok) { _, _ -> } .show() return } @@ -119,16 +120,21 @@ class MetaFeatureSettingsActivity : BaseActivity() { else -> "" } - withContext(Dispatchers.IO) { - val outputFile = File(clashDir, outputFileName); - contentResolver.openInputStream(uri).use { ins -> - FileOutputStream(outputFile).use { outs -> - ins?.copyTo(outs) + try { + withContext(Dispatchers.IO) { + val input = contentResolver.openInputStream(uri) + ?: throw IOException("empty stream") + input.use { + importReplacingGeoFile(clashDir, outputFileName, it) } } + Toast.makeText(this, getString(R.string.geofile_imported, displayName), + Toast.LENGTH_LONG).show() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Toast.makeText(this, R.string.geofile_import_failed, Toast.LENGTH_LONG).show() } - Toast.makeText(this, getString(R.string.geofile_imported, displayName), - Toast.LENGTH_LONG).show() return } } diff --git a/app/src/main/java/com/github/kr328/clash/NetworkSettingsActivity.kt b/app/src/main/java/com/github/kr328/clash/NetworkSettingsActivity.kt index c7cc72451f..304a5d0150 100644 --- a/app/src/main/java/com/github/kr328/clash/NetworkSettingsActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/NetworkSettingsActivity.kt @@ -2,8 +2,14 @@ package com.github.kr328.clash import com.github.kr328.clash.common.util.intent import com.github.kr328.clash.design.NetworkSettingsDesign +import com.github.kr328.clash.design.util.showExceptionToast import com.github.kr328.clash.service.store.ServiceStore +import com.github.kr328.clash.service.webdav.WebdavAutoSync +import com.github.kr328.clash.service.webdav.WebdavSettingsStore +import com.github.kr328.clash.service.webdav.WebdavSync +import com.github.kr328.clash.util.withProfile import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.coroutines.selects.select class NetworkSettingsActivity : BaseActivity() { @@ -30,10 +36,33 @@ class NetworkSettingsActivity : BaseActivity() { when (it) { NetworkSettingsDesign.Request.StartAccessControlList -> startActivity(AccessControlActivity::class.intent) + NetworkSettingsDesign.Request.AccessControlChanged -> + launch { syncAccessControl(design) } } } } } } + private suspend fun syncAccessControl(design: NetworkSettingsDesign) { + WebdavAutoSync.noteAccessControl(this) + val settings = WebdavSettingsStore(this).load() + if (!settings.autoSync || !settings.hasEndpoint() || settings.username.isBlank() || settings.password.isEmpty()) { + return + } + try { + withProfile { + webdavPerform( + WebdavSync.ACTION_SYNC, + settings.url, + settings.lanUrl, + settings.username, + settings.password, + settings.remotePath, + ) + } + } catch (e: Exception) { + design.showExceptionToast(e) + } + } } diff --git a/app/src/main/java/com/github/kr328/clash/NewProfileActivity.kt b/app/src/main/java/com/github/kr328/clash/NewProfileActivity.kt index 99af8d5c76..bf7eb42a21 100644 --- a/app/src/main/java/com/github/kr328/clash/NewProfileActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/NewProfileActivity.kt @@ -38,10 +38,10 @@ class NewProfileActivity : BaseActivity() { override suspend fun main() { val design = NewProfileDesign(this) - design.patchProviders(queryProfileProviders()) - setContentDesign(design) + design.patchProviders(queryProfileProviders()) + while (isActive) { select { events.onReceive { diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index 13d2eedd9a..fe570680b7 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -75,8 +75,6 @@ class ProfilesActivity : BaseActivity() { startActivity(PropertiesActivity::class.intent.setUUID(uuid)) } - is ProfilesDesign.Request.ShareQrCode -> - design.showProfileQrCode(it.profile) } } if (activityStarted) { @@ -126,4 +124,4 @@ class ProfilesActivity : BaseActivity() { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/PropertiesActivity.kt b/app/src/main/java/com/github/kr328/clash/PropertiesActivity.kt index a6fc07550a..4c65e456d3 100644 --- a/app/src/main/java/com/github/kr328/clash/PropertiesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/PropertiesActivity.kt @@ -1,5 +1,6 @@ package com.github.kr328.clash +import androidx.activity.addCallback import com.github.kr328.clash.common.util.intent import com.github.kr328.clash.common.util.setUUID import com.github.kr328.clash.common.util.uuid @@ -36,6 +37,16 @@ class PropertiesActivity : BaseActivity() { withProfile { release(uuid) } } + onBackPressedDispatcher.addCallback(this) { + this@PropertiesActivity.launch { + if (!design.progressing) { + if (original == design.profile || design.requestExitWithoutSaving()) { + finish() + } + } + } + } + while (isActive) { select { events.onReceive { @@ -69,17 +80,6 @@ class PropertiesActivity : BaseActivity() { } } - override fun onBackPressed() { - design?.apply { - launch { - if (!progressing) { - if (original == profile || requestExitWithoutSaving()) - finish() - } - } - } ?: return super.onBackPressed() - } - private suspend fun PropertiesDesign.verifyAndCommit() { when { profile.name.isBlank() -> { diff --git a/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt index 9136a3000d..566d037124 100644 --- a/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt @@ -1,35 +1,90 @@ package com.github.kr328.clash +import android.os.Bundle import com.github.kr328.clash.common.util.intent import com.github.kr328.clash.core.Clash -import com.github.kr328.clash.core.model.Proxy import com.github.kr328.clash.design.ProxyDesign import com.github.kr328.clash.design.model.ProxyState import com.github.kr328.clash.util.withClash import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.selects.select +import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withPermit class ProxyActivity : BaseActivity() { + private var restoredGroup: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + restoredGroup = savedInstanceState?.getString(STATE_GROUP) + super.onCreate(savedInstanceState) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + design?.currentGroupName?.let { outState.putString(STATE_GROUP, it) } + } + override suspend fun main() { - val mode = withClash { queryOverride(Clash.OverrideSlot.Session).mode } val names = withClash { queryProxyGroupNames(uiStore.proxyExcludeNotSelectable) } - val states = List(names.size) { ProxyState("?") } - val unorderedStates = names.indices.map { names[it] to states[it] }.toMap() + val states = List(names.size) { ProxyState("") } + val unorderedStates = names.indices.associate { names[it] to states[it] } val reloadLock = Semaphore(10) + val reloadSeq = IntArray(names.size) + val selectSeq = IntArray(names.size) + // Non-zero only for the reload a speed test itself starts. A dropped result must not clear it. + val clearOnTicket = IntArray(names.size) + val seqLock = Mutex() + val index = initialGroupIndex(names) val design = ProxyDesign( this, - mode, names, - uiStore - ) + index, + uiStore, + states, + ) { group -> + uiStore.proxyLastGroup = group + intent.putExtra(EXTRA_PROXY_GROUP, group) + } setContentDesign(design) - design.requests.send(ProxyDesign.Request.ReloadAll) + suspend fun startQuery(groupIndex: Int, fromUrlTest: Boolean) { + val (ticket, selectAtStart) = seqLock.withLock { + val next = ++reloadSeq[groupIndex] + if (fromUrlTest) clearOnTicket[groupIndex] = next + next to selectSeq[groupIndex] + } + val group = reloadLock.withPermit { + withClash { queryProxyGroup(names[groupIndex], uiStore.proxySort) } + } + val clearUrlTesting: Boolean = seqLock.withLock { + if (reloadSeq[groupIndex] != ticket) return@withLock null + if (selectSeq[groupIndex] == selectAtStart) { + states[groupIndex].now = group.now + } + val clear = clearOnTicket[groupIndex] == ticket + if (clear) clearOnTicket[groupIndex] = 0 + clear + } ?: return + design.updateGroup( + groupIndex, + group.proxies, + group.type == "Selector", + states[groupIndex], + unorderedStates, + group.type, + clearUrlTesting, + ) + } + + // Direct mode and a fully hidden list are empty. Do not Reload index 0. + if (index >= 0) { + design.requests.trySend(ProxyDesign.Request.Reload(index)) + } while (isActive) { select { @@ -41,9 +96,7 @@ class ProxyActivity : BaseActivity() { } if (newNames != names) { - startActivity(ProxyActivity::class.intent) - - finish() + relaunchShowingCurrent() } } else -> Unit @@ -51,37 +104,19 @@ class ProxyActivity : BaseActivity() { } design.requests.onReceive { when (it) { - ProxyDesign.Request.ReLaunch -> { - startActivity(ProxyActivity::class.intent) - - finish() - } + ProxyDesign.Request.ReLaunch -> relaunchShowingCurrent() ProxyDesign.Request.ReloadAll -> { names.indices.forEach { idx -> design.requests.trySend(ProxyDesign.Request.Reload(idx)) } } is ProxyDesign.Request.Reload -> { - launch { - val group = reloadLock.withPermit { - withClash { - queryProxyGroup(names[it.index], uiStore.proxySort) - } - } - val state = states[it.index] - - state.now = group.now - - design.updateGroup( - it.index, - group.proxies, - group.type == "Selector", - state, - unorderedStates - ) - } + if (it.index !in names.indices) return@onReceive + launch { startQuery(it.index, fromUrlTest = false) } } is ProxyDesign.Request.Select -> { + if (it.index !in names.indices) return@onReceive + seqLock.withLock { selectSeq[it.index]++ } withClash { patchSelector(names[it.index], it.name) @@ -91,12 +126,12 @@ class ProxyActivity : BaseActivity() { design.requestRedrawVisible() } is ProxyDesign.Request.UrlTest -> { + if (it.index !in names.indices) return@onReceive launch { withClash { healthCheck(names[it.index]) } - - design.requests.send(ProxyDesign.Request.Reload(it.index)) + startQuery(it.index, fromUrlTest = true) } } is ProxyDesign.Request.PatchMode -> { @@ -115,4 +150,32 @@ class ProxyActivity : BaseActivity() { } } } -} \ No newline at end of file + + private fun initialGroupIndex(names: List): Int { + if (names.isEmpty()) return -1 + // Rotation only keeps uiMode. Prefer the group that was on screen, then the launch extra. + restoredGroup?.takeIf { it.isNotEmpty() }?.let { saved -> + val savedIndex = names.indexOf(saved) + if (savedIndex >= 0) return savedIndex + } + intent.getStringExtra(EXTRA_PROXY_GROUP)?.takeIf { it.isNotEmpty() }?.let { extra -> + val extraIndex = names.indexOf(extra) + if (extraIndex >= 0) return extraIndex + } + val last = names.indexOf(uiStore.proxyLastGroup) + if (last >= 0) return last + return 0 + } + + private fun relaunchShowingCurrent() { + val next = ProxyActivity::class.intent + design?.currentGroupName?.let { next.putExtra(EXTRA_PROXY_GROUP, it) } + startActivity(next) + finish() + } + + companion object { + const val EXTRA_PROXY_GROUP = "proxy_group" + private const val STATE_GROUP = "proxy_shown_group" + } +} diff --git a/app/src/main/java/com/github/kr328/clash/SettingsActivity.kt b/app/src/main/java/com/github/kr328/clash/SettingsActivity.kt index 95a0393367..65c47b3d30 100644 --- a/app/src/main/java/com/github/kr328/clash/SettingsActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/SettingsActivity.kt @@ -26,6 +26,8 @@ class SettingsActivity : BaseActivity() { startActivity(OverrideSettingsActivity::class.intent) SettingsDesign.Request.StartMetaFeature -> startActivity(MetaFeatureSettingsActivity::class.intent) + SettingsDesign.Request.StartWebdav -> + startActivity(WebdavSettingsActivity::class.intent) } } } diff --git a/app/src/main/java/com/github/kr328/clash/TileService.kt b/app/src/main/java/com/github/kr328/clash/TileService.kt index d24abbb470..fe658f72d0 100644 --- a/app/src/main/java/com/github/kr328/clash/TileService.kt +++ b/app/src/main/java/com/github/kr328/clash/TileService.kt @@ -1,5 +1,6 @@ package com.github.kr328.clash +import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -9,6 +10,7 @@ import android.os.Build import android.service.quicksettings.Tile import android.service.quicksettings.TileService import androidx.annotation.RequiresApi +import com.github.kr328.clash.common.compat.pendingIntentFlags import com.github.kr328.clash.common.compat.registerReceiverCompat import com.github.kr328.clash.common.constants.Intents import com.github.kr328.clash.common.constants.Permissions @@ -27,7 +29,10 @@ class TileService : TileService() { when (tile.state) { Tile.STATE_INACTIVE -> { - startClashService() + val prepare = startClashService() + if (prepare != null) { + openVpnConsent(prepare) + } } Tile.STATE_ACTIVE -> { stopClashService() @@ -35,6 +40,23 @@ class TileService : TileService() { } } + private fun openVpnConsent(prepare: Intent) { + val consent = VpnRequestActivity.consentIntent(this, prepare) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + val pending = PendingIntent.getActivity( + this, + 0, + consent, + pendingIntentFlags(PendingIntent.FLAG_UPDATE_CURRENT), + ) + startActivityAndCollapse(pending) + } else { + @Suppress("DEPRECATION") + startActivityAndCollapse(consent) + } + } + override fun onStartListening() { super.onStartListening() diff --git a/app/src/main/java/com/github/kr328/clash/VpnRequestActivity.kt b/app/src/main/java/com/github/kr328/clash/VpnRequestActivity.kt new file mode 100644 index 0000000000..b36f71418b --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/VpnRequestActivity.kt @@ -0,0 +1,58 @@ +package com.github.kr328.clash + +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContracts +import com.github.kr328.clash.util.startClashService + +class VpnRequestActivity : ComponentActivity() { + private val launcher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == RESULT_OK) { + startClashService() + } + finish() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + @Suppress("DEPRECATION") + overridePendingTransition(0, 0) + if (savedInstanceState != null) return + val prepare = readPrepare() + if (prepare == null) { + finish() + return + } + launcher.launch(prepare) + } + + override fun finish() { + super.finish() + @Suppress("DEPRECATION") + overridePendingTransition(0, 0) + } + + private fun readPrepare(): Intent? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(EXTRA_PREPARE, Intent::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(EXTRA_PREPARE) + } + } + + companion object { + const val EXTRA_PREPARE = "vpn_prepare" + + fun consentIntent(context: Context, prepare: Intent): Intent { + return Intent(context, VpnRequestActivity::class.java) + .putExtra(EXTRA_PREPARE, prepare) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + } +} diff --git a/app/src/main/java/com/github/kr328/clash/WebdavSettingsActivity.kt b/app/src/main/java/com/github/kr328/clash/WebdavSettingsActivity.kt new file mode 100644 index 0000000000..f450491c23 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/WebdavSettingsActivity.kt @@ -0,0 +1,78 @@ +package com.github.kr328.clash + +import com.github.kr328.clash.design.WebdavSettingsDesign +import com.github.kr328.clash.design.ui.ToastDuration +import com.github.kr328.clash.design.R as DesignR +import com.github.kr328.clash.design.util.showExceptionToast +import com.github.kr328.clash.service.webdav.WebdavLocalState +import com.github.kr328.clash.service.webdav.WebdavSettingsStore +import com.github.kr328.clash.service.webdav.WebdavSync +import com.github.kr328.clash.util.withProfile +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.selects.select + +class WebdavSettingsActivity : BaseActivity() { + private var busy = false + + override suspend fun main() { + val store = WebdavSettingsStore(this) + val design = WebdavSettingsDesign( + this, + store, + WebdavLocalState(this).lastMessage(), + ) + + setContentDesign(design) + + while (isActive) { + select { + events.onReceive { + } + design.requests.onReceive { + val action = when (it) { + WebdavSettingsDesign.Request.Test -> WebdavSync.ACTION_TEST + WebdavSettingsDesign.Request.Sync -> WebdavSync.ACTION_SYNC + WebdavSettingsDesign.Request.Pull -> WebdavSync.ACTION_PULL + WebdavSettingsDesign.Request.Push -> WebdavSync.ACTION_PUSH + } + runAction(design, store, action) + } + } + } + } + + private suspend fun runAction( + design: WebdavSettingsDesign, + store: WebdavSettingsStore, + action: String, + ) { + // Queued work is received only after finally, when busy is already false. + if (busy) return + busy = true + try { + val settings = store.load() + val message = design.withProgress { update -> + withProfile { + webdavPerform( + action, + settings.url, + settings.lanUrl, + settings.username, + settings.password, + settings.remotePath, + ) { phase, current, total, label -> + design.launch { update(phase, current, total, label) } + } + } + } + design.setStatus(message) + design.showToast(message, ToastDuration.Long) + } catch (e: Exception) { + design.showExceptionToast(e) + } finally { + busy = false + design.onActionFinished() + } + } +} diff --git a/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt index 9be7026bf5..94fac4000b 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt @@ -5,9 +5,11 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.widget.Toast import com.github.kr328.clash.common.compat.registerReceiverCompat import com.github.kr328.clash.common.constants.Intents import com.github.kr328.clash.common.log.Log +import com.github.kr328.clash.design.R as DesignR import java.util.* class Broadcasts(private val context: Application) { @@ -72,6 +74,14 @@ class Broadcasts(private val context: Application) { it.onProfileLoaded() } } + Intents.ACTION_CLASH_RELOAD_REJECTED -> { + val app = this@Broadcasts.context + Toast.makeText( + app, + app.getString(DesignR.string.access_control_reload_rejected), + Toast.LENGTH_LONG, + ).show() + } } } } @@ -97,6 +107,7 @@ class Broadcasts(private val context: Application) { addAction(Intents.ACTION_PROFILE_UPDATE_COMPLETED) addAction(Intents.ACTION_PROFILE_UPDATE_FAILED) addAction(Intents.ACTION_PROFILE_LOADED) + addAction(Intents.ACTION_CLASH_RELOAD_REJECTED) }) clashRunning = StatusClient(context).currentProfile() != null diff --git a/app/src/main/java/com/github/kr328/clash/util/Clash.kt b/app/src/main/java/com/github/kr328/clash/util/Clash.kt index 777fb12d99..b700febf03 100644 --- a/app/src/main/java/com/github/kr328/clash/util/Clash.kt +++ b/app/src/main/java/com/github/kr328/clash/util/Clash.kt @@ -29,4 +29,8 @@ fun Context.startClashService(): Intent? { fun Context.stopClashService() { sendBroadcastSelf(Intent(Intents.ACTION_CLASH_REQUEST_STOP)) +} + +fun Context.requestClashReload() { + sendBroadcastSelf(Intent(Intents.ACTION_CLASH_REQUEST_RELOAD)) } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/util/GeoFiles.kt b/app/src/main/java/com/github/kr328/clash/util/GeoFiles.kt new file mode 100644 index 0000000000..6da55f851f --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/util/GeoFiles.kt @@ -0,0 +1,121 @@ +package com.github.kr328.clash.util + +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream + +internal val userGeoFileNames: Set = setOf( + "geoip.metadb", + "geosite.dat", + "ASN.mmdb", +) + +internal fun shouldInstallGeoAsset( + exists: Boolean, + lastModified: Long, + lastUpdateTime: Long, + userMarked: Boolean, +): Boolean { + if (userMarked) return false + if (!exists) return true + return lastModified < lastUpdateTime +} + +internal fun geoUserSidecar(clashDir: File): File { + val parent = clashDir.parentFile ?: throw IOException("missing parent") + return File(parent, "clash-user") +} + +internal fun isUserGeoMarked(clashDir: File, name: String): Boolean { + return File(geoUserSidecar(clashDir), name).isFile +} + +internal fun markUserGeoFile(clashDir: File, name: String) { + if (name !in userGeoFileNames) return + val dir = geoUserSidecar(clashDir) + if (!dir.exists() && !dir.mkdirs() && !dir.isDirectory) { + throw IOException("failed to create ${dir.path}") + } + val marker = File(dir, name) + if (!marker.exists()) { + marker.createNewFile() + } + if (!marker.isFile) { + throw IOException("failed to mark $name") + } +} + +internal fun clearUserGeoMark(clashDir: File, name: String) { + File(geoUserSidecar(clashDir), name).delete() +} + +internal fun replaceFileFromStream(target: File, input: InputStream) { + val parent = target.parentFile ?: throw IOException("missing parent") + if (!parent.exists() && !parent.mkdirs() && !parent.isDirectory) { + throw IOException("failed to create ${parent.path}") + } + val tmp = File.createTempFile(".${target.name}.", ".partial", parent) + var published = false + try { + FileOutputStream(tmp).use { output -> + input.copyTo(output) + } + published = tmp.renameTo(target) + if (!published) { + throw IOException("failed to replace ${target.name}") + } + } finally { + if (!published && tmp.exists()) tmp.delete() + } +} + +internal fun installBundledGeoFile( + clashDir: File, + name: String, + lastUpdateTime: Long, + userManaged: Boolean, + open: () -> InputStream, +) { + val target = File(clashDir, name) + val managed = userManaged && name in userGeoFileNames + withGeoInstallLock(clashDir, name) { + if (managed && !target.exists()) { + clearUserGeoMark(clashDir, name) + if (isUserGeoMarked(clashDir, name)) { + throw IOException("failed to clear mark $name") + } + } + val marked = managed && target.exists() && isUserGeoMarked(clashDir, name) + if (!shouldInstallGeoAsset(target.exists(), target.lastModified(), lastUpdateTime, marked)) { + return@withGeoInstallLock + } + open().use { input -> + replaceFileFromStream(target, input) + } + } +} + +internal fun importReplacingGeoFile(clashDir: File, name: String, input: InputStream) { + val target = File(clashDir, name) + withGeoInstallLock(clashDir, name) { + replaceFileFromStream(target, input) + markUserGeoFile(clashDir, name) + } +} + +private fun withGeoInstallLock(clashDir: File, name: String, block: () -> T): T { + val dir = geoUserSidecar(clashDir) + if (!dir.exists() && !dir.mkdirs() && !dir.isDirectory) { + throw IOException("failed to create ${dir.path}") + } + val lockFile = File(dir, ".$name.lock") + return FileOutputStream(lockFile).use { stream -> + val lock = stream.channel.lock() + try { + block() + } finally { + if (lock.isValid) lock.release() + } + } +} diff --git a/app/src/main/res/raw/domestic_packages.txt b/app/src/main/res/raw/domestic_packages.txt new file mode 100644 index 0000000000..32acf8a957 --- /dev/null +++ b/app/src/main/res/raw/domestic_packages.txt @@ -0,0 +1,1761 @@ +# Domestic apps commonly distributed on Chinese app stores. +# Package names are a snapshot of mnixry/direct-android-ruleset +# rules/@Merged/APP on 2026-10-01, plus a few widely used packages +# that snapshot did not include. Lines are `package # name`. +# The app set matches these names against apps installed on the device. +ai.zuoye.app # 爱作业 +aihuishou.aihuishouapp # 爱回收 +air.com.bbfriend.ddsz # 哆哆识字 +air.com.gongfubb.wksz # 悟空识字 +air.tv.douyu.android # 斗鱼 +all.parttimeguidesystem # 兼职猫-同城找工作求职招聘平台 +android.translate.xuedianba # 同声翻译超级版 +android.zhibo8 # 直播吧 +app.Artronauction # 雅昌拍卖图录 +az.studio.gkztc # 高考直通车 +battymole.trainticket # 买火车票 +bin.mt.plus # MT管理器 +bj.wanquan.hanfeislidecompact # 汉飞滑动输入法大屏手机版 +bubei.tingshu # 懒人听书-QQ音乐听书版 +bzjh.three.com # 神图壁纸 +cc.rs.gc # GG租号 +cc.rs.rrzh # 人人租号 +ch999.app.UI # 九机网 +cmb.pb # 招商银行 +cmccwm.mobilemusic # 咪咕音乐 +cn.banshenggua.aichang # 爱唱 +cn.bdqn.pictureview # 手机图库 +cn.bluecrane.album # 相册管家 +cn.bluecrane.calendar # 万年历黄历 +cn.buding.martin # 微车 +cn.caocaokeji.user # 曹操出行 +cn.cbct.seefm # 听见广播 +cn.cisc # 中金财富 +cn.cj.pe # 139邮箱 +cn.cntv # 央视影音 +cn.cntvhd # 央视影音HD +cn.cntvnews # 央视新闻 +cn.colorv # 彩视 +cn.com.bmac.nfc +cn.com.cmbc.newmbank # 民生银行 +cn.com.enorth.jinyun # 津云 +cn.com.gjzq.yjb.kh # 国金证券开户 +cn.com.gxrb.client # 广西云 +cn.com.htjc.pay # 电e宝 +cn.com.modernmedia.modernlady # InStyle iLady +cn.com.nbd.nbdmobile # 每日经济新闻 +cn.com.njxmxbank.mbank # 南银法巴消金 +cn.com.open.mooc # 慕课网 +cn.com.pansky.sxzgyl # 陕西社会保险 +cn.com.pcauto.android.browser # 太平洋汽车 +cn.com.pcgroup.magazine # PChouse太平洋家居 +cn.com.pconline.android.browser # 太平洋科技 +cn.com.qlwb.qiluyidian # 齐鲁壹点 +cn.com.shbank.mper # 上海银行 +cn.com.sina.finance # 新浪财经 +cn.com.sina.sports # 新浪体育 +cn.com.spdb.mobilebank.per # 浦发银行 +cn.com.tx.chat.android # 天下网 +cn.com.wiisoft.glass # 灵动液态玻璃 +cn.com.wiisoft.skin # 灵动主题皮肤 +cn.com.zjol # 潮新闻 +cn.dahebao # 顶端新闻 +cn.damai # 大麦 +cn.danatech.xingseapp # 形色 +cn.dict.android.pro # 海词词典 +cn.dictcn.android.digitize.wys_korean # 外研社韩语 +cn.dxy.android.aspirin # 丁香医生 +cn.dxy.idxyer # 丁香园 +cn.dxy.medicinehelper # 用药助手 +cn.eclicks.drivingtest # 车轮驾考通 +cn.eclicks.wzsearch # 车轮 +cn.ecook # 网上厨房 +cn.edaijia.android.client # e代驾 +cn.edu.zjicm.wordsnet_d # 知米背单词 +cn.egame.terminal.cloud5g # 天翼云游戏 +cn.emagsoftware.gamehall # 咪咕游戏 +cn.emoney.level2 # 益盟操盘手经典版 +cn.etouch.ecalendar # 中华万年历 +cn.gov.pbc.dcep # 数字人民币 +cn.gov.tax.its # 个人所得税 +cn.guancha.app # 观察者网 +cn.gzmovement # 动静新闻 +cn.hsa.app # 国家医保服务平台 +cn.huntlaw.android # 好律师 +cn.ibabyzone.music # 胎教盒子 +cn.idashi.app # 大师一百 +cn.ishansong # 闪送 +cn.itheme.tech # 手机主题 +cn.izdax.ime # izdax输入法 +cn.jingling.motu.photowonder # 魔图精灵 +cn.jjpengcheng.wallpaper # 动态壁纸 +cn.jsb.china # 江苏银行 +cn.kaoshi100.view # 考试100 +cn.knet.eqxiu # 易企秀 +cn.kuwo.kwmusichd # 酷我音乐HD +cn.kuwo.player # 酷我音乐 +cn.ledongli.ldl # 乐动力 +cn.longmaster.pengpeng # 语玩 +cn.lrapps.changliaocall # 畅聊网络电话 +cn.mama.activity # 妈妈网轻聊 +cn.microants.merchants # 义采宝义乌小商品批发网 +cn.mimilive.xianyu # 爆聊 +cn.mopon.film.xflh # 幸福看 +cn.mucang.android.jiakao.uygur # 驾考宝典维语版 +cn.mucang.android.jkxt # 驾考宝典多语言版 +cn.mucang.android.parallelvehicle # 平行之家 +cn.mucang.android.parallelvehicle.manager # 平行之家商家版 +cn.mucang.android.qichetoutiao # 车友头条 +cn.mucang.drunkremind.android # 小猪二手车 +cn.myhug.baobao # 抱抱直播 +cn.ninegame.gamemanager # 九游 +cn.nokia.speedtest5g # speedtest5g +cn.opda.a.phonoalbumshoushou # 百度手机卫士 +cn.poco.jane # 简拼 +cn.poco.pMix # 图片合成器 +cn.rainbow.westore # 天虹 +cn.samsclub.app # 山姆会员商店 +cn.shengmimi.callshow # 视频铃声 +cn.shuangshuangfei # 找对象 +cn.soulapp.android # Soul +cn.szjxgs.szjob # 工友通 +cn.tianya.light # 天涯社区 +cn.vcinema.cinema # 南瓜电影 +cn.wangxiao.cpazhuntiku # 注册会计师准题库 +cn.wangxiao.gwyzhuntiku # 公务员准题库 +cn.weli.sweet # CUCU +cn.wenyu.bodian # 波点音乐 +cn.wiz.note # 为知笔记 +cn.wondershare.filmorago # 万兴喵影 +cn.wps.moffice_eng # WPS Office +cn.wsds.gamemaster # 迅游手游加速器 +cn.xiaochuankeji.tieba # 最右 +cn.xiaofengkj.fitpro # FitPro +cn.xuexi.android # 学习强国 +cn.yonghui.hyd # 永辉线上超市 +cn.youth.news # 中青看点 +cn.zepeto.main # 崽崽ZEPETO +cn.zymk.comic # 知音漫客 +co.runner.app # 悦跑圈 +co.welab.wolaidai # 我来数科 +com.A17zuoye.mobile.homework # 一起作业 +com.able.wisdomtree # 知到 +com.acadsoc.talkshow # 阿卡索英语 +com.achievo.vipshop # 唯品会 +com.actionsmicro.ezcast # EZCast +com.ada.desks # 安卓桌面 +com.admin.dingdongphone # 叮咚网络电话 +com.adnonstop.mancamera2017 # 型影 +com.adobe.reader # PDF阅读器 Adobe reader +com.advasoft.touchretouch # TouchRetouch +com.aheading.news.lanzhourb # 奔流新闻 +com.ahnews.newsclient # 中安在线 +com.ai.marki # 马克水印相机 +com.aikan # 免费小说大全 +com.aiwu.market # 爱吾游戏宝盒 +com.alarmclock # 闹钟 +com.alibaba.ailabs.tg # 天猫精灵 +com.alibaba.android.rimet # 钉钉 +com.alibaba.intl.android.apps.poseidon # Alibaba.com +com.alibaba.wireless # 阿里巴巴 +com.alicloud.databox # 阿里云盘 +com.alicom.smartdial +com.alimama.moon # 淘宝联盟 +com.aliyun.tongyi # 千问 +com.ancda.parents # 掌心宝贝 +com.android.abustory # 阿布睡前故事 +com.android.app.quanmama # 券妈妈优惠券 +com.android.autoAirPlaneMode # 自动飞行模式 +com.android.bankabc # 农行掌上银行 +com.android.benlailife.activity # 本来生活 +com.android.clock.sd # 山东农信 +com.android.dazhihui # 大智慧 +com.android.haitong # 国泰海通通财 +com.android.icredit # 企查查 +com.android.tutuerge # 兔兔儿歌 +com.android36kr.app # 36氪 +com.Android56 # 56视频 +com.androidesk # 安卓壁纸 +com.androidesk.livewallpaper # 安卓动态壁纸 +com.anhuioss.simpleime # Simple IME +com.anjiu.buff.yingyongbao # BUFF手游 +com.anjuke.android.app # 安居客 +com.ants360.yicamera # 小蚁摄像机 +com.antutu.ABenchMark # 安兔兔评测 +com.antutu.benchmark.full # 安兔兔评测3D +com.anwei.meivliao # 美V聊天 +com.app.dzy.btchat # 蓝牙闲聊 +com.app.hero.ui # K歌达人 +com.app.ymqzy # 动态壁纸秀场 +com.appbyme.app71974 # 编织人生论坛 +com.appchina.anpai.2019071601 # 鸟类壁纸 +com.appchina.anpai.20210203003 # 什么时间 - 翻转时钟桌面小组件Widget +com.appcina.anpai.2019080818 # 克拉壁纸 +com.appcina.anpai.2019092902 # 落格输入法 +com.appcina.anpai.2019112101 # Artpaper:有 1,300 多张艺术画壁纸可每日更换 +com.appcina.anpai.2019112901 # Hola Px - 我的专属精选壁纸专家 +com.appcina.anpai.2019122013 # WLPPR - background wallpapers +com.appcina.anpai.2020011202 # Berkanan Messenger +com.apprush.game.chinesechar # 汉语字典 +com.appshare.android.ilisten # 口袋故事 +com.appshare.android.ilisten.hd # 口袋故事HD +com.appsino.bingluo.fycz # 移动公证 +com.appsys.goodname # 易经起名取名 +com.Apricotforest_epocket # 医口袋 +com.artron.toutiao # 艺术头条 +com.autohome.usedcar # 二手车之家-看车选车买车卖车 +com.autonavi.minimap # 高德地图 +com.babycloud.hanju # 韩小圈 +com.babytree.apps.lama # 宝宝树小时光 +com.babytree.apps.pregnancy # 宝宝树孕育 +com.baidu.appsearch # 百度手机助手 +com.baidu.BaiduMap # 百度地图 +com.baidu.baidutranslate # 百度翻译 +com.baidu.browser.apps # 百度浏览器 +com.baidu.haokan # 好看视频 +com.baidu.hi # 如流 +com.baidu.homework # 作业帮 +com.baidu.iknow # 百度知道 +com.baidu.input # 百度输入法 +com.baidu.input_mi # 百度输入法小米版 +com.baidu.lbs.crowdapp # 地图淘金 +com.baidu.netdisk # 百度网盘 +com.baidu.newapp # 文心一言 +com.baidu.searchbox # 百度APP +com.baidu.searchbox.lite # 百度极速版 +com.baidu.tieba # 百度贴吧 +com.baidu.video # 百搜视频 +com.baidu.video.pad # 百搜视频HD +com.baidu.wenku # 百度文库 +com.baidu.youavideo # 一刻相册 +com.baidu.yuedu # 百度阅读 +com.baihe # 百合网 +com.baijiu.bizhi.sjbixiaz # Wallpaper +com.baile.shanduo # 闪聊闪约 +com.bankcomm.Bankcomm # 交通银行 +com.bankcomm.maidanba # 买单吧 +com.baojiazhijia.qichebaojia # 买车宝典 +com.baoruan.theme.mhczpffedYqofhovJAD # 古剑奇谭之风晴雪-3D桌面主题 +com.baoruan.vmiic # 主题商店 +com.baoruan.wallpaper.lmaczcgcdjTJGknirRd # 绚丽水晶-桌面壁纸 +com.baoyun.babycloud # 口袋宝宝 +com.barmak.client.fast # BARMAK维语输入法 +com.bbbao.shop.client.android.activity # 比比宝 +com.bbk.appstore # vivo应用商店 +com.bbqk.quietlycall # 免费电话宝 +com.bdatu.geography # 每日精选 +com.bench.yylc # 盈盈 +com.benqu.wuta # 无他相机 +com.bertadata.qxb # 启信宝 +com.bfire.da.nui # 微分身双开 +com.bilibili.app.blue # 哔哩哔哩概念 +com.bilibili.app.in +com.bilibili.comic # 哔哩哔哩漫画 +com.bilibili.studio # 必剪 +com.biligyar.izdax # izdax翻译 +com.bingo.yeliao # 十点聊天 +com.bjbyhd.voiceback # 保益悦听 +com.bjut.cyl.findnurse # 金牌护士 +com.black.unique # 全球购骑士特权 +com.blink.academy.nomo # NOMO CAM +com.bly.dkplat # 小X分身 +com.boc.bocsoft.phone.enterprise # 中行企业银行 +com.bokecc.dance # 糖豆 +com.boniu.saomiaoquannengwang # 扫描转文字王 +com.boohee.one # 薄荷健康 +com.booking # Booking.com缤客 +com.bowerswilkins.splice # Music Bowers and Wilkins +com.brains.byzmnew # 益智脑筋急转弯大全 +com.browser2345 # 2345浏览器 +com.btime.bjtime # 北京时间 +com.budejie.www # 百思漂流瓶 +com.businessvalue.android.app # 钛媒体 +com.by.butter.camera # 黄油相机 +com.bytedance.android.doubaoime # 豆包输入法 +com.bytedance.lark +com.bz.xiu # 壁纸秀秀 +com.bz.ziti.diy # paper壁纸大全 +com.c2vl.kgamebox # 狼人杀 +com.caijing # 财经杂志 +com.caing.news # 财新 +com.cainiao.wireless # 菜鸟 +com.caishi.cronus # 唔哩头条 +com.call.myyb # 满意网络电话 +com.campmobile.snowcamera # B612咔叽 +com.capelabs.leyou # 乐友 +com.cat.readall # 悟空浏览器 +com.cbnweekly # 第一财经杂志 +com.ccb.longjiLife # 建行生活 +com.ccgs.sms # 短信群发助手 +com.cctir.huinongbao # 惠农网 +com.cctv.caijing # 央视财经 +com.cctv.cctv5ultimate # 央视体育 +com.cctv.yangshipin.app.androidp # 央视频 +com.cdb.app # 充电加速器 +com.cdcd.milv # 觅侣 +com.cdsb.newsreader # 红星新闻 +com.cdtv.app # 看度新闻 +com.cdv.video360 # 云美摄 +com.cebbank.mobile.cemb # 光大银行 +com.ceic.app # 地震速报 +com.cffex.htqh # 海通期货期海通行 +com.cfy.xqwxt # 象棋微学堂 +com.cgbchina.xpt # 广发银行 +com.chan.cwallpaper # 图凌 +com.changba # 唱吧 +com.changshuo.ui # 畅说108 +com.chaoxing.mobile # 学习通 +com.chaozh.iReader.dj # 得间免费小说 +com.chaozh.iReaderFree # 掌阅 +com.chaozh.iReaderFree15 # 油梨 +com.chebada # 巴士管家 +com.chenai.eyes # 夜间模式 +com.chenai.ieyes # 夜间护眼 +com.chenyang.wzzyy # 文字转语音 +com.china3s.android # 春秋航空 +com.chinaamc.MainActivityAMC # 华夏基金管家 +com.chinaamc.mfbh.amcActivity # 华夏活期通 +com.chinablue.tv # Z视介 +com.chinaideal.bkclient.tabmain # 你我嘉选 +com.chinalife.ebz # 中国人寿寿险 +com.chinamobile.mcloud # 中国移动云盘 +com.chinamworld.bocmbci # 中国银行 +com.chinamworld.main # 中国建设银行 +com.chinaso.toutiao # 中国搜索 +com.chinasoft.mall # 好享购物 +com.chinatelecom.bestpayclient # 翼支付 +com.chineseall.singlebook # 中文书城 +com.chipsea.btcontrol # OKOK +com.ci123.pregnancywap # 妈妈社区 +com.cib.cibmb # 兴业银行 +com.cib.xyk # 兴业生活 +com.cio.project # 通信助手 +com.citiccard.mobilebank # 动卡空间 +com.cj.yun.esz # 云上恩施 +com.cleanmaster.mguard_cn # 猎豹清理大师 +com.cliff # 藏书馆 +com.clipzz.media # 视频剪辑精灵 +com.cloudpower.netsale.activity # 中国人保 +com.clov4r.android.nil # MoboPlayer +com.clover.daysmatter # 倒数日 +com.cmbc.cc.mbank # 全民生活 +com.cmbchina.ccd.pluto.cmbActivity # 掌上生活 +com.cmcc.cmvideo # 咪咕视频 +com.cmcc.hebao # 和包 +com.cmcm.cfwallpaper # 元气桌面壁纸 +com.cmi.jegotrip # 无忧行 +com.cmri.universalapp # 移动爱家 +com.cmschina.stock # 招商证券 +com.cmstop.zswz # 掌上温州 +com.cns.mc.activity # 中国新闻网 +com.codoon.gps # 咕咚 +com.color.by.coloring.wallpaper.android # 涂个壁纸 +com.comic.isaman # 飒漫画 +com.comic.manhuatai # 漫画台 +com.community.cape # 海角社区 +com.cooby.news # 华人头条 +com.coolapk.market # 酷安 +com.cootek.crazyreader # 疯读小说 +com.cpic.maker # 太享贷 +com.cpic.sxbxxe # 太平洋保险 +com.creditwealth.client # 宜人优选 +com.crv.wanjia # 华润万家 +com.csair.mbp # 南方航空 +com.csii.changjiang.mobilebank # 江苏长江商业银行 +com.csii.gdnx.mobilebank # 广东农信 +com.csii_qzbank # 泉州银行 +com.csztv.yyk # 看苏州 +com.ct.client # 中国电信 +com.cubic.autohome # 汽车之家 +com.cubic.choosecar # 汽车报价 +com.cutv.mobile.zsntclient # 掌上南通 +com.cxzapp.yidianling # 壹点灵 +com.cyjh.ddyun # 多多云手机 +com.dachen.medicalcircle # 医生圈 +com.dada.kdb123 # 快递短信宝 +com.dada.wallpaper # 壁纸神器 +com.dailyyoga.cn # 每日瑜伽 +com.dalongtech.cloud # 云电脑 +com.dangdang.buy2 # 当当 +com.dangdang.reader # 当当云阅读 +com.danielstudio.app.wowtu # 煎蛋 +com.daofeng.zuhaowan # 租号玩 +com.daqo.wallpaper # 多多壁纸铃声 +com.datayes.irr # 萝卜投研 +com.dazhuanjia.dcloud # 大专家 +com.ddmh.livewallpaper # 动态壁纸秀 +com.ddnapalon.calculator.gp # 计算器语音大师 +com.dealmoon.android # 北美省钱快报 +com.dedao.juvenile # 少年得到 +com.deepseek.chat # DeepSeek +com.desktop.pendant # 银河壁纸多多 +com.devuni.flashlight # 超级手电筒 +com.dewmobile.kuaiya # 快牙 +com.deyi.client # 得意生活 +com.dianping.v1 # 大众点评 +com.dida.help # 滴答滴 +com.didago.dida # 滴答滴顺风车 +com.didago.travel # 滴答滴出行 +com.didapinche.booking # 嘀嗒出行 +com.didikee.gifparser # GIF助手 +com.diershoubing.erbing # 二柄 +com.digitalgd.dgyss # 粤省事 +com.diguayouxi # 当乐 +com.dingchebao # 订车宝 +com.dingtai.wxhn.activity # 新湖南 +com.dingYZ.dingyzwater # 微脉水印相机 +com.dinoenglish.yyb # E英语宝 +com.dipiapp.pcall # 网络电话 +com.diyidan # 第一弹 +com.djbox.product # DJ音乐盒 +com.docin.zlibrary.ui.android # 豆丁书房 +com.dongfanghn.com # Only婚恋 +com.dongqiudi.news # 懂球帝 +com.dotamax.app # Max+ +com.dotools.nightcamera # 夜视相机 +com.douban.book.reader # 豆瓣阅读 +com.douban.frodo # 豆瓣 +com.douban.radio # 豆瓣FM +com.doudou.ysvideo # 影视大全韩剧 +com.douguo.recipe # 豆果美食 +com.doumi.jianzhi # 斗米 +com.dp.android.elong # 艺龙旅行 +com.dragon.read # 番茄免费小说 +com.droid.credit # 移动手机贷 +com.droidpiggy.weiqi # 围棋宝典 +com.duapps.recorder # 小熊录屏 +com.duitang.main # 堆糖 +com.duoduo.cailing # 彩铃多多 +com.duoduo.child.story # 儿歌多多 +com.duoduo.child.storyhd # 儿歌多多HD +com.duokan.phone.remotecontroller # 万能遥控 +com.duokan.reader # 多看 +com.duorong.smarttool # 时光序 +com.duowan.kiwi # 虎牙直播 +com.duowan.mobile # YY +com.dute.dutenews # 读特 +com.duwo.reading # 伴鱼绘本 +com.duxiaoman.umoney # 有钱花 +com.duxiaoman.wallet # 度小满 +com.dw.btime # 亲宝宝 +com.dx.x1myapplication # 电销外呼电话 +com.dz.hmjc # 河马剧场 +com.dzqh.online11 # 东方证券期货 +com.dzwww.sd.android # 海报新闻 +com.eastday.listen_news # 翱翔 +com.eastedge.zhuzhounews # 掌上株洲 +com.eastmoney.android.berlin # 东方财富 +com.eastmoney.android.fund # 天天基金 +com.eastmoney.android.tokyo # 妙想炒股 +com.easy.wallpaper # 番茄动态壁纸 +com.ebank.creditcard # 阳光惠生活 +com.ebnews # 亿邦 +com.edujia.weimai # 微脉圈 +com.eduyun.app.factory.ykt # 智慧中小学 +com.eg.android.AlipayGphone # 支付宝 +com.ejeas.ejeasapp # Ejeas +com.eno.android # 国海金探号 +com.eno.android.cj.page # 长江e号 +com.eno.xyzq.page # 兴业证券优理宝 +com.enotary.cloud # 公证云 +com.esbook.reader # 宜搜小说 +com.essdcbz.nyxx # 多彩壁纸 +com.estrongs.android.pop # ES文件浏览器 +com.eusoft.dehelper # 德语助手 +com.eusoft.eudic # 欧路词典 +com.eusoft.frhelper # 法语助手 +com.eusoft.ting.en # 每日英语听力 +com.eusoft.ting.fr # 每日法语听力 +com.evaair.android # 长荣航空 +com.everimaging.photoeffectstudio # Fotor +com.exam8.yijian # 一建万题库 +com.example.raccoon.dialogwidget # 万象小组件 +com.excean.gspace # OurPlay +com.excean.masaid # 微分身版 +com.excelliance.dualaid # 双开助手微分身版 +com.ezen.ehshig # egshig +com.ezg.camper # 约你游 +com.faloo.BookReader4Android # 飞卢小说 +com.fanhuan # 返还网 +com.fanli.android.apps # 返利 +com.fanlwgaoylmengw # 返利高佣联盟 +com.fbx.handwriteime # 手写输入法 +com.fcbox.hivebox # 丰巢管家 +com.fcbox.hiveconsumer # 丰巢 +com.feheadline.news # 财经头条 +com.feiteng.lieyou # 猎游 +com.fenbi.android.gaozhong # 猿题库 +com.fenbi.android.leo # 小猿AI +com.fenbi.android.servant # 粉笔 +com.fenbi.android.solar # 小猿搜题 +com.fenbi.android.zenglish # 斑马AI学 +com.fengbo.live # 幻梦直播 +com.fengnanwlkj.gongxiangApp # 共享货源 +com.fenqile.fenqile # 分期乐 +com.fiberhome.exmobi.client.gaeaclientandroidedn7262 # 天然工坊 +com.fimo.camera # FIMO-复古胶片相机 +com.financial.quantgroup # 羊小咩 +com.fivehundredpx.viewer.main # 500px中国版 +com.flapp.wanzi # 代练丸子 +com.flash.zhongcao # 种草生活 +com.flightmanager.view # 航班管家 +com.flybbj.remotecontrol # 空调手机遥控器 +com.flyersoft.seekbooks # 搜书大师 +com.focusgene.findme # 真我 +com.font # 写字先生 +com.fosung.lighthouse # 灯塔党建在线 +com.foundao.bjnews # 新京报 +com.founder.qujing # 掌上曲靖 +com.founder.shihuabao # 新石化 +com.founder.zhanjiang # 湛江云媒 +com.foundersc.app.xf # 小方 +com.foxit.mobile.pdf.lite # 福昕PDF阅读器 +com.fq.wallpaper # 飞火动态壁纸 +com.fqapps.fdsh # 福袋生活 +com.free_ride.yiwei # 顺风车 +com.free_ride_pin.yiwei # 汽车票 +com.freebird.zsp.DailyAccounting # 日常记账 +com.freeme.lockscreen.levect # 杂志锁屏 +com.frozenape.tempo # 移动节拍器 Metronome Tempo +com.fullgoal.android # 富国富钱包 +com.funcamerastudio.videomaker # Filmigo视频剪辑制作软件 +com.funinput.digit # 数字尾巴 +com.funshion.video.mobile # 风行视频 +com.fx678.finace # 汇通财经 +com.galaxy.metawp # 元壁纸 +com.galaxy.stock # 中国银河证券 +com.gameabc.zhanqiAndroid # 战旗直播 +com.gamestar.perfectpiano # 完美钢琴 +com.gamestar.pianoperfect # 随身乐队 +com.ganji.android # 赶集招聘 +com.ganji.android.haoche_c # 瓜子二手车 +com.gaoping.makewallpaper # 动态壁纸制作 +com.gaotu100.superclass # 高途 +com.gator.mm # 美女图集大全 +com.gau.go.launcherex.theme.droidglass # 玻璃主题包 +com.gbcom.gwifi.school # GiWiFi校园助手 +com.gdwx.cnwest # 陕西头条 +com.geihui # 给惠网 +com.gelonghui.glhapp # 格隆汇 +com.gem.tastyfood # 食行生鲜 +com.gewara # 格瓦拉生活 +com.gf.client # 广发易淘金 +com.gfqh.guangfa # 广发期货小添财-投资交易软件 +com.ggeye.jiakao.api # 驾考通驾照考试 +com.gh.gamecenter # 光环助手 +com.gift.android # 驴妈妈旅游 +com.giiso.jinantimes # 新黄河 +com.gioct.weather # 天气预报 +com.glodon.drawingexplorer # CAD快速看图 +com.gome.eshopnew # 国美 +com.gomemyc.mylc.android # 国美金融 +com.gongyinruixin # 工银瑞信基金 +com.gonlan.iplaymtg # 旅法师营地 +com.goodtech.tq # 更云天气预报 +com.gorgeous.lite # 轻颜 +com.goseet.VidTrim # 视频剪辑 +com.gotokeep.keep # Keep - AI 运动教练 +com.gozap.chouti # 抽屉 +com.greenline.guahao # 微医 +com.greenpoint.android.mc10086.activity # 中国移动 +com.greentree.android # 格林 +com.growing.topwidgets # 万能小组件 Top Widgets +com.grunkr.autowallpaper # 自动壁纸 +com.gstarmc.android # CAD看图王 +com.gtgj.view # 高铁管家 +com.guagua.finance # 呱呱财经 +com.guagua.qiqi # 齐齐直播 +com.guangdaqihuo.mhdxh # 光大期货 +com.guosen.android # 国信金太阳 +com.guotai.dazhihui # 国泰海通君弘 +com.gwchina.lssw.parent # 青葱守护家长 +com.gwdang.app # 购物党 +com.gwsoft.imusic.controller # 爱音乐 +com.gzfns.ecar # 车e估企业版 +com.gzrcb.mobilebank # 广州农商银行 +com.haidilao # 海底捞 +com.haier.uhome.uplus # 海尔智家 +com.haiqiu.jihai # 即嗨体育 +com.haitao # 55海淘 +com.haixiu.ui # 嗨秀直播 +com.haizhengqihuo.mhdxh # 上海证券期货 +com.handsgo.jiakao.android # 驾考宝典 +com.handsgo.jiakao.android.kehuo # 教练宝典 +com.hangzhoucaimi.financial # 挖财宝 +com.hanweb.android.sdzwfw.activity # 爱山东 +com.hanweb.android.zhejiang.activity # 浙里办 +com.haodf.android # 好大夫在线 +com.happigo.activity # 快乐购 +com.happylife.multimedia.image # 相片集 +com.happyteam.dubbingshow # 配音秀 +com.hazq.huiying.android # 华安证券 +com.hbrb.jiyun # 冀云 +com.hcrs # 华彩人生 +com.hcyh.scanner # 扫描仪全能王 +com.heiyan.reader # 黑岩阅读 +com.hexin.android.bank # 同花顺爱基金 +com.hexin.android.stockassistant # 同花顺i问财 +com.hexin.ifind.android # 同花顺iFinD +com.hexin.plat.android # 同花顺 +com.hexin.plat.android.CaitongSecurity # 财通证券 +com.hexin.plat.android.GuoJinZXGSecurity # 全能行 +com.hexin.plat.android.WanlianSecurity # 万联e万通 +com.hexin.plat.android.ZhongyuanSecurity # 中原证券 +com.hexun.news # 和讯财经 +com.heytap.market # OPPO软件商店 +com.hf # 中国天气 +com.hh.wallpaper.a # 全民免费壁纸 +com.hichip # CamHi +com.hicorenational.antifraud # 国家反诈中心 +com.hihonor.appmarket # 荣耀应用市场 +com.hipu.yidian # 一点资讯 +com.hisense.ms.fly2tv # 海信爱家 +com.hjkj66.fakecall # 虚拟电话 +com.hjwordgames # 沪江开心词场 +com.hkairlines.apps # 香港航空 +com.hkx.youandme # 情侣我和你 +com.hlg.daydaytobusiness # 稿定设计 +com.hmkx.zgjkj # 健康界 +com.hnzx.hnrb # 河南日报 +com.hnzycfc.zyxj # 中原消费金融 +com.hoge.android.chinablue # 中国蓝新闻 +com.hoge.android.wuxiwireless # 无锡博报 +com.hoge.android.yancwireless # 智慧盐城 +com.homelink.android # 链家 +com.honeymoon.stone.jean.poweredit # 图片编辑工具 +com.honghesoft.calligrapher # 大书法家 +com.hongju.aishang # 情趣店 +com.HotelZhiDing.zhiding365 # 直订 +com.hp.android.printservice # HP 打印服务插件 +com.hp.marykay # 玫琳凯之窗 +com.hpbr.bosszhipin # BOSS直聘 +com.hpplay.happycast # 乐播投屏 +com.hqf.app.yuanqi # 趣玩桌面壁纸 +com.hrz.quick # 红人装极速版 +com.htffund.mobile.ec.ui # 汇添富现金宝 +com.htinns # 华住会 +com.huaban.android # 花瓣 +com.huajiao # 花椒直播 +com.hualongxiang # 化龙巷 +com.huankaifa.tpjwz # 图片加文字 +com.huanqiu.news # 环球时报 +com.huati # 葫芦侠3楼 +com.huawei.appmarket # 华为应用市场 +com.huawei.hwid # HMS Core +com.huawei.intelligent # 智慧助手·今天 +com.huawei.search # 智慧搜索 +com.huawei.smarthome # 智慧生活 +com.huicao.stbg # 主题秀秀 +com.huimai365 # 惠买 +com.huitouche.android.app # 省省货主 +com.hujiang.dict # 沪江小D词典 +com.hujiang.hjclass # 沪江网校 +com.huluxia.gametools # 葫芦侠 +com.hunantv.imgo.activity # 芒果TV +com.hundsun.stockwinner.cyqh # 创元期货 +com.hundsun.stockwinner.gdzq # 光大证券金阳光 +com.hundsun.stockwinner.paqh # 平安期货 +com.hundsun.stockwinner.yhqh # 银河期货专业版 +com.hundsun.winner.pazq # 平安证券 +com.huodao.hdphone # 找靓机 +com.hupu.games # 虎扑 +com.hupu.shihuo # 识货 +com.huxiu # 虎嗅 +com.huxiupro # 妙投 +com.huyun.ybb # 运帮帮 +com.hw.okm # 二三里 +com.hx.mall # 华西证券益理财 +com.hxak.liangongbao # 链工宝 +com.hybunion.miaodao # 会员宝秒到版 +com.hyhy.view # 掌上天津 +com.hzqj.callshow.yuedong # 悦动来电 +com.i61.draw.live # 画啦啦美术课堂 +com.ibike.publicbicycle.activity # 永安行 +com.ibox.calculators # 多多计算器 +com.ibox.flashlight # 多多手电筒 +com.iboxpay.minicashbox # 钱盒商户通 +com.icbc # 中国工商银行 +com.icbc.elife # 工银e生活 +com.icbc.emallmobile # 融e购 +com.ichano.athome.avs # 掌上看家采集端 +com.ichano.athome.camera # 掌上看家观看端 +com.ichinait.gbpassenger # 首汽约车 +com.iCitySuzhou.suzhou001 # 引力播 +com.icoolme.android.weather # 最美天气 +com.id10000 # 智企 +com.idaodan.clean.master # 雪豹速清 +com.idotools.rings # 多多铃声 +com.ifanr.activitys # 爱范儿 +com.ifeimo.audiorecord # 录音大师 +com.ifeimo.videoeditor # 视频剪辑大师 +com.ifeng.android # 翻阅小说 +com.ifeng.fhdt # 凤凰FM +com.ifeng.news2 # 凤凰新闻 +com.ifeng.newvideo # 凤凰视频 +com.ifext.news # 凤凰新闻极速版 +com.iflytek.elpmobile.smartlearning # 智学网 +com.iflytek.inputmethod # 讯飞输入法 +com.iflytek.ringdiyclient # 酷音铃声 +com.iflytek.womusicclient # 联通视频彩铃 +com.ifreetalk.ftalk # 派派 +com.ihaozhuo.youjiankang # 优健康 +com.ihg.apps.android # 洲际优悦会 +com.ihope.hbdt # 冀时新闻 +com.ihuman.recite # 万词王 +com.imaginer.yunji # 云集 +com.imendon.cococam # 可可修图 +com.imendon.fomz # Fomz +com.imendon.lovelycolor # 少女与猫 +com.imendon.painterspace # 画师之境 +com.imgo.pad # 芒果TV HD +com.immomo.momo # MOMO陌陌 +com.imusic.iting # 爱听 +com.inditex.zara # ZARA +com.infinitus.bupm # 无限极 +com.infothinker.gzmetro # 广州地铁 +com.infzm.ireader # 南方周末 +com.insurance.agency # 亲亲小保 +com.intsig.BizCardReader # 名片全能王 +com.intsig.camscanner # 扫描全能王 +com.iodkols.onekeylockscreen # 一键锁屏 +com.iqilu.ksd # 闪电新闻 +com.iqinbao.android.songs # 亲宝儿歌 +com.iqiyi.acg # 爱奇艺叭嗒 +com.iqiyi.ivrcinema.cb # 爱奇艺VR +com.iqiyi.qixiu # 奇秀 +com.irongroup # 电话时间统计 +com.ishansong # 闪送员 +com.ishowedu.child.peiyin # 少儿趣配音 +com.ishowedu.peiyin # 英语趣配音 +com.ishugui # 点众快看小说 +com.istone.activity # 邦购商城 +com.itcode.reader # 漫漫漫画 +com.iyuba.AE # 美语怎么说 +com.iyuba.cet4 # 英语四级听力 +com.iyuba.CSvoa # VOA常速英语 +com.iyuba.music # 听歌学英语 +com.iyuba.voa # VOA慢速英语 +com.iznet.thailandtong # 三毛游 +com.jd.healthy.daily # 人民日报健康 +com.jd.jdlite # 京东特价版 +com.jd.jrapp # 京东金融 +com.jeejen.family # 极简桌面 +com.jesson.meishi # 美食杰家常菜谱大全 +com.jf.lkrj # 花生日记 +com.jhdapp.xhbycm # 交汇点新闻 +com.jhss.youguu # 优顾炒股 +com.jiadi.quannengsaomiaowang # 合同扫描王 +com.jianyou.swatch # 智能守护5 +com.jianzhiku.zhongrenbang # 众人帮 +com.jiaoyinbrother.monkeyking # 悟空租车 +com.jiayuan # 世纪佳缘 +com.jie.lockscreen # 锁屏大全 +com.jiemian.news # 界面新闻 +com.jifen.qukan # 趣头条 +com.jiliguala.niuwa # 叽里呱啦 +com.jingdian.tianxiameishi.android # 美食天下 +com.jingdong.app.mall # 京东 +com.jingdong.pdj # 京东到家 +com.jingyao.easybike # 哈啰 +com.jingzhou # 荆州社区 +com.jinxin.namibox # 纳米盒 +com.jiongji.andriod.card # 百词斩 +com.jiuxianapk.ui # 酒仙 +com.jiuyan.infashion # in +com.jjwxc.reader # 晋江小说阅读 +com.jm.fxw # 发型屋 +com.job.android # 前程无忧51Job +com.joey.fui # 一幅相框 +com.join.android.app.mgsim.wufun # 悟饭游戏厅 +com.jojoread.huiben # 叫叫绘本 +com.jooan.qiaoanzhilian # 乔安智联 +com.jovetech.CloudSee.temp # 云视通 +com.joygo # 忘忧围棋 +com.joyintech.wise.seller # 智慧商贸进销存 +com.jsbc.lznews # 荔枝新闻 +com.jsmcc # 中国移动江苏 +com.jtjr99.jiayoubao # 加油宝 +com.juejinchain.android # 掘金宝 +com.julanling.app # 安心记加班 +com.juronghm.wallpaper # 神图壁纸主题大全 +com.jxedt # 驾校一点通 +com.jy.recorder # 录屏精灵 +com.jyyx.fuli # 九妖游戏 +com.jz.androidacts.superiscreen # xScreen超级桌面 +com.jzyd.coupon # 省钱快报 +com.jzzs.ParentsHelper # 安全教育平台 +com.kankanews.kankanxinwen # 看看新闻 +com.kekeclient_ # 可可英语 +com.kevinstudio.test # 转盘做决定 +com.keyue.widget # 百变桌面小组件 +com.Kingdee.Express # 快递100 +com.kingsoft # 金山词霸 +com.kmxs.reader # 七猫免费小说 +com.knowbox.rc.student.pk # 小盒AI学 +com.komoxo.octopusime # 章鱼输入法 +com.koudai.weidian.buyer # 微店 +com.koudai.weishop # 微店店长版 +com.ktls.fileinfo # 存储空间清理 +com.kuaiduizuoye.scan # 快对AI +com.kuaikan.comic # 快看漫画 +com.kuaishou.nebula # 快手极速版 +com.kuangxiang.novel # 欢乐书客 +com.kuangxiangciweimao.novel # 刺猬猫阅读 +com.kugou.android # 酷狗音乐 +com.kugou.android.elder # 酷狗免费版 +com.kugou.android.ktvapp # 酷狗唱唱 +com.kugou.android.lite # 酷狗概念版 +com.kugou.android.ringtone # 酷狗铃声 +com.kugou.fanxing # 酷狗直播 +com.kugou.playerHD # 酷狗音乐HD +com.kunpeng.babyting # 宝贝听听 +com.kunpeng.bluefont # 字体下载极速版 +com.kunpeng.zitice # 字体大全 +com.kwai.hisense # 回森 +com.kwai.livepartner # 快手直播伴侣 +com.kwai.m2u # 一甜相机 +com.kwai.thanos # 噗叽 +com.kwai.videoeditor # 快影 +com.lakala.android # 拉卡拉易分期 +com.lalamove.huolala.client # 货拉拉 +com.lanjinger.choiassociatedpress # 财联社 +com.lanjingren.ivwen # 美篇 +com.laoyouzhibo.app # 老柚 +com.laoyuegou.android # 捞月狗 +com.larus.nova # 豆包 +com.lchr.diaoyu # 钓鱼人 +com.lcworld.haiwainet # 海客新闻 +com.ld.dianquan # 雷电云手机 +com.le123.ysdq # 影视大全 +com.lechuan.mdwz # 米读极速版 +com.lechuan.midunovel # 米读小说 +com.lehai.ui # 乐嗨直播 +com.leisu.sports # 雷速体育 +com.leixun.taofen8 # 淘粉吧 +com.lemon.faceu # Faceu激萌 +com.lemon.lv # 剪映 +com.leqi.idpicture # 智能证件照 +com.leqi.ProfessionalIDPhoto # 专业证件照 +com.letv.android.client # 乐视视频 +com.letv.android.client.pad # 乐视视频Pad +com.lezhi.mythcall # 北瓜电话 +com.lft.turn # 导学号 +com.lgzgykc.xddt # 熊多多 +com.lianaibiji.dev # 恋爱记 +com.lianjia.beike # 贝壳找房 +com.lianmengsylm.app # 联盟生态 +com.liba.android # 篱笆社区 +com.lietou.mishu # 猎聘 +com.linecorp.foodcamcn.android # Foodie +com.lingan.seeyou # 美柚 +com.lingan.yunqi # 宝宝记 +com.lingcreate.speechtotext # 音频转文字 +com.lingyue.zebraloan # 洋钱罐借款 +com.linkiing.show # 联客show +com.lion.market # 虫虫助手 +com.liulishuo.engzo # 流利说-英语 +com.ljmobile.zlj.rom.release # 授权管理软件搬家 +com.llspace.pupu # 平行世界 +com.lltskb.lltskb # 路路通 +com.lofter.android # 网易LOFTER +com.lolaage.tbulu.tools # 两步路 +com.lovebizhi.wallpaper # 爱壁纸 +com.lphtsccft # 涨乐财富通 +com.lqh.love.comic # 爱动漫壁纸 +com.ls.sjdtbz # 手机动态壁纸 +com.lt.lighting # 来电闪光 +com.lu.ashionweather # 天气预报 +com.ludashi.benchmark # 鲁大师 +com.luna.music # 汽水音乐 +com.luojilab.player # 得到 +com.luoyu.gangqin # 真实钢琴 +com.luoyu.shichanglianer # 视唱练耳 +com.luyun.diary # 每日记 +com.luyun.lightnote # 轻记事本 +com.luyun.pinkdiary # 粉萌日记 +com.lvshiguan.app # 律师馆法律咨询 +com.lxkj.dmhw # 呆萌价 +com.lyh.talkphoto # 让照片会说话 +com.lyx.piano # 钢琴节奏键盘大师 +com.lz.mycall # 千千电话 +com.m4399.gamecenter # 4399游戏盒 +com.macrovideo.v380 # V380 +com.magicpoint.mobile.bank # 重庆农商行 +com.maihan.tredian # 淘最热点 +com.maijun.inputmethod # 语音输入法 +com.maipu.tourist # 小鹿导游 +com.maka.app # MAKA设计 +com.manmanbuy.bijia # 慢慢买 +com.market2345 # 2345手机助手 +com.marqio.widget.keeplighting # 屏幕常亮 +com.marryu # MarryU相亲交友 +com.max.xiaoheihe # 小黑盒 +com.maxxipoint.android # 集享联盟 +com.mayi.android.shortrent # 蚂蚁短租 +com.mcdonalds.gma.cn # 麦当劳 +com.mediabox.voicechanger # 安卓变声器 +com.meelive.ingkee # 映客直播 +com.meet.pianolearn # 钢琴教练 +com.meetstudio.app.Zither # 魔幻古筝 +com.meijialove.activity # 美甲帮 +com.meitianmeihuimtmh.app # 喵小盒 +com.meitu.makeup # 美妆相机 +com.meitu.meipaimv # 美拍 +com.meitu.meiyancamera # 美颜相机 +com.meitu.poster # 美图设计室 +com.meitu.wheecam # 潮自拍 +com.meitu.wink # Wink +com.melot.meshow # KK +com.memory.me # 英语魔方秀 +com.mendon.riza # 日杂相机 +com.messi.cantonese.study # 粤语说 +com.messi.languagehelper # 中英互译 +com.meta.box # 233乐园 +com.mfw.roadbook # 马蜂窝 +com.mgyun.rootmaster # ROOT大师 +com.mi.health # 小米运动健康 +com.mianfeia.book # 爱看书免费小说 +com.midea.ai.appliances # 美的美居 +com.midea.vm.washer # U净 +com.mifeng.ui # 蜜疯直播 +com.mihoyo.hyperion # 米游社 +com.mimikko.mimikkoui # 兽耳助手 +com.minitools.miniwidget # 心动主题壁纸 +com.mishangwo # 迷上我 +com.mistong.ewt360 # 升学e网通 +com.mm.android.lc # 乐橙 +com.mmbox.xbrowser.pro # X浏览器 +com.mmote.play # 买萌陪玩 +com.mobile.myeye # 监控眼 +com.MobileTicket # 铁路12306 +com.mobivans.onestrokecharge # 预记智能账本 +com.mobvoi.baiding # 出门问问 +com.mogujie # 蘑菇街 +com.moji.mjweather # 墨迹天气 +com.moneywise.mhmoney # 财智快账 +com.moonlight.huatu # 画画图 +com.moonshot.kimichat # Kimi +com.mosheng # 爱聊 +com.motoband # 摩托邦 +com.moutian.moutianshuiyinwang # 水印王 +com.mp.qcoperate # 红旗e岗通 +com.mrkj.sm # 知命 +com.msg.schedule # 定时信息 +com.msxf.ayh # 安逸花 +com.mt.mtxx.mtxx # 美图秀秀-视频/图片/Live人像精修工具 +com.mtime # 时光网 +com.music.qishui # 汽水铃声 +com.mx.browser # 傲游浏览器 +com.my.peiyin # 配音文字转语音 +com.mybank.android.phone # 网商银行 +com.mygolbs.mybus # 掌上公交 +com.mygolbs.mybuswz # 我看行 +com.mymoney # 随手记 +com.mymoney.sms # 卡牛信用管家 +com.mysteel.android.steelphone # 我的钢铁 +com.myzaker.ZAKER_Phone # ZAKER +com.n_add.android # 粉象生活 +com.nacai.bocai # 花花直播 +com.nade.ownerride # 顺风车司机版 +com.nade.tickowner # 滴答滴出行司机端 +com.naiyoubz.main # 奶油桌面 +com.nandu # 南都N视频 +com.nanyibang.nomi # 男衣邦穿衣搭配 +com.nanyibang.rm # 穿衣有品 +com.NationalPhotograpy.weishoot # 微摄 +com.naver.linewebtoon.cn # 咚漫漫画 +com.nbbank # 宁波银行 +com.ncpaclassic # 大剧院·古典 +com.nd.android.pandareader # 熊猫看书 +com.neil # 返利助手 +com.net263.ecm.display # 263移动会议 +com.netease.a13.avg # 易次元 +com.netease.buff # 网易BUFF +com.netease.cloudmusic # 网易云音乐 +com.netease.edu.ucmooc # 中国大学MOOC +com.netease.epay # 网易支付 +com.netease.gl # 网易大神 +com.netease.mail # 网易邮箱大师 +com.netease.mkey # 网易将军令 +com.netease.mobimail # 网易邮箱 +com.netease.newsreader.activity # 网易新闻 +com.netease.pris # 网易云阅读 +com.netease.uu # UU加速器 +com.netease.vopen # 网易公开课 +com.netease.xyqcbg # 梦幻藏宝阁 +com.netease.yanxuan # 网易严选 +com.neusoft.app.beijingdaily.phone # 北京日报 +com.newcapec.mobile.ncp # 完美校园 +com.news.shangyounews # 上游新闻 +com.nfdaily.nfplus # 南方Plus +com.nice.main # nice +com.nillu.kuaiqu.ui # 快去水印 +com.nineteenlou.nineteenlou # 19楼 +com.nineton.handwriteime # 友友输入法 +com.ninexiu.sixninexiu # 九秀直播 +com.niuguwang.stock # 牛股王股票 +com.niwodai.universityloan # 你我贷借款 +com.njh.biubiu # biubiu加速器 +com.njmizhilan.wallpaper # 神图主题 +com.njwry.zhuochong # 桌面宠物多多 +com.novv.resshare # 彩蛋视频壁纸 +com.nowcasting.activity +com.nxy.sc # 四川农信手机银行 +com.ocj.oms.mobile # 东方购物 +com.octinn.birthdayplus # 生日管家 +com.oda_cad # CAD迷你看图 +com.oeiskd.easysoftkey # 小白点虚拟按键 +com.offcn.android.offcn # 中公教育 +com.okcam.ookcam # 小鹰看看 +com.okmyapp.liuying # 留影音乐相册 +com.One.WoodenLetter # 一个木函 +com.onesoft.app.Tiiku.Duia.KJZ # 会计随身学 +com.onlytongchengzh.com # 两性同城聊 +com.ophone.reader.ui # 咪咕悦看 +com.oppo.market # OPPO应用商店 +com.oppo.store # OPPO商城 +com.oray.sunlogin # 向日葵远程控制 +com.oupeng.browser # 欧朋浏览器极速版 +com.oupeng.mini.android # 欧朋浏览器 +com.ovital.ovitalMap # 奥维互动地图 +com.owqsic.dwtihfwexq # pubg tool +com.p1.mobile.putong # 探探 +com.pa.health # 平安乐健康 +com.paem # 平安融易 +com.paic.yl.health # 好福利 +com.paic.zhifu.wallet.activity # 壹钱包 +com.parallel.odyssey # 猫箱 +com.pcncn.jj # 作业精灵 +com.pdmi.guangzhou.media # 广州日报新花城 +com.pdmi.studio.newmedia.people.video # 人民视频 +com.people.health.doctor # 人民好医生 +com.peopledailychina.activity # 人民日报 +com.perracolabs.tcc # Tiny Call Confirm +com.phoenix.read # 红果免费短剧 +com.phone.show # 秀来电 +com.piaopiao.idphoto # 证件照随拍 +com.picooc # PICOOC +com.pingan.carowner # 平安好车主 +com.pingan.lifeinsurance # 平安金管家 +com.pingan.paces.ccms # 平安口袋银行 +com.pingdiguo.tuner # 乐器调音器 +com.pinguo.edit.sdk # MIX滤镜大师 +com.pixelorange.filmroll # 最后一卷胶片 +com.pla.daily # 解放军报 +com.popalm.duizhuang # 对庄翡翠 +com.ppdai.loan # 拍拍贷借款 +com.pplive.androidphone # PP视频 +com.pplive.androidphone.sport # PP体育 +com.prinics.kodak.photoprinter # KODAK Photo Printer +com.prompt.lines # 提词器大师 +com.pupumall.customer # 朴朴超市 +com.pzds.pzds # 盼之账号 +com.qb.qtranslator # 腾讯翻译君 +com.qclianai.com # 全城恋爱 +com.qdnews.qd # 青岛新闻 +com.qdruotu.app # 若途旅行 +com.qianyouyu # 钱有余 +com.qiaola.desktop # 主题壁纸美化 +com.qicai.contacts # 西瓜通讯录 +com.qicai.translate # 出国翻译官 +com.qidian.QDReader # 起点读书 +com.qihoo.appstore # 360手机助手 +com.qihoo.browser # 360浏览器 +com.qihoo.camera # 360摄像机 +com.qihoo.cleandroid_cn # 360清理大师 +com.qihoo.dr # 360行车助手 +com.qihoo.freewifi # 360免费WiFi +com.qihoo.loan # 奇富借条 +com.qihoo.qeditor # 快剪辑 +com.qihoo360.antilostwatch # 360儿童卫士 +com.qihoo360.mobilesafe # 360手机卫士 +com.qihuo.changjiang # 长江期货开户交易 +com.qihuo.guangda # 光大期货e开户 +com.qihuo.yinhe # 银河期货开户交易 +com.qiku.os.show # 欧思奇主题 +com.qilesoft.en.eights # 英语口语8000句 +com.qima.kdt # 有赞微商城 +com.qimingzz.qm # 宝宝起名取名 +com.qinhe.ispeak # IS语音 +com.qinqinxiong.apps.dj99 # DJ99 +com.qiouou.app # 七鸥鸥 +com.qisi.freepaper # 壁纸桌面大全 +com.qisi.wallpaper # 壁纸主题大全 +com.qiyi.video # 爱奇艺 +com.qiyi.video.child # 爱奇艺奇巴布 +com.qiyi.video.lite # 爱奇艺极速版 +com.qiyi.video.pad # 爱奇艺PAD +com.qiyi.video.reader # 爱奇艺小说 +com.qlscupgrade # 中泰齐富通 +com.qnap.qremote # Qremote +com.qq.ac.android # 腾讯动漫 +com.qq.qcloud # 腾讯微云 +com.qq.reader # QQ阅读 +com.qsmy.walkmonkey # 步多多 +com.qtx.qtxty # 球天下体育 +com.quanleimu.activity # 百姓网 +com.quark.browser # 夸克 +com.Qunar # 去哪儿旅行 +com.quvideo.slideplus # 小影记 +com.quvideo.xiaoying # 小影 +com.qyer.android.jinnang # 穷游 +com.qzone # QQ空间 +com.raiyi.wxcs # 流量掌厅 +com.rakutec.android.iweekly # iWeekly都市潮力 +com.rarlab.rar # RAR +com.redfinger.app # 红手指云手机 +com.redrcd.ylxf # 云岭先锋 +com.renren.mobile.android # 人人直播 +com.renrendai.haohuan # 好分期 +com.renxing.xys # 性用社 +com.rhythm.dance.gz # 古筝世界 +com.roamingsoft.manager # WiFi连接管理器 +com.rong.fastloan # 时光分期 +com.rong360.app # 融360 +com.ruanmei.ithome # IT之家 +com.rumtel.mobiletv # 影评大全看看 +com.runbey.ybjk # 元贝驾考 +com.sainti.eduread # 出口成章 +com.samoyed.credit # 省呗 +com.samsung.android.app.watchmanager # 三星智能穿戴 +com.sand.airdroid # AirDroid +com.sangame.jjhcps.android # 金投网 +com.sangdh # 3G网络电话 +com.sankuai.meituan # 美团 +com.sankuai.meituan.dispatch.crowdsource # 美团众包 +com.sankuai.meituan.meituanwaimaibusiness # 美团外卖商家版 +com.sankuai.meituan.merchant # 美团经营宝 +com.sankuai.meituan.takeoutnew # 美团外卖 +com.sankuai.movie # 猫眼 +com.sanmu.liaoliaoba # 聊聊吧 +com.sanya.xinxiulive # Bei贝 +com.sanya.xinxiulive.lite # bei world +com.sbds.miaoyin # 秒音 +com.scanning # 二维码与条形码 +com.screeclibinvoke # 录屏大师 +com.sdu.didi.gsui # 滴滴车主 +com.sdu.didi.psnger # 滴滴 +com.sec.android.app.shealth # 三星健康 +com.sec.android.easyMover # S 换机助手 +com.secoo # 寺库奢侈品 +com.seebaby # 掌通家园 +com.seetong.app.seetong # Seetong +com.seewo.easicare.pro # 班级优化大师 +com.sender.yiwei # 一喂宠物托运 +com.seven.two.zero.yun # 720云 +com.sevenmmobile # 7M足球实时比分 +com.sevenplus.chinascience # 科普中国 +com.sf.activity # 顺丰速运 +com.sfacg # 菠萝包轻小说 +com.sgcc.wsgw.cn # 网上国网 +com.shabro.android.activity # 沙师弟司机 +com.shanbay.sentence # 扇贝单词英语版 +com.shangbiao.activity # 尚标商标转让 +com.shangbiao.searchsb86 # 商标注册查询 +com.shangfang.gylm # 高佣联盟 +com.shangshaban.zhaopin.activity # 上啥班 +com.shanxidaily.activity # 山西日报 +com.shark.jizhang # 鲨鱼记账 +com.shenghuoquan # 生活圈 +com.shengmi.photo # 私密Key应用助手 +com.shensu.jmb # 金苗宝 +com.shenyuan.militarynews # 环球新军事 +com.shfy.voice # 和平游戏变声器 +com.shilv.yuejiao # 越她 +com.shilv.yueliao # 越聊 +com.shinyv.cnr # 云听 +com.shiyue.fensigou # 粉丝福利购 +com.shizhuang.duapp # 得物 +com.shoujiduoduo.dj # DJ多多 +com.shoujiduoduo.ringtone # 铃声多多 +com.shoujiduoduo.wallpaper # 壁纸多多 +com.shouxiu.tcjy # 乡遇 +com.show.piccollagemaker # 相片拼图 +com.showself.ui # 秀色直播 +com.shuangyingh.qpyyefeu # 汽泡音乐 +com.shuhekeji # 还呗 +com.shuidihuzhu.aixinchou # 水滴筹 +com.shuiyinbao.android # 水印宝 +com.shulantech.video # 视频转换压缩 +com.shunfengche.ride # 拼客顺风车 +com.shunlian.app # 顺联动力 +com.shuqi.controller # 书旗小说 +com.shuqu.banyan # 榕树贷款 +com.shuyi.csdj # 熊猫DJ +com.shyl.train # 全国火车票实时查 +com.shyltts # 读短信来电报姓名 +com.shyz.toutiao # 清理大师 +com.sichuanol.cbgc # 川观新闻 +com.sina.news # 新浪新闻 +com.sina.VDisk # 微盘 +com.sina.weibo # 微博 +com.sina.weibog3 # 新浪微博4G版 +com.sina.weibolite # 微博极速版 +com.sing.client # 5sing音乐 +com.sinosoft.mobilebiz.chinalife # 中国人寿综合金融 +com.sinovatech.gxmobile.ui # 中国移动广西 +com.sinovatech.unicom.ui # 中国联通 +com.sinyee.babybus.abc # 宝宝巴士英语 +com.sinyee.babybus.amusement # 奇妙科普馆 +com.sinyee.babybus.babydrawing # 宝宝画妈妈猜 +com.sinyee.babybus.babyhospital # 宝宝医院 +com.sinyee.babybus.chants # 宝宝巴士儿歌 +com.sinyee.babybus.chef # 宝宝小厨房 +com.sinyee.babybus.dailycommodities # 宝宝认日用品 +com.sinyee.babybus.dining # 宝宝爱吃饭 +com.sinyee.babybus.drinks # 宝宝甜品店 +com.sinyee.babybus.duibi # 有趣的对比 +com.sinyee.babybus.farm # 奇妙数字农场 +com.sinyee.babybus.findfriendsII # 宝宝玩配对 +com.sinyee.babybus.food # 中华美食 +com.sinyee.babybus.foodstreet # 宝宝美食街 +com.sinyee.babybus.kindergarten # 宝宝欢乐教室 +com.sinyee.babybus.lovehealthy # 宝宝爱卫生 +com.sinyee.babybus.magichouse # 宝宝调色屋 +com.sinyee.babybus.number # 宝宝玩数字 +com.sinyee.babybus.organized # 宝宝大扫除 +com.sinyee.babybus.puzzle # 宝宝认知大全 +com.sinyee.babybus.recommendapp # 宝宝巴士 +com.sinyee.babybus.season # 宝宝缤纷四季 +com.sinyee.babybus.seaworld # 宝宝钓鱼 +com.sinyee.babybus.shopping # 宝宝超市 +com.sinyee.babybus.show # 宝宝服装秀 +com.sinyee.babybus.talk2kiki # 宝宝巴士奇妙屋 +com.sinyee.babybus.toilet # 宝宝上厕所 +com.sinyee.babybus.travelsafety # 宝宝出行安全 +com.sinyee.babybus.vegetable # 宝宝爱蔬菜 +com.sinyee.education.shape # 宝宝玩形状 +com.site.maigoo # 买购十大品牌排行榜 +com.sitech.rhtx # 易信 +com.sixrpg.opalyer # 橙光阅读器 +com.sjz.cl.smspopup # 短信弹窗 +com.skp.adf.photopunch # 抠图神手 +com.sktq.weather # 实况天气预报 +com.skyplatanus.crucio # 快点阅读 +com.slanissue.apps.mobile.erge # 贝瓦儿歌 +com.sloth.removelogo # 视频去水印全能王 +com.smallyin.musiceditor # 音乐剪辑助手 +com.smile.gifmaker # 快手 +com.smsf.iwallpaper # 壁纸自定义 +com.smzdm.client.android # 什么值得买 +com.snda.lantern.wifilocating # WiFi万能钥匙极速版 +com.snda.wifilocating # WiFi万能钥匙 +com.sohu.infonews # 搜狐资讯 +com.sohu.inputmethod.sogou # 搜狗输入法 +com.sohu.mail.client.cordova # 搜狐邮箱 +com.sohu.newsclient # 搜狐新闻 +com.sohu.sohuvideo # 搜狐视频 +com.sohu.tv # 搜狐视频Pad +com.songguoyhqw # 赚钱联盟 +com.sony.store.china # 索尼中国 +com.soohoot.ptest # 普通话发音测试 +com.soufun.app # 房天下 +com.souk.zuh # kk助手 +com.spdbccc.app # 浦大喜奔 +com.speedsoftware.rootexplorer # RE文件管理器 +com.ss.android.article.lite # 今日头条极速版 +com.ss.android.article.news # 今日头条 +com.ss.android.article.video # 西瓜视频 +com.ss.android.auto # 懂车帝 +com.ss.android.lark +com.ss.android.tuchong # 图虫 +com.ss.android.ugc.aweme # 抖音 +com.ss.android.ugc.aweme.lite # 抖音极速版 +com.ss.android.ugc.live # 抖音火山版 +com.ss.android.ugc.livelite # 抖音商城 +com.ss.readpoem # 为你诵读 +com.sskj.flashlight # 随手电筒 +com.ssports.mobile.video # 爱奇艺体育 +com.ssxg.cheers # 悦享家生活 +com.starsidea.wallpaper # 星辰壁纸 +com.storm.smart # 暴风影音 +com.student.yuwen.yimilan # 一米阅读 +com.suddenfix.customer # 闪电修 +com.sun.quickpay # 神奇小部件 +com.sunboxsoft.charge.institute # 中油好客e站 +com.suning.mobile.ebuy # 苏宁易购 +com.suning.mobile.epa # 星图金融 +com.sunying.booster # 光影 +com.sup.android.superb # 皮皮虾 +com.superlib # 移动图书馆 +com.suteng.zzss480 # 趣拿 +com.swlvrm.anwsi # 蜜桃主题壁纸 +com.sxm.farpoolcar # 拼车 +com.sxm.yiwei # 一喂顺风车 +com.synology.DSfile # DS file +com.sywg.stock # 申万宏源高端 +com.szeureka.camerasrc # 联趣拍摄遥控器 +com.szqd.dialer # 神指拨号 +com.szyy2106.pdfscanner # 扫描王PDF +com.szzc # 神州租车 +com.szzc.ucar.pilot # 神州专车 +com.tadu.read # 塔读小说 +com.taihai.app2 # 看厦门 +com.tal.kaoyan # 考研帮 +com.talicai.timiclient # Timi记账 +com.taobao.etao # 一淘 +com.taobao.idlefish # 闲鱼 +com.taobao.litetao # 淘宝特价版 +com.taobao.live # 点淘 +com.taobao.movie.android # 淘票票 +com.taobao.qianniu # 千牛 +com.taobao.taobao # 淘宝 +com.taobao.trip # 飞猪旅行 +com.taoche.qctt # 汽车头条 +com.taoguba.app # 淘股吧 +com.taomibaw # 返利赚钱联盟 +com.taou.maimai # 脉脉 +com.taowuyou.tbk # 淘无忧 +com.tdx.AndroidNew # 通达信 +com.teamnet.hula # 粤听 +com.telecom.video # 天翼超高清 +com.tencent.android.qqdownloader # 应用宝 +com.tencent.androidqqmail # QQ邮箱 +com.tencent.cmocmna # 腾讯手游加速器 +com.tencent.djcity # 掌上道聚城 +com.tencent.gallerymanager # 腾讯相册管家 +com.tencent.gamehelper # 腾讯游戏助手 +com.tencent.gamehelper.dnf # DNF助手 +com.tencent.gamehelper.nz # 逆战助手 +com.tencent.gamehelper.pg # 和平营地 +com.tencent.gamehelper.smoba # 王者营地 +com.tencent.gamereva # 腾讯先锋 +com.tencent.hunyuan.app.chat # 元宝-腾讯旗下AI助手 +com.tencent.igame # 王者人生 +com.tencent.karaoke # 全民K歌 +com.tencent.map # 腾讯地图 +com.tencent.mm # 微信 +com.tencent.mobileqq # QQ +com.tencent.mtt # QQ浏览器 +com.tencent.news # 腾讯新闻 +com.tencent.portfolio # 腾讯自选股 +com.tencent.qqgame # QQ游戏 +com.tencent.qqlive # 腾讯视频 +com.tencent.qqlivekid # 小企鹅乐园 +com.tencent.qqmusic # QQ音乐 +com.tencent.qqmusicpad # QQ音乐HD +com.tencent.qqpim # QQ同步助手 +com.tencent.qqpimsecure # 腾讯手机管家 +com.tencent.qqpinyin # QQ输入法 +com.tencent.qqsports # 腾讯体育 +com.tencent.qt.qtl # 掌上英雄联盟 +com.tencent.qt.sns # 掌上穿越火线 +com.tencent.tgclub # 心悦俱乐部 +com.tencent.tim # TIM +com.tencent.token # QQ安全中心 +com.tencent.transfer # 换机助手 +com.tencent.ttpic # 天天P图 +com.tencent.weishi # 腾讯微视 +com.tencent.wemeet.app # 腾讯会议 +com.tencent.weread # 微信读书 +com.tencent.wework # 企业微信 +com.tencent.xriver # 腾讯加速器 +com.tencent.zebra # 水印相机 +com.tgelec.aqsh # 安全守护2 +com.tgry.j7yr.rqtko # 壁纸素材库 +com.themewallpaper.douping # 主题动态壁纸 +com.theotino.chinadaily # China Daily +com.theotino.sztv # 无线苏州 +com.thestore.main # 1号会员店 +com.thinkive.mobile.open.account.huaan # 华安证券手机开户 +com.thinkwu.live # 千聊 +com.thinkyeah.galleryvault # 照片保险箱 +com.threegene.yeemiao # 小豆苗 +com.tianqi2345 # 2345天气王 +com.tianqiyubao2345 # 15日天气预报 +com.tianwen.jjrb # 经济日报 +com.tianxingjian.screenshot # 超级截图录屏大师 +com.tianyancha.skyeye # 天眼查 +com.tieyou.train.ark # 铁友火车票 +com.ting.mp3.android # 千千音乐 +com.tingniu.audioclip # 音频剪辑大师 +com.tinmanarts.JoJoSherlock # 叫叫识字 +com.tiqiaa.icontrol # 遥控精灵 +com.tjhxyq.show.xuan # 炫来电 +com.tmall.wireless # 天猫 +com.tmri.app.main # 交管12123 +com.tom.ule.ui # 邮乐 +com.tongcheng.android # 同程旅行 +com.tongchengpzhl.com # 同城品质婚恋 +com.tonghuashun.stocktrade.gtjaqh # 国泰君安掌上期货 +com.tongzhuo.tongzhuogame # 同桌派对 +com.topgether.sixfoot # 六只脚 +com.toutiao.yangtse # 紫牛新闻 +com.towords # 拓词 +com.tpv.familylink # 菲家 +com.trantor.genesis # 维迈通多多 +com.tripadvisor.tripadvisor.daodao # Tripadvisor猫途鹰 +com.truckhome.bbs # 卡车之家 +com.truedian.dragon # 微购相册 +com.tsf.shell.widget.message # TSF 信息小部件 +com.tst.tinsecret # TST庭秘密 +com.tttci.tik # 提客AI提词器 +com.ttxg.fruitday # 天天果园 +com.tudou.android # 土豆 +com.tujia.hotel # 途家民宿 +com.tuniu.app.ui # 途牛旅游 +com.tuoniu.pingmuezuo # 屏幕恶作剧 +com.twentyfirstcbh.epaper # 21财经 +com.ubestkid.beilehu.android # 贝乐虎儿歌 +com.ubestkid.collection.a # 贝乐虎启蒙 +com.ucar.app # 淘车车 +com.UCMobile # UC浏览器 +com.ucmobile.lite # UC浏览器极速版 +com.ujigu.ytb # 优题宝 +com.umaman.laiyifen # 来伊份社区购 +com.ume.browser # 微米浏览器 +com.umetrip.android.msky.app # 航旅纵横 +com.unicom.wopay # 沃钱包 +com.unicom.zworeader.ui # 联通爱看 +com.unionpay # 云闪付 +com.unionpay.tsmservice +com.uqwe.xmjs # 兼小宝兼职 +com.ushaqi.zhuishushenqi # 追书神器 +com.ushaqi.zhuishushenqi.adfree # 追书神器免费版 +com.uxin.usedcar # 优信二手车 +com.v2gogo.project # 百姓关注 +com.vbill.shoushua.biz # 随行付Plus +com.vcredit.ddcash # 豆豆钱 +com.vcredit.kkcredit # 维信卡卡贷 +com.versa # 马卡龙玩图 +com.video.link.heart # 心心相印 +com.videogo # 萤石云视频 +com.vistastory.news # 看天下 +com.vivo.appstore # vivo应用商店 +com.vivo.browser # vivo浏览器 +com.vivo.easyshare # 互传 +com.vivo.health # vivo健康 +com.vivo.space # vivo 官网 +com.vivo.symmetry # vivo摄影 +com.vmall.client # 华为商城 +com.vodone.o2o.health_care.demander # 医护到家-快天使医护到家用户版 +com.vphonegaga.titan # 光速虚拟机 +com.wacai365 # 挖财记账 +com.wakeup.howear # Wearfit Pro +com.wallpaper.movephsy # 桌面主题精美 +com.wallstreetcn.news # 华尔街见闻 +com.wandafilm.app # 万达影城 +com.wander.android.wallpaper # 元气壁纸 +com.wandoujia.phoenix2 # 豌豆荚 +com.wangdou.prettygirls.dress # 装扮次元手帐 +com.wangyin.payment # 京东钱包 +com.wanmeizhensuo.zhensuo # 更美 +com.waterfall.metronome # 节拍器 +com.wbxm.icartoon # 看漫 +com.we.keyuan # 微商码头 +com.webank.wemoney # 微众银行 +com.weface.kksocialsecurity # 看看生活 +com.weico.international # 微博轻享版 +com.welove520.welove # 微爱 +com.wemall.onlineshopping # WEMALL +com.wenhua.bamboo # 随身行 +com.wenhui.ebook # 文汇 +com.when.coco # 365日历 +com.whizen.iptv.activity # 万视达 +com.whty.wicity.china # 和生活 +com.wifi.reader # 连尚读书 +com.wifi.reader.free # 连尚免费读书 +com.winbaoxian.wybx # 保险师 +com.winderinfo.yashanghui # 雅商汇荟 +com.wisentsoft.chinapost.android # 中国邮政 +com.wizeyes.colorcapture # 色采 +com.wkjyqh.wukuangqihuo # 五矿期货 +com.wkscuiphone # 五矿证券 +com.wlqq # 货车帮司机 +com.wlqq4consignor # 货车帮货主 +com.wlstock.fund # 持股帮 +com.wm.dmall # 多点 +com.wodi.who # 玩吧 +com.wole56.ishow # 我秀 +com.womai # 中粮我买网 +com.wondertek.cj_yun # 长江云 +com.wondertek.miguaikan # 咪咕视频爱看版 +com.wondertek.paper # 澎湃新闻 +com.woqu.attendance # 喔趣考勤 +com.wsecar.wsjc # 万顺叫车 +com.wshifu.app.jiedanyi # 万师傅师傅版 +com.wuba # 58同城 +com.wuba.huoyun # 快狗打车 +com.wuba.town.client # 58本地 +com.wuba.zhuanzhuan # 转转 +com.wudaokou.hippo # 盒马 +com.wuhquantuo.ringtonedaquanfree # 铃声大全免费版 +com.www.ccoocity.ui # 城市通 +com.xcar.activity # 爱卡汽车 +com.xdf.recite # 新东方乐词 +com.xdtech.news.greatriver # 江西新闻 +com.xfplay.play # 影音先锋 +com.xhey.xcamera # 今日水印相机 +com.xhqb.app # 小花钱包 +com.xiachufang # 下厨房 +com.xiangha # 香哈菜谱 +com.xiangjiaosp.bject # 香蕉视频 +com.xiangrikui.sixapp # 向日葵保险人 +com.xianqing.driving3d # 驾考模拟3D +com.xiaobanlong.main # 小伴龙 +com.xiaodao.jiemeng # 复真书法 +com.xiaoenai.app # 小恩爱 +com.xiaoji.emulator # 小鸡模拟器 +com.xiaomi.gamecenter.sdk.service # 游戏服务 +com.xiaomi.hm.health # Zepp Life +com.xiaomi.market # 小米应用商店 +com.xiaomi.mico # 小爱音箱 +com.xiaomi.mitv.phone.tvassistant # 小米电视助手 +com.xiaomi.router # 小米WiFi +com.xiaomi.shop # 小米商城 +com.xiaomi.smarthome # 米家 +com.xiaomi.youpin # 小米有品 +com.xiaoshijie.sqb # 好省 +com.xiaoying.cardloan # 小赢卡贷 +com.xiaoyusan.android # 小雨伞 +com.xiaozhu.xzdz # 小猪民宿 +com.xiguakeji.bddh # 北斗侠导航 +com.ximalaya.ting.android # 喜马拉雅 +com.xingin.xhs # 小红书 +com.xingluo.mpa # 魔力相册 +com.xinhang.mobileclient # 中国移动河南 +com.xinhu.futures # 新湖期货开户交易 +com.xinli.fm # 心理Fm +com.xinshuru.inputmethod # 手心输入法 +com.xinyongfei.xyf # 信用飞 +com.xinyu210.enchdictionary # 英汉词典 +com.xiongmaoqiming.quming # 真熊猫起名 +com.xiuyuan.phonelive # 秀员 +com.xiwei.logistics.consignor # 运满满货主 +com.xk.span.zutuan # 美逛 +com.xmcy.hykb # 好游快爆 +com.xmtj.mkz # 漫客栈 +com.xqw369.ttsz # 天天识字 +com.xs.fm # 番茄畅听 +com.xs.fm.lite # 番茄音乐(原畅听音乐) +com.xt.retouch # 醒图 +com.xtc.watch # 小天才 +com.xtuone.android.syllabus # 超级课程表 +com.xuanyou2022.bizhi # 好看手机壁纸 +com.xueersi.parentsmeeting # 学而思网校 +com.xueqiu.android # 雪球 +com.xuexiang.myring # 炫铃声 +com.xunlei.downloadprovider # 迅雷 +com.xunmeng.merchant # 拼多多商家版 +com.xunmeng.pinduoduo # 拼多多 +com.xunyou.rb # 次元姬小说 +com.xvideostudio.videoeditor # 乐秀视频剪辑视频编辑 +com.xyhui # PU口袋校园 +com.yangcong345.android.phone # 洋葱学园 +com.yaoo.qlauncher # 如意老人桌面 +com.yaoyu.wanzhou # 看万州 +com.yaya.zone # 叮咚买菜 +com.ybws.ucast # Ucast +com.ycwb.android.ycpai # 羊城派 +com.ydtx.camera # 元道经纬相机 +com.yeahka.mach.android.openpos # 乐刷商务版 +com.yek.android.game5173 # 5173 +com.yek.android.kfc.activitys # 肯德基 +com.yek.android.uniqlo # 优衣库 +com.ygkj.chelaile.standard # 车来了 +com.ygxc.student # 阳光学车 +com.yiban.app # 易班 +com.yiban1413.yiban # 一伴婚恋专业版 +com.yibanlu.luck # 知趣聊天 +com.yibasan.lizhifm # 荔枝 +com.yibo.xiaoguan.toc # 销冠输入法 +com.yicai.news # 第一财经 +com.yiche.autoeasy # 易车 +com.yiche.price # 易车汽车报价 +com.yidian.xiaomi # 一点新闻资讯 +com.yigequanquanqq.app # 一个圈圈 +com.yigusjgh.daoshuri # 倒计时DDD +com.yilesoft.app.beautifulimageshow # 图片文字美化 +com.yingsoft.ksbao.Activity # 考试宝典 +com.yingyonghui.market # 应用汇 +com.yipiao # 智行火车票 +com.yiruike.sodacn.android # 甜盐相机 +com.yitong.fjnx.mbank.android # 福建农信 +com.yitong.mbank # 江苏·农商行 +com.yitong.mbank.psbc # 邮储银行 +com.yiwang # 1药网 +com.yiwugou.yiwukan # 义乌购 +com.yixia.videoeditor # 秒拍 +com.yixianghua # 宜享花 +com.yiyou.ga # TT语音 +com.ylmf.androidclient # 115生活 +com.ymatou.seller # 洋码头扫货神器买手版 +com.ymatou.shop # 洋码头 +com.ymt360.app.mass # 一亩田 +com.yoloho.dayima # 大姨妈 +com.youba.barcode # 二维码扫描 +com.youcheyihou.iyoursuv # 有车以后 +com.youdao.dict # 网易有道词典 +com.youdao.note # 有道云笔记 +com.youdao.translator # 有道翻译官 +com.yougou # 优购时尚商城 +com.youku.crazytogether # 来疯 +com.youku.phone # 优酷视频 +com.youloft.calendar # 万年历 +com.youxiang.soyoungapp # 新氧医美 +com.ysy.ayy # 木鸟民宿 +com.yuanli.recordingSoftware # 录音软件 +com.yuanli.wallpapermaster # 手机壁纸大师 +com.yuanlue.chongwu # 互动桌面宠物 +com.yuantiku.tutor # 猿辅导 +com.yuedong.sport # 悦动圈 +com.yueme.itv # 小翼管家 +com.yuewen.cooperate.reader.free # 飞读小说 +com.yuexiang.wallpaper # 悦享动态壁纸 +com.yumc.phsuperapp # 必胜客 +com.yumei.game.engine.ui.client # 玩股成金 +com.yunlan.unlock.dewdropunlock # 3D水滴-随变锁屏 +com.yunlian.wewe # 微微网络电话 +com.yunxiao.haofenshu # 好分数 +com.yuyuesports.yuyue.android # 羽约运动 +com.ywallpaper.ddztbz # 多多主题壁纸 +com.yx # 有信 +com.yxcps.trainning # 益学堂 +com.yy.android.udbsec # YY安全中心 +com.yy.dreamer # 知偶 +com.yy.huanju # Hello +com.yy.yomi # Yo语音 +com.yyyfs.wallpaper # 元宇宙壁纸 +com.zaxd.loan # 众安贷 +com.zcbl.bjjj_driving # 北京交警 +com.zenmen.palmchat # 连信 +com.zerone.hidesktop # 我的桌面iScreen +com.zhangdan.app # 51信用卡管家 +com.zhangxiong.art # 张雄艺术 +com.zhanlang.dailyscreen # 录屏君 +com.zhaopin.social # 智联招聘 +com.zhenai.android # 珍爱 +com.zhibo.mm # 秘密直播 +com.zhichan.msmds # 买什么都省 +com.zhihu.android # 知乎 +com.zhihu.daily.android # 知乎日报 +com.zhinengremote.mohan # 空调智能遥控器+ +com.zhiyaolai.playschool # 锦鲤宇宙 +com.zhongan.insurance # 众安保险 +com.zhubajie.client # 猪八戒 +com.zhutisp.sdbzcm # 主题商店免费 +com.ziipin.homeinn # 首旅如家 +com.ziipin.softkeyboard.kazakh # Badam哈语输入法 +com.zlj.picture.recover.restore.master # 图片恢复 +com.zmlearn.course.am # 掌门1对1辅导 +com.zol.android # 中关村在线 +com.zongheng.reader # 纵横小说 +com.zstv # 中山手机台 +com.zsyj.trans # 全局透明壁纸 +com.zsyj.wallpaper # 熊猫动态壁纸 +com.zuinengsheng.io # 奶萌 +com.zxscnew # 中信证券 +com.zybang.parent # 作业帮口算 +com.zydsoft.acuherb # 中医辨证云 +com.zyhd.voice # 语音包变声器 +com.zz.voip # 小鹿网络电话 +com.zzu.sxm.chemical # 化学方程式 +com.zzw.october # 志愿汇 +consumer.ttpc.com.consumer # 天天拍车 +cszy.wdtk.dntkbz # 我的图库 +ctrip.android.view # 携程旅行 +dji.go.v5 # DJI Fly +dongzheng.szkingdom.android.phone # 掌证宝 +enfc.metro # 亿通行 +epson.print # Epson iPrint +fkg.client.side.activity # 蜂狂购 +flipboard.cn # 红板报 +fm.qingting.qtradio # 蜻蜓FM +gov.pianzong.androidnga # NGA玩家社区 +gyjf.alfdcyj.ooo # 囧次元 +gz.lifesense.weidong # 乐心健康 +gzry.mxbz.ihauh # 魔秀桌面壁纸 +hbyc.china.medical.view # 壹生 +hg.zp.ui # 天眼新闻 +howbuy.android.palmfund # 好买基金 +howbuy.android.piggy # 储蓄罐 +hugh.android.app.zidianadd # 新华字典补丁 +im.weshine.keyboard # KK键盘 +im.xingzhe # 行者 +im.yixin # 易信 +io.dcloud.H576E6CC7 # 鱼泡直聘 +io.dcloud.H5999961F # 宝宝云 +io.dcloud.Pintui # 品推资讯 +io.iftech.android.box # 小组件盒子 +jiakaokeer.app.good # 驾考宝典科目二 +jiakaokesi.app.good # 驾考宝典极速版 +jiakaotong.app.good # 考试全能王 +jiakaozhushou.app.good # 驾考助手 +jiaxiao.app.good # 驾照宝典 +jp.ne.neko.freewing.JapaneseKeyboard # 简易的日语输入 日文输入法 五十音图 虚拟键盘方式 +jyeoo.app.ystudy # 菁优网 +kehuo.app.good # 驾考宝典客货车 +llbt.ccb.lnga # 辽事通 +long.xian.wallpaper # 免费主题商店 +longbin.helloworld # 计算管家 +mall.dfs168.com.mall # 大丰收农服 +mark.via # Via +me.chunyu.ChunyuDoctor # 春雨医生 +me.ele # 饿了么 +me.ele.napos # 淘宝闪购商家版 +me.gfuil.bmap +me.iweek.rili # 生活日历 +me.niuke.app.gupiaozhitou # 阿牛智投 +me.piebridge.brevent # 黑阈 +me.suncloud.marrymemo # 婚礼纪 +me.yidui # 伊对 +music.f2bcutter.mp3editor # 音乐音频剪辑 +my.beautyCamera # 美人相机 +my.maya.android # 多闪 +net.aituyue.app # 图曰 +net.ask123.mima96 # 密码酷 +net.bontec.wxqd.activity # 爱青岛 +net.cmoaciopm.sgsdict # 三国杀大典 +net.joydao.radio # 听听广播 +net.joydao.spring2011 # 祝福短信 +net.kdnet.club # 凯迪网 +net.qihoo.launcher.widget.clockweather # 360天气 +net.whty.app.eyu # 人人通空间 +net.xinhuamm.mainclient # 新华社 +net.zdsoft.zerobook_android # 智人播客 +news.cnr.cn # 央广网 +oms.mmc.app.almanac_inland # 老黄历万年历顺历 +oms.mmc.fortunetelling # 灵机 +one.hh.oneclient # 一个 +org.fungo.fungolive # 天天电视直播 +org.moegirl.moegirlview # 萌娘百科 +org.mozilla.fennec_mylinux # 蚂蚁浏览器 +org.sojex.finance # 口袋贵金属 +org.thinkinbunny.autoanswer # 自动应答 +org.zywx.wbpalmstar.widgetone.uex10075364 # 经管之家 +org.zywx.wbpalmstar.widgetone.uexaaagg10001 # 安全守护 +org.zywx.wbpalmstar.widgetone.uexaaagg10013 # 关爱守护 +paint.by.number.pixel.art.coloring.drawing.puzzle # 乐涂数字填色 +pet.desktop.elf # 图图宠物 +photo.editor.polarr # 泼辣修图 +pinkdiary.xiaoxiaotu.com # 粉粉日记 +protect.eye # 护眼宝 +qianlong.qlmobile # 钱龙 +qnahj.water.xianx # 水仙 +sg.bigo.shrimp # 皮皮蟹 +shengang.szkingdom.android.phone # 申港证券 +sina.mobile.tianqitong # 天气通 +sl.mySms # Q信 +smskb.com # 盛名时刻表 +software.ecenter.study # 状元共享课堂 +sogou.mobile.explorer # 搜狗浏览器极速版 +sr.daiv.alls.ja # 基础日语口语 +sr.daiv.alls.ko # 基础韩语口语 +ts.NetTraffic # 网速流量监测 +tt.lt.tt # 聊天漂流瓶 +tv.acfundanmaku.video # AcFun +tv.danmaku.bili # 哔哩哔哩 +tv.danmaku.bilibilihd # 哔哩哔哩HD +tv.pps.mobile # 爱奇艺随刻 +tv.yilan.howto.app # 好兔视频 +tv.yixia.bobo # 波波视频 +us.pinguo.april_collage # 拼图酱 +vStudio.Android.Camera360 # 相机360 +vz.com # 飞常准 +weicom.yi.wallpaper # 免费主题壁纸 +wind.deposit # 万得基金 +wshifu.app.android # 万师傅企业版 +yiqi.bazi # 易奇文化 +ysykzheng.hotlauncher # 热门启动器 +yyc.xk # 星空视频壁纸 +zhongxinjiantou.szkingdom.android.newphone # 蜻蜓点金 diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml index efbcacffcc..dff9194d6f 100644 --- a/app/src/main/res/xml/network_security_config.xml +++ b/app/src/main/res/xml/network_security_config.xml @@ -1,10 +1,11 @@ - + + - \ No newline at end of file + diff --git a/build.gradle.kts b/build.gradle.kts index 73ec8aff25..5982fb3428 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -65,12 +65,12 @@ subprojects { resValue("integer", "release_code", "$versionCode") ndk { - abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64") + abiFilters += listOf("arm64-v8a"/*, "armeabi-v7a", "x86", "x86_64"*/) } externalNativeBuild { cmake { - abiFilters("arm64-v8a", "armeabi-v7a", "x86", "x86_64") + abiFilters("arm64-v8a"/*, "armeabi-v7a", "x86", "x86_64"*/) } } @@ -188,16 +188,21 @@ subprojects { isEnable = true isUniversalApk = true reset() - include("arm64-v8a", "armeabi-v7a", "x86", "x86_64") + include("arm64-v8a") } } } compileOptions { + isCoreLibraryDesugaringEnabled = true sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } } + + dependencies { + add("coreLibraryDesugaring", "com.android.tools:desugar_jdk_libs:2.1.4") + } } task("clean", type = Delete::class) { diff --git a/common/README.md b/common/README.md new file mode 100644 index 0000000000..cd8abc4329 --- /dev/null +++ b/common/README.md @@ -0,0 +1,16 @@ +--- +title: "common 模块" +type: overview +status: accepted +phase: N/A +updated: 2026-10-15 +summary: "跨模块常量、Store 与 Android 兼容工具" +--- + +# common + +被 `app`、`core`、`service`、`design` 共用的常量、偏好 Store 和兼容工具。对 `hideapi` 是 `compileOnly`。 + +导航:[docs/modules/common/INDEX.md](../docs/modules/common/INDEX.md) + +设计:[docs/INDEX.md](docs/INDEX.md) diff --git a/common/build.gradle.kts b/common/build.gradle.kts index bff1f8bd1c..798b75782d 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -8,4 +8,6 @@ dependencies { implementation(libs.kotlin.coroutine) implementation(libs.androidx.core) + + testImplementation(libs.junit) } diff --git a/common/docs/INDEX.md b/common/docs/INDEX.md new file mode 100644 index 0000000000..e38e224cf3 --- /dev/null +++ b/common/docs/INDEX.md @@ -0,0 +1,74 @@ +--- +title: "common 模块设计" +type: index +status: accepted +phase: N/A +updated: 2026-10-25 +summary: "common 的包、依赖,以及 Store、常量以外的 Global、语言、compat 和 util" +--- + +# common + +`app`、`core`、`service`、`design` 以 `implementation` 依赖本模块。本模块自己只 `compileOnly` [`hideapi`](../../hideapi/docs/INDEX.md),另外 `implementation` 了协程和 `androidx.core`。namespace 来自根 `build.gradle.kts`:模块名不是 `app` 时为 `com.github.kr328.clash.<模块名>`,因此是 `com.github.kr328.clash.common`。依赖图见 [架构概览](../../docs/architecture/overview.md)。 + +源码在 `common/src/main/java/com/github/kr328/clash/common/`。 + +| 包 | 作用 | +|:---|:-----| +| `store` | 带类型的偏好读写。见 [Store](store.md) | +| `constants` | 按运行时包名拼出的动作、组件、权限、authority。见 [常量](constants.md) | +| `compat` | 按 `SDK_INT` 分支的 Android API 包装 | +| `util` | 包名、Intent、分片 Parcel、定时 Channel | +| `Global.kt` | 进程内的 `Application` 和 IO 协程作用域 | +| `AppLanguages.kt` | 应用语言偏好与进程内套用。行为见 [界面](../../docs/systems/ui/INDEX.md) | +| `log` | 固定 tag 的 `android.util.Log` 包装 | +| `id` | 进程内递增整数。仓库里由 `ProfileWorker` 用作通知 id | + +本模块的 Manifest 只声明 `RECEIVE_BROADCASTS` 权限,见 [常量](constants.md)。外部控制动作怎样进 Manifest,见 [运行时](../../docs/systems/runtime/INDEX.md)。 + +## Global + +`Global` 是 `object`,用 `CoroutineScope(Dispatchers.IO)` 委托实现 `CoroutineScope`。`init` 保存 `Application`;`application` 在 `init` 之前访问会因为 `lateinit` 失败。`destroy` 取消这个作用域。 + +仓库里 `Global.init` 的调用在 `app` 的 `MainApplication.attachBaseContext`。`Global.destroy` 只出现在该类的 `finalize()` 方法里,没有 `override`,源码里也没有别的调用点。 + +`util` 的顶层 `packageName` 在第一次读取时取 `Global.application.packageName`,之后不再变。`constants` 里那些 `val` 在对应 `object` 第一次初始化时读取它。 + +## compat + +都在 `com.github.kr328.clash.common.compat`。版本判断以源码里的 `Build.VERSION.SDK_INT` 为准。 + +| 符号 | 行为 | +|:-----|:-----| +| `Application.currentProcessName` | API 28 起用 `Application.getProcessName()`。更低版本调用 `ActivityThread.currentProcessName()`;任何 `Throwable` 记 `Log.w` 后返回 `packageName`。存根见 [hideapi](../../hideapi/docs/INDEX.md) | +| `Drawable.foreground()` | API 26 起,且是 `AdaptiveIconDrawable`、`background == null` 时返回 `foreground`,否则返回自身 | +| `pendingIntentFlags` | `SDK_INT < 24` 时原样返回。`SDK_INT > 30` 且 `mutable` 为真时或上 `FLAG_MUTABLE`,否则在 `SDK_INT >= 24` 时或上 `FLAG_IMMUTABLE`。`mutable` 默认是 `false` | +| `PackageInfo.versionCodeCompat` | API 28 起读 `longVersionCode`,否则把 `versionCode` 转成 `Long` | +| `Configuration.preferredLocale` | API 24 起取 `locales[0]`,否则取 `locale` | +| `fromHtmlCompat` | API 24 起用 `FROM_HTML_MODE_COMPACT`,否则调用单参数 `Html.fromHtml` | +| `Context.getColorCompat` / `getDrawableCompat` | 转到 `ContextCompat` | +| `Context.registerReceiverCompat` | API 33 起:`permission == null` 时 flag 为 `RECEIVER_EXPORTED`,否则为 `RECEIVER_NOT_EXPORTED`。更低版本不传这个 flag | +| `Context.startForegroundServiceCompat` | API 26 起 `startForegroundService`,否则 `startService` | +| `Service.startForegroundCompat` | API 34 起带 `FOREGROUND_SERVICE_TYPE_SPECIAL_USE`,否则两参数的 `startForeground` | +| `TextView.textAppearance` | 只写。API 23 起 `setTextAppearance(value)`,否则 `setTextAppearance(context, value)`。读会抛 `UnsupportedOperationException` | +| `Window.isSystemBarsTranslucentCompat` | 只写。API 30 起 `setDecorFitsSystemWindows`;更低版本改 `systemUiVisibility` 的 layout 标志。API 28 起同时改 `layoutInDisplayCutoutMode` | +| `Window.isLightStatusBarsCompat` / `isLightNavigationBarCompat` | 只写。API 30 起走 `WindowInsetsController`,否则改对应的 `systemUiVisibility` 亮色标志 | +| `Window.isAllowForceDarkCompat` | API 29 起读写 `decorView.isForceDarkAllowed`。更低版本读出 `false`,写被忽略 | + +## util、log、id + +| 符号 | 行为 | +|:-----|:-----| +| `KClass.componentName` | `ComponentName(Global.application.packageName, java.name)` | +| `KClass.intent` | `Intent(Global.application, java)` | +| `Intent.grantPermissions` | 默认同时加上读、写 URI 权限 flag,返回 `this` | +| `Intent.fileName` | `data` 的 scheme 为 `file` 时取其 scheme-specific part。写入用 `Uri.fromParts("file", …)` | +| `Intent.uuid` | scheme 为 `uuid` 时把 scheme-specific part 解析成 `UUID`。另有 `setUUID` / `setFileName` | +| `List.writeToParcelSlice` | 先写列表长度,再写一个 `Binder`。对方用事务码 `10` 按 offset 和 chunk 取一段,逐个 `writeToParcel` | +| `Creator.createListFromParcelSlice` | 按块 `transact`。返回 `false`、读到长度为 0,或已取满时停止 | +| `PatternFileName` | `Regex("[^*&%\\n\\r/]+")` | +| `CoroutineScope.ticker` | `Channel.RENDEZVOUS`。循环里先 `send` 当前毫秒时间,再 `delay(period)`。捕获 `Exception` 后循环结束,没有 `close` | +| `Log` | tag 常量是 `ClashMetaForAndroid`,`i` / `w` / `e` / `d` / `v` 都带上它。`f` 调用的是 `android.util.Log.wtf(message, throwable)`,按这个两参数重载,第一个参数是 tag,不是上面的常量 | +| `UndefinedIds.next` | `@Synchronized`。`current = ((current and 0x00FFFFFF) + 1) or 0x14000000`。初始 `current` 为 0 | + +导航:[模块 README](../README.md)、[模块导航](../../docs/modules/common/INDEX.md)。 diff --git a/common/docs/constants.md b/common/docs/constants.md new file mode 100644 index 0000000000..07e272839f --- /dev/null +++ b/common/docs/constants.md @@ -0,0 +1,81 @@ +--- +title: "common 常量" +type: architecture +status: accepted +phase: N/A +updated: 2026-10-25 +summary: "Intents、Components、Permissions、Authorities、Metadata 实际拼出的字符串" +--- + +# common 常量 + +包是 `com.github.kr328.clash.common.constants`。除下面标明的 `const` 和类名外,字符串都是运行时 `packageName` 加上固定后缀。 + +`packageName` 定义在 `util/Global.kt`,第一次读取时等于 `Global.application.packageName`。这些 `object` 的 `val` 在该 `object` 第一次初始化时求值,所以要发生在 `Global.init` 之后。它读的是进程里的应用包名,不是本模块的 namespace,也不是写死的 flavor。包名和 namespace 的差别见 [术语表](../../docs/glossary.md)。 + +动作字符串怎样和导出组件的 Manifest 对齐,见 [运行时](../../docs/systems/runtime/INDEX.md)。下文只列出常量本身。 + +## Intents + +源码把前四个标为 Public,其余标为 Self。Public 的后缀是 `.action.`,Self 的后缀是 `.intent.action.`。Kotlin 名字和后缀并不总是同一段文字:`ACTION_PROFILE_REQUEST_UPDATE` 的后缀是 `REQUEST_UPDATE`,不是 `PROFILE_REQUEST_UPDATE`。 + +| 常量 | 值 | +|:-----|:---| +| `ACTION_PROVIDE_URL` | `$packageName.action.PROVIDE_URL` | +| `ACTION_START_CLASH` | `$packageName.action.START_CLASH` | +| `ACTION_STOP_CLASH` | `$packageName.action.STOP_CLASH` | +| `ACTION_TOGGLE_CLASH` | `$packageName.action.TOGGLE_CLASH` | +| `ACTION_SERVICE_RECREATED` | `$packageName.intent.action.CLASH_RECREATED` | +| `ACTION_CLASH_STARTED` | `$packageName.intent.action.CLASH_STARTED` | +| `ACTION_CLASH_STOPPED` | `$packageName.intent.action.CLASH_STOPPED` | +| `ACTION_CLASH_REQUEST_STOP` | `$packageName.intent.action.CLASH_REQUEST_STOP` | +| `ACTION_CLASH_REQUEST_RELOAD` | `$packageName.intent.action.CLASH_REQUEST_RELOAD` | +| `ACTION_PROFILE_CHANGED` | `$packageName.intent.action.PROFILE_CHANGED` | +| `ACTION_PROFILE_UPDATE_COMPLETED` | `$packageName.intent.action.PROFILE_UPDATE_COMPLETED` | +| `ACTION_PROFILE_UPDATE_FAILED` | `$packageName.intent.action.PROFILE_UPDATE_FAILED` | +| `ACTION_PROFILE_REQUEST_UPDATE` | `$packageName.intent.action.REQUEST_UPDATE` | +| `ACTION_PROFILE_SCHEDULE_UPDATES` | `$packageName.intent.action.SCHEDULE_UPDATES` | +| `ACTION_PROFILE_LOADED` | `$packageName.intent.action.PROFILE_LOADED` | +| `ACTION_OVERRIDE_CHANGED` | `$packageName.intent.action.OVERRIDE_CHANGED` | +| `ACTION_APP_LANGUAGE_CHANGED` | `$packageName.intent.action.APP_LANGUAGE_CHANGED` | + +Extra 键是 `const`,不加包名前缀: + +| 常量 | 值 | +|:-----|:---| +| `EXTRA_NAME` | `name` | +| `EXTRA_STOP_REASON` | `stop_reason` | +| `EXTRA_APP_LANGUAGE` | `app_language` | +| `EXTRA_UUID` | `uuid` | +| `EXTRA_FAIL_REASON` | `fail_reason` | + +## Components + +类名包是源码里的私有常量 `com.github.kr328.clash`。`ComponentName` 的第一个参数才是运行时 `packageName`。 + +| 常量 | 类名 | +|:-----|:-----| +| `MAIN_ACTIVITY` | `com.github.kr328.clash.MainActivity` | +| `PROPERTIES_ACTIVITY` | `com.github.kr328.clash.PropertiesActivity` | + +## Permissions + +`RECEIVE_SELF_BROADCASTS` 是 `$packageName.permission.RECEIVE_BROADCASTS`。 + +同一字符串由本模块 Manifest 声明:`${applicationId}.permission.RECEIVE_BROADCASTS`,`protectionLevel` 为 `privileged|signature`,同时有一条 `uses-permission`。label 是 `@string/receive_clash_broadcasts`,description 是 `@string/receive_broadcasts_of_clash`。这两条字符串在 `common/src/main/res/` 的 `values`、`values-zh`、`values-zh-rTW`、`values-ru`。 + +## Authorities + +| 常量 | 值 | +|:-----|:---| +| `STATUS_PROVIDER` | `$packageName.status` | +| `SETTINGS_PROVIDER` | `$packageName.settings` | +| `FILES_PROVIDER` | `$packageName.files` | + +`service/src/main/AndroidManifest.xml` 里 `StatusProvider`、`PreferenceProvider`、`FilesProvider` 的 `android:authorities` 分别是 `${applicationId}.status`、`${applicationId}.settings`、`${applicationId}.files`。 + +## Metadata + +`GEOIP_FILE_NAME` 是 `$packageName.GEOIP_FILE_NAME`。在本仓库的 Kotlin、Java 和 XML 里,只有这一处定义,没有别的引用。 + +返回:[common 设计索引](INDEX.md)。 diff --git a/common/docs/store.md b/common/docs/store.md new file mode 100644 index 0000000000..12bb33cf92 --- /dev/null +++ b/common/docs/store.md @@ -0,0 +1,56 @@ +--- +title: "Store 与 StoreProvider" +type: architecture +status: accepted +phase: N/A +updated: 2026-10-15 +summary: "Store 用属性委托按给定的键读写 StoreProvider,本模块只有 SharedPreferences 这一份实现" +--- + +# Store 与 StoreProvider + +`common` 的 `store` 包有三个类型:`StoreProvider`、`Store`、`SharedPreferenceProvider`。这里没有缓存,没有变更监听,也没有跨进程逻辑。每次读和写都进到当时的 `provider`。 + +## StoreProvider + +接口按键提供五种值,读都带默认值: + +| 方法 | 值 | +|:-----|:---| +| `getInt` / `setInt` | `Int` | +| `getLong` / `setLong` | `Long` | +| `getString` / `setString` | `String` | +| `getStringSet` / `setStringSet` | `Set` | +| `getBoolean` / `setBoolean` | `Boolean` | + +本模块里唯一的实现是 `SharedPreferenceProvider`。`SharedPreferences.asStoreProvider()` 返回它。写入使用 `androidx.core.content.edit` 的 `preferences.edit { }`,调用处没有传 `commit = true`。`getString` 和 `getStringSet` 对平台返回值做了 `!!`,平台给出 `null` 时会在这里抛空指针。 + +## Store + +`Store` 只保存构造时传入的 `StoreProvider`。每种工厂方法返回 `Store.Delegate`,实现 `getValue` / `setValue`,所以调用方写成 `var field by store.xxx(...)`。 + +键是工厂参数里的字符串,不是 Kotlin 属性名。委托不读 `KProperty.name`。 + +| 工厂 | 存储 | 读到不能用的值时 | +|:-----|:-----|:-----------------| +| `int` / `long` / `boolean` | 同类型 | 键不存在时用传入的默认值 | +| `string` / `stringSet` | 同类型 | 同上;经过上面的 `!!` | +| `enum` | `value.name` 字符串 | `getString` 的默认值是 `defaultValue.name`。`values` 里没有同名项时返回 `defaultValue` | +| `typedString` | 字符串,类型是 `T?` | 读:`from(provider.getString(key, to(null)))`。写:`setString(key, to(value))`。缺键时没有单独的 `T` 默认值,而是先 `to(null)` 再 `from` | + +`enum` 的 `values` 由调用方传入,通常是某个枚举的 `values()`。 + +## 谁在用 + +本模块不定义具体偏好项。下面这些类自己 `new Store(...)`,字段和默认值留在各类里: + +| 类 | `StoreProvider` 从哪来 | +|:---|:------------------------| +| `app` 的 `AppStore` | `getSharedPreferences("app", MODE_PRIVATE)` | +| `app` 的 `TipsStore` | `getSharedPreferences("tips", MODE_PRIVATE)` | +| `design` 的 `UiStore` | `getSharedPreferences("ui", MODE_PRIVATE)` | +| `service` 的 `ServiceStore` | `PreferenceProvider.createSharedPreferencesFromContext(context)`,再 `asStoreProvider()` | + +`ServiceStore` 里还有不经过 `Store` 的字段,直接写 `settings` 目录下的文件。那部分不属于本模块。 + +返回:[common 设计索引](INDEX.md)。 diff --git a/common/src/main/java/com/github/kr328/clash/common/AppLanguages.kt b/common/src/main/java/com/github/kr328/clash/common/AppLanguages.kt new file mode 100644 index 0000000000..48d6d1d750 --- /dev/null +++ b/common/src/main/java/com/github/kr328/clash/common/AppLanguages.kt @@ -0,0 +1,126 @@ +package com.github.kr328.clash.common + +import android.content.Context +import android.content.Intent +import android.content.res.Configuration +import android.os.Build +import android.os.LocaleList +import androidx.core.os.LocaleListCompat +import com.github.kr328.clash.common.constants.Intents +import com.github.kr328.clash.common.constants.Permissions +import java.util.Locale + +/** + * Tags follow `app/src/main/res/xml/locales_config.xml`, in the same order. + * An empty tag follows the system, including an Android 13 per-app locale. + */ +object AppLanguages { + const val PREFERENCE_NAME = "ui" + const val KEY = "app_language" + const val FOLLOW_SYSTEM = "" + + data class Option(val tag: String, val nativeName: String) + + val supported: List