需求是"改完 Android 端怎么送到手机上"。不发应用商店,也不想每次手动传包,
于是做了最小闭环:后端存一份安装包清单,App 启动时查、下载、交给系统安装器。
后端
- 新增 /app/release/latest 与 /app/release/download/{file},都免登录:
还没装上新包的客户端未必带着有效令牌,检查更新不该被登录挡住
- download_url 用相对路径,客户端拿自己配置的 API 地址拼——同一份清单要在
内网、公网、模拟器之间共用,写死域名只会让其中一个坏掉
- has_update 只比对 version_code,不比版本名字符串(0.10.0 与 0.9.0 的字典序是坑)
- 下载只认清单里登记的文件名,含 ..%2F 一律 9001
- scripts/publish_apk.py 负责复制 APK 并写 latest.json(sha256/size/published_at),
backend/static/apk/ 不进版本库
安卓
- AppUpdater:查清单 → 下到应用自己的 updates 目录(先写 .part 再改名)→
校验 sha256 → 经 FileProvider 交给系统安装器
- UpdateDialog:版本号、更新说明、下载进度、失败重试;「允许安装未知应用」
是系统开关,只跳设置页,不代点
- 启动时自动查一次,首页底部另有「检查更新」入口
- 图标换成和 Web 的 <Logo/> 同一套图形:墨绿圆角底、三条米白圆头线、陶土点;
同时出 mipmap 各密度 PNG、自适应图标与 512px 预览
- versionCode=2 / versionName=0.2.0;发布签名从 android/keystore.properties 读,
没有该文件时回落调试签名,保证别人 clone 下来也能 assembleRelease
文档
- API.md 第 7 节:两个接口、清单字段、签名与 sha256 的约定、9001
- DEPLOY.md 第 5 节:出包命令(JDK 17 + Gradle 8.9 + 干净的 GRADLE_USER_HOME)、
发布清单、传到服务器、验证
- ROADMAP 第 6 节记录本次范围与本机踩过的构建环境坑
验证
- 后端 pytest 103 passed;ruff check / format --check 全绿
- assembleRelease 成功,app-release.apk 7.4 MB,apksigner 验签显示 CN=Dadao Task
- 本地与生产 GET /app/release/latest?version_code=1 → has_update=true,
version_code=2 → false;下载返回 200 / application/vnd.android.package-archive
This commit is contained in:
@@ -17,6 +17,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.dadao.task.data.Api
|
||||
import com.dadao.task.data.AppRelease
|
||||
import com.dadao.task.data.AppUpdater
|
||||
import com.dadao.task.data.TokenStore
|
||||
import com.dadao.task.data.Workspace
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -31,6 +33,7 @@ import com.dadao.task.ui.NotificationsScreen
|
||||
import com.dadao.task.ui.RichTextEditorScreen
|
||||
import com.dadao.task.ui.TaskListScreen
|
||||
import com.dadao.task.ui.TicketsScreen
|
||||
import com.dadao.task.ui.UpdateDialog
|
||||
import com.dadao.task.ui.WorkspaceScaffold
|
||||
import com.dadao.task.ui.WorkspaceTab
|
||||
|
||||
@@ -80,6 +83,9 @@ private fun AppRoot(joinUri: String?) {
|
||||
var reopenItem by remember { mutableStateOf<String?>(null) }
|
||||
// 上面那条是任务还是知识:决定回哪个页面去重开
|
||||
var reopenKnowledge by remember { mutableStateOf(false) }
|
||||
// 应用内升级:启动后查一次,查到了就把对话框摆出来
|
||||
val updater = remember { AppUpdater(context, api) }
|
||||
var update by remember { mutableStateOf<AppRelease?>(null) }
|
||||
|
||||
// 有本地令牌就先静默校验一次:令牌过期/被吊销要退回登录页,而不是进了主页再崩。
|
||||
LaunchedEffect(screen) {
|
||||
@@ -114,11 +120,33 @@ private fun AppRoot(joinUri: String?) {
|
||||
unread = api.unreadCount(ws.id)
|
||||
}
|
||||
|
||||
// 版本检查:登录成功后查一次就够,查不到(网络、没发布过包)就当没有新版
|
||||
LaunchedEffect(screen) {
|
||||
if (screen != Screen.Home && screen != Screen.Workspace) return@LaunchedEffect
|
||||
if (update != null) return@LaunchedEffect
|
||||
update = runCatching { updater.check() }.getOrNull()
|
||||
}
|
||||
|
||||
/** 手动检查:结果用一句话回给首页显示,查到新版顺便把对话框摆出来。 */
|
||||
suspend fun checkUpdate(): String = runCatching { updater.checkStrict() }.fold(
|
||||
onSuccess = { release ->
|
||||
if (release == null) {
|
||||
"已经是最新版 ${updater.currentVersionName}"
|
||||
} else {
|
||||
update = release
|
||||
"发现新版本 ${release.versionName}"
|
||||
}
|
||||
},
|
||||
onFailure = { "检查更新失败:${it.message ?: "网络不通"}" },
|
||||
)
|
||||
|
||||
when (screen) {
|
||||
Screen.Splash -> Unit
|
||||
Screen.Login -> LoginScreen(api = api, onLoggedIn = { screen = Screen.Home })
|
||||
Screen.Home -> HomeScreen(
|
||||
api = api,
|
||||
versionName = updater.currentVersionName,
|
||||
onCheckUpdate = { checkUpdate() },
|
||||
onLogout = {
|
||||
tokens.clear()
|
||||
screen = Screen.Login
|
||||
@@ -219,4 +247,14 @@ private fun AppRoot(joinUri: String?) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新对话框是独立窗口,摆在哪一层都能盖住当前界面
|
||||
update?.let { release ->
|
||||
UpdateDialog(
|
||||
release = release,
|
||||
currentVersionName = updater.currentVersionName,
|
||||
updater = updater,
|
||||
onLater = { update = null },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user