Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/android-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Android Build

# 目的:把「必须构建 Rust native 库」这件事固化到 CI,杜绝
# 「只跑 gradlew assemble → 得到能装必崩的包」这类问题。
#
# 说明:
# - 构建前会先校验 patches/core/*.patch 能干净应用(上游 core 变更会导致失败)。
# - 需要 cargo-ndk 与 lib-patcher;若 lib-patcher 不在 crates.io,请改为从源码安装。

on:
push:
branches: [ main, 'fix/**' ]
pull_request:

jobs:
core-patch-check:
name: core 子模块补丁可干净应用
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: 校验 patches/core/*.patch
run: |
shopt -s nullglob
status=0
count=0
for p in patches/core/*.patch; do
count=$((count + 1))
if git -C core apply --reverse --check "$p" 2>/dev/null; then
echo "已应用: $p"
elif git -C core apply --check "$p"; then
echo "可应用: $p"
else
echo "::error::core 补丁无法干净应用: $p(上游 core 可能已变更,需要 rebase)"
status=1
fi
done
echo "共检查 $count 个补丁"
exit $status

build-apk:
name: 构建 native 库 + APK (arm64-v8a)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- uses: android-actions/setup-android@v3

- name: 安装 NDK 与 platform-tools
run: sdkmanager --install "ndk;27.0.12077973" "platform-tools"

- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android

- name: 安装 cargo-ndk 与 lib-patcher
run: |
cargo install cargo-ndk
cargo install lib-patcher

- name: 构建(Rust + APK)
run: |
export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT:-$ANDROID_HOME}/ndk/27.0.12077973"
./scripts/build-android.sh debug arm64-v8a

- uses: actions/upload-artifact@v4
with:
name: app-debug-apk
path: app/build/outputs/apk/debug/app-debug.apk
32 changes: 30 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.io.File
import java.util.Properties

plugins {
Expand All @@ -23,8 +24,16 @@ android {
applicationId = "cn.yzjtiantian.android"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0.0"
versionCode = 2
versionName = "1.1.0"

val builtAbis = file("src/main/jniLibs")
.listFiles { f -> f.isDirectory && File(f, "libpeytchat_bridge.so").exists() }
?.map { it.name }
.orEmpty()
if (builtAbis.isNotEmpty()) {
ndk { abiFilters += builtAbis }
}
}
signingConfigs {
create("release") {
Expand Down Expand Up @@ -62,6 +71,25 @@ kotlin {
}
}

// 构建前校验 native 桥接库已就位:缺失时直接报可操作的错误,
// 而不是产出一个「能装、一启动就 NoClassDefFoundError」的包。
val verifyNativeLibs = tasks.register("verifyNativeLibs") {
val jniDir = file("src/main/jniLibs")
doFirst {
val abis = jniDir.listFiles { f -> f.isDirectory && File(f, "libpeytchat_bridge.so").exists() }
?.map { it.name }
.orEmpty()
if (abis.isEmpty()) {
throw GradleException(
"app/src/main/jniLibs/ 下没有 libpeytchat_bridge.so,构建出的 APK 会在启动时崩溃。\n" +
"请先生成 native 库,例如:\n" +
" ./scripts/build-android.sh debug arm64-v8a,x86_64"
)
}
}
}
tasks.matching { it.name == "preBuild" }.configureEach { dependsOn(verifyNativeLibs) }

dependencies {
implementation(project(":core"))
implementation(project(":data"))
Expand Down
3 changes: 1 addition & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<application
android:allowBackup="true"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/Theme.Peytchat">
<provider
Expand Down
4 changes: 1 addition & 3 deletions app/src/main/java/cn/yzjtiantian/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -230,13 +230,11 @@ fun AppRoot(
}
}

// ✅ 传入 accountManager
ShellScreen(
repository = repository,
accountManager = accountManager, // ✅ 新增参数
accountManager = accountManager,
onLoggedOut = {
loggedIn = false
// 退出登录:停止常驻消息服务,清掉全部通知
MessageNotificationService.stop(context)
MessageNotifications.cancelAll(context)
},
Expand Down
13 changes: 0 additions & 13 deletions app/src/main/res/xml/network_security_config.xml

This file was deleted.

30 changes: 16 additions & 14 deletions docs/hot-update.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# 热更新(Hot Update)说明

自托管 DEX 补丁热更新:客户端从服务端拉取**更新清单**,下载补丁 `.dex` 文件并做
MD5 校验,记录后**下次启动时**通过 `DexClassLoader` 加载注册。
自托管 DEX 补丁热更新:客户端从服务端拉取**更新清单**(**仅支持 https**),下载补丁
`.dex` 文件并做 SHA-256 校验,记录后**下次启动时**通过 `DexClassLoader` 加载注册。

## 模块结构

Expand Down Expand Up @@ -31,13 +31,13 @@ MD5 校验,记录后**下次启动时**通过 `DexClassLoader` 加载注册。
"module": "shell",
"version": "1.0.1",
"url": "https://your-server/peytchat/patches/shell_1.0.1.dex",
"md5": "d41d8cd98f00b204e9800998ecf8427e"
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"module": "login",
"version": "1.0.2",
"url": "https://your-server/peytchat/patches/login_1.0.2.dex",
"md5": "9e107d9d372bb6826bd81d3542a419d6"
"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
]
}
Expand All @@ -47,9 +47,11 @@ MD5 校验,记录后**下次启动时**通过 `DexClassLoader` 加载注册。

- `appVersion`:可选,App 版本(当前仅展示用,不做强制校验)。
- `patches[].module`:模块名,取值 `shell` / `login` / `chat` / `account`。
- `patches[].version`:补丁版本号(字符串比较;与本地已记录版本不同即视为需要更新)。
- `patches[].url`:补丁 dex 的下载地址。
- `patches[].md5`:补丁 dex 的 MD5(小写十六进制),客户端下载后校验,不通过则丢弃。
- `patches[].version`:补丁版本号(语义化比较;仅当**严格大于**本地已记录版本时才更新,
相等或更低一律跳过,防止被下发旧版补丁做降级攻击)。
- `patches[].url`:补丁 dex 的下载地址(必须是 `https://`,否则拒绝)。
- `patches[].sha256`:补丁 dex 的 SHA-256(64 位小写十六进制),客户端下载后校验,不通过则丢弃;
为兼容旧清单仍接受 `md5`(32 位)。

## 部署到 GitHub(免费静态托管)

Expand Down Expand Up @@ -109,15 +111,15 @@ https://cdn.jsdelivr.net/gh/<用户名>/<仓库名>@main/patches/shell_1.0.1.dex
updates/patches/login_1.0.2.dex
```

脚本自动计算每个文件的 MD5 并生成 `update.json`,然后提交推送即可。
脚本自动计算每个文件的 SHA-256 并生成 `update.json`,然后提交推送即可。

## 客户端流程

1. 设置页 →「检查更新」→ 弹出对话框(默认清单地址
`https://peyt.org/peytchat-android-update/update.json`,可修改并持久化)。
`https://peyt.org/peytchat-android-update/update.json`,可修改并持久化;**仅接受 https**)。
2. 点击「立即检查」→ `HotUpdateManager.checkForUpdates(url)`:
- 拉取清单,逐条对比本地已记录版本;
- 新版本 → 下载 dex → MD5 校验 → 写入
- 新版本(严格递增)→ 下载 dex → SHA-256 校验 → 写入
`SharedPreferences`(`<module>_patch_path` / `<module>_version`,`loaded=false`)。
3. 下次进程启动时 `EventBridge.start()` 调用 `HotUpdateManager.loadPendingPatches()`:
- 重置各模块 `loaded=false`(DexClassLoader 不跨进程保留,需重新加载);
Expand All @@ -132,8 +134,8 @@ https://cdn.jsdelivr.net/gh/<用户名>/<仓库名>@main/patches/shell_1.0.1.dex
cd /tmp/update && python3 -m http.server 8000
```

3. 手机与电脑同一网络,清单地址填
`http://<电脑IP>:8000/update.json`。
3. 手机与电脑同一网络。**客户端只接受 https 清单地址**,明文 `http://` 会被直接拒绝;
本地联调可自签证书或走内网穿透(如 ngrok)提供 https。
4. 设置页 →「检查更新」→「立即检查」,观察下载/校验结果提示。

### 构造一个测试补丁 dex
Expand All @@ -152,8 +154,8 @@ EOF
javac Patch.java
d8 --output . Patch.class # 生成 classes.dex,改名为 shell_1.0.1.dex

# 3. 计算 md5 并填进清单
md5sum shell_1.0.1.dex
# 3. 计算 sha256 并填进清单
sha256sum shell_1.0.1.dex
```

## 补丁类怎么写
Expand Down
Loading
Loading