From a34e9f11699dc30a6f6b455579c8df20e6187880 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Mon, 12 Oct 2026 00:18:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=81=B5=E5=8A=A8=E5=B2=9B=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=9B=9E=E9=80=80=E5=8D=95=E4=B8=80=E6=A0=B9=E8=8A=82?= =?UTF-8?q?=E7=82=B9+=E5=B2=9B=E5=BC=80=E5=85=B3=EF=BC=9B=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=BA=94=E7=94=A8=E5=86=85=E5=BF=AB=E6=8D=B7=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=EF=BC=88=E6=A3=80=E6=9F=A5/=E4=B8=8B=E8=BD=BD/?= =?UTF-8?q?=E5=AE=89=E8=A3=85=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 1 + .../main/java/com/smbfast/app/data/Prefs.kt | 2 + .../smbfast/app/transfer/TransferService.kt | 25 +-- .../smbfast/app/ui/screens/SettingsScreen.kt | 161 +++++++++++++++++- .../main/java/com/smbfast/app/util/Updater.kt | 82 +++++++++ app/src/main/res/xml/file_paths.xml | 1 + 7 files changed, 262 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/smbfast/app/util/Updater.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index db166d3..4c0bf63 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "com.smbfast.app" minSdk = 26 targetSdk = 35 - versionCode = 20 - versionName = "1.18.0" + versionCode = 21 + versionName = "1.19.0" } signingConfigs { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c3af654..83140fa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ + + vm.updateSettings { it.copy(islandOn = checked) } + }, + colors = SwitchDefaults.colors( + checkedThumbColor = Color(0xFF04121E), + checkedTrackColor = Neon.Cyan, + uncheckedTrackColor = Color.White.copy(alpha = 0.12f), + ), + ) + Spacer(Modifier.width(12.dp)) + Text( + if (s.islandOn) "已开启" else "已关闭(仅普通通知)", + color = Neon.TextDim, + fontSize = 13.sp, + ) + } + } + + UpdateSection(vm = vm, currentName = currentVersionName(), currentCode = currentVersionCode()) + SectionCard( icon = Icons.Default.Speed, brush = Neon.Sunset, @@ -161,7 +202,7 @@ fun SettingsScreen(vm: AppViewModel) { icon = Icons.Default.Info, brush = Neon.File, title = "隐私提示", - subtitle = "连接密码保存在本机应用私有数据中(未加密)。请勿在共享设备上保存敏感凭据。", + subtitle = "连接密码使用 AndroidKeyStore 加密后保存在本机应用私有数据中,root/备份不可直接读取。", ) {} SectionCard( @@ -220,6 +261,124 @@ fun SettingsScreen(vm: AppViewModel) { } } +@Composable +private fun currentVersionCode(): Long { + val ctx = LocalContext.current + return remember { + val pi = ctx.packageManager.getPackageInfo(ctx.packageName, 0) + if (android.os.Build.VERSION.SDK_INT >= 28) pi.longVersionCode + else @Suppress("DEPRECATION") pi.versionCode.toLong() + } +} + +@Composable +private fun currentVersionName(): String { + val ctx = LocalContext.current + return remember { + ctx.packageManager.getPackageInfo(ctx.packageName, 0).versionName ?: "?" + } +} + +/** 应用内快捷更新:检查 version.json → 下载 APK → 拉起系统安装器 */ +@Composable +private fun UpdateSection(vm: AppViewModel, currentName: String, currentCode: Long) { + val s by vm.settings.collectAsStateWithLifecycle() + val ctx = LocalContext.current + val scope = rememberCoroutineScope() + + var checking by remember { mutableStateOf(false) } + var info by remember { mutableStateOf(null) } + var msg by remember { mutableStateOf(null) } + var progress by remember { mutableStateOf(null) } + + SectionCard( + icon = Icons.Default.CloudDownload, + brush = Neon.Sunset, + title = "应用更新", + subtitle = "当前版本 $currentName ($currentCode) · 从分发服务器一键检查、下载并安装新版本", + ) { + Spacer(Modifier.height(8.dp)) + GlassTextField( + value = s.updateUrl, + onValueChange = { v -> vm.updateSettings { it.copy(updateUrl = v) } }, + label = "分发服务器地址", + ) + Spacer(Modifier.height(10.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + MiniChip(if (checking) "检查中…" else "检查更新") { + if (checking || progress != null) return@MiniChip + checking = true + info = null + msg = null + scope.launch { + Updater.check(s.updateUrl, currentCode).fold( + onSuccess = { found -> + checking = false + if (found == null) { + msg = "已是最新版本($currentName / $currentCode)" + } else { + info = found + msg = "发现新版本 ${found.versionName} (${found.versionCode})" + + (found.note?.let { " · $it" } ?: "") + } + }, + onFailure = { + checking = false + msg = "检查失败:${it.message ?: "网络不可达"}" + }, + ) + } + } + if (info != null) { + MiniChip( + if (progress != null) "下载中 ${((progress ?: 0f) * 100).toInt()}%" + else "下载并安装", + ) { + val cur = info ?: return@MiniChip + if (progress != null) return@MiniChip + progress = 0f + scope.launch { + val dir = ctx.getExternalFilesDir("update") ?: ctx.filesDir + val apk = File(dir, "SmbFast-update.apk") + Updater.download(Updater.resolveUrl(s.updateUrl, cur), apk) { p -> progress = p } + .fold( + onSuccess = { f -> + progress = null + runCatching { + val uri = androidx.core.content.FileProvider.getUriForFile( + ctx, "${ctx.packageName}.fileprovider", f, + ) + val i = android.content.Intent(android.content.Intent.ACTION_VIEW) + .setDataAndType(uri, "application/vnd.android.package-archive") + .addFlags( + android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION + or android.content.Intent.FLAG_ACTIVITY_NEW_TASK, + ) + ctx.startActivity(i) + }.onFailure { msg = "无法启动安装器:${it.message}" } + }, + onFailure = { + progress = null + msg = "下载失败:${it.message ?: "网络错误"}" + }, + ) + } + } + } + } + val m = msg + if (m != null) { + Spacer(Modifier.height(8.dp)) + Text(m, color = Neon.TextDim, fontSize = 11.sp, lineHeight = 15.sp) + } + val p = progress + if (p != null) { + Spacer(Modifier.height(8.dp)) + NeonProgress(fraction = p, modifier = Modifier.fillMaxWidth(), brush = Neon.Primary) + } + } +} + @Composable private fun MiniChip(text: String, onClick: () -> Unit) { Box( diff --git a/app/src/main/java/com/smbfast/app/util/Updater.kt b/app/src/main/java/com/smbfast/app/util/Updater.kt new file mode 100644 index 0000000..2a98b42 --- /dev/null +++ b/app/src/main/java/com/smbfast/app/util/Updater.kt @@ -0,0 +1,82 @@ +package com.smbfast.app.util + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject +import java.io.File +import java.net.HttpURLConnection +import java.net.URL + +/** + * 应用内快捷更新:从分发服务器(dist/ 目录的 HTTP 服务)检查并下载新版本。 + * 服务器需提供 version.json:{"versionCode":21,"versionName":"1.19.0","file":"SmbFast-release-v21.apk","note":"..."} + */ +object Updater { + + data class Info( + val versionCode: Long, + val versionName: String, + val file: String, + val note: String?, + ) + + /** 拉取 {base}/version.json;服务器版本比本地新时返回信息,否则返回 null */ + suspend fun check(base: String, currentCode: Long): Result = withContext(Dispatchers.IO) { + runCatching { + val u = if (base.endsWith("/")) base else "$base/" + val conn = URL(u + "version.json").openConnection() as HttpURLConnection + conn.connectTimeout = 5000 + conn.readTimeout = 5000 + val body = try { + conn.inputStream.use { it.readBytes().decodeToString() } + } finally { + conn.disconnect() + } + val o = JSONObject(body) + val info = Info( + versionCode = o.getLong("versionCode"), + versionName = o.optString("versionName", ""), + file = o.getString("file"), + note = o.optString("note", "").ifBlank { null }, + ) + if (info.versionCode > currentCode) info else null + } + } + + /** 拼出 APK 下载地址(file 为相对名时基于 base) */ + fun resolveUrl(base: String, info: Info): String = + if (info.file.startsWith("http")) info.file + else (if (base.endsWith("/")) base else "$base/") + info.file + + /** 下载 APK 到 dest,onProgress 回调 0~1 */ + suspend fun download(url: String, dest: File, onProgress: (Float) -> Unit): Result = + withContext(Dispatchers.IO) { + runCatching { + val conn = URL(url).openConnection() as HttpURLConnection + conn.connectTimeout = 5000 + conn.readTimeout = 15000 + val total = conn.contentLengthLong + dest.parentFile?.mkdirs() + try { + dest.outputStream().use { out -> + conn.inputStream.use { input -> + val buf = ByteArray(256 * 1024) + var done = 0L + while (true) { + val n = input.read(buf) + if (n < 0) break + out.write(buf, 0, n) + done += n + if (total > 0) { + onProgress((done.toDouble() / total).coerceIn(0.0, 1.0).toFloat()) + } + } + } + } + } finally { + conn.disconnect() + } + dest + } + } +} diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml index e1f3dc3..d557bbe 100644 --- a/app/src/main/res/xml/file_paths.xml +++ b/app/src/main/res/xml/file_paths.xml @@ -3,4 +3,5 @@ +