From 9fd2287396bc4eb7b0ed5315388324bb8395c26a Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Mon, 12 Oct 2026 01:47:22 +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=E6=A8=A1=E5=BC=8F=EF=BC=88=E5=AE=8C=E6=95=B4/?= =?UTF-8?q?=E6=9E=81=E7=AE=80=EF=BC=89=E2=80=94=E2=80=94=E6=9E=81=E7=AE=80?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E9=AA=8C=E8=AF=81=E7=99=BD=E5=90=8D=E5=8D=95?= =?UTF-8?q?=E6=98=AF=E5=90=A6=E5=8F=AA=E6=8B=A6=E5=AE=8C=E6=95=B4=E5=B2=9B?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 +- .../main/java/com/smbfast/app/data/Prefs.kt | 5 +- .../smbfast/app/transfer/TransferService.kt | 85 +++++++++++-------- .../smbfast/app/ui/screens/SettingsScreen.kt | 36 ++++++++ 4 files changed, 92 insertions(+), 38 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 51c2b82..a0a2e46 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 = 24 - versionName = "1.20.0" + versionCode = 25 + versionName = "1.21.0" } signingConfigs { diff --git a/app/src/main/java/com/smbfast/app/data/Prefs.kt b/app/src/main/java/com/smbfast/app/data/Prefs.kt index 6f8c1c4..0524530 100644 --- a/app/src/main/java/com/smbfast/app/data/Prefs.kt +++ b/app/src/main/java/com/smbfast/app/data/Prefs.kt @@ -18,6 +18,7 @@ data class SpeedSettings( var streams: Int = 4, // 并行流数(每个流独立 SMB 句柄,分块并行下载) var chunkKB: Int = 1024, // 每个请求的分块大小 KB var islandOn: Boolean = true, // HyperOS 灵动岛/焦点通知 + var islandMode: String? = null, // 岛参数模式:full=完整岛结构 minimal=极简(仅baseInfo/ticker,白名单实验);null 视为 full var updateUrl: String = "http://192.168.31.3:8080/", // 应用内快捷更新的分发服务器 ) @@ -51,7 +52,9 @@ class Prefs(context: Context) { fun loadSettings(): SpeedSettings { val raw = loadJson("settings") ?: return SpeedSettings() return runCatching { gson.fromJson(raw, SpeedSettings::class.java) } - .getOrNull() ?: SpeedSettings() + .getOrNull() + ?.also { s -> runCatching { if (s.islandMode.isNullOrBlank()) s.islandMode = "full" } } + ?: SpeedSettings() } fun saveSettings(s: SpeedSettings) { diff --git a/app/src/main/java/com/smbfast/app/transfer/TransferService.kt b/app/src/main/java/com/smbfast/app/transfer/TransferService.kt index 559b69d..fb607d7 100644 --- a/app/src/main/java/com/smbfast/app/transfer/TransferService.kt +++ b/app/src/main/java/com/smbfast/app/transfer/TransferService.kt @@ -159,7 +159,8 @@ class TransferService : Service() { ) b.addAction(NotificationCompat.Action(android.R.drawable.ic_delete, "全部停止", stopIntent)) } - if (islandOn()) { + val isl = islandSettings() + if (isl != null && isl.second != MODE_MINIMAL) { runCatching { val extras = Bundle() extras.putBundle("miui.focus.pics", islandPics()) @@ -167,21 +168,23 @@ class TransferService : Service() { } } val n = b.build() - if (islandOn()) { + if (isl != null) { // 小米 HyperOS 焦点通知/超级岛:客户端本地上岛(官方 extras 协议)。 // 注意:HyperOS 对焦点通知有系统级白名单,侧载应用携带岛参数时可能被 // 系统整体过滤(岛和通知都不显示)——在设置里关岛开关即可恢复普通通知。 // islandFirstFloat 只在第一条通知为 true,否则每次更新都会重新弹出岛。 val first = firstIslandNotify - runCatching { n.extras.putString("miui.focus.param", islandParam(running, first)) } + runCatching { n.extras.putString("miui.focus.param", islandParam(running, first, isl.second)) } firstIslandNotify = false } return n } - private fun islandOn(): Boolean = - runCatching { com.smbfast.app.data.Prefs(applicationContext).loadSettings().islandOn } - .getOrDefault(true) + /** 读取岛设置:(开关, 参数模式);读取失败返回 null 表示不带岛参数 */ + private fun islandSettings(): Pair? = runCatching { + val s = com.smbfast.app.data.Prefs(applicationContext).loadSettings() + s.islandOn to (s.islandMode ?: MODE_FULL) + }.getOrNull() /** 岛内图标:直接复用应用图标 */ private fun islandPics(): Bundle = Bundle().apply { @@ -192,12 +195,13 @@ class TransferService : Service() { /** * 超级岛/焦点通知参数(miui.focus.param): - * - HyperOS 3 读 param_v2.param_island(屏幕顶部大/小岛) - * - HyperOS 2 读 param.baseInfo(状态栏焦点) - * 空闲(无任务)时展示"就绪"岛,让常驻通知不传文件也能上岛。 - * 每次更新同一通知 id + sequence 递增即可刷新岛内容。 + * - MODE_FULL:param_v2 全量(param_island 大/小岛 + pics + baseInfo),HyperOS 3 超级岛; + * 实测会被白名单整体过滤(通知都消失) + * - MODE_MINIMAL:param_v2 仅 baseInfo/ticker/aodTitle,无任何岛结构与图片引用, + * 用于验证白名单校验是否只在完整岛结构时触发(实验二) + * 空闲(无任务)时展示"就绪"内容。每次更新同一通知 id + sequence 递增即可刷新。 */ - private fun islandParam(running: List?, firstFloat: Boolean): String { + private fun islandParam(running: List?, firstFloat: Boolean, mode: String): String { islandSequence += 1 val idle = running.isNullOrEmpty() val up = running?.count { it.direction == Direction.UPLOAD } ?: 0 @@ -218,43 +222,48 @@ class TransferService : Service() { val islandContent = if (idle) "后台就绪 · 等待任务" else "$speedText · $size 个任务" val first = running?.firstOrNull()?.name ?: "SMBFast" - val textInfo = JSONObject() - .put("frontTitle", dirText) - .put("title", islandTitle) - .put("content", islandContent) - .put("useHighLight", false) - - val picInfo = JSONObject().put("type", 1).put("pic", "miui.focus.pic_imageText") - val bigLeft = JSONObject() - .put("type", 1) - .put("picInfo", picInfo) - .put("textInfo", textInfo) - - val paramIsland = JSONObject() - .put("islandProperty", 1) - .put("bigIslandArea", JSONObject().put("imageTextInfoLeft", bigLeft).put("picInfo", picInfo)) - .put("smallIslandArea", JSONObject().put("picInfo", picInfo)) - val baseInfo = JSONObject() .put("title", dirText) .put("content", if (idle) "后台就绪" else "$first · $pct% · $speedText") .put("colorTitle", "#22D3EE") .put("type", 2) + val tickerText = if (idle) "SMBFast 就绪" else "$dirText $pct% · $speedText" val paramV2 = JSONObject() .put("protocol", 1) .put("business", "smbfast_transfer") .put("updatable", true) - .put("enableFloat", true) // 启用浮动(胶囊/浮窗形态);false 会压住岛不展示 - .put("islandFirstFloat", firstFloat) // 仅首条通知展开一次 .put("sequence", islandSequence) - .put("ticker", if (idle) "SMBFast 就绪" else "$dirText $pct% · $speedText") - .put("aodTitle", if (idle) "SMBFast 就绪" else "$dirText $pct% · $speedText") - .put("tickerPic", "miui.focus.pic_ticker") - .put("aodPic", "miui.focus.pic_ticker") - .put("param_island", paramIsland) + .put("ticker", tickerText) + .put("aodTitle", tickerText) .put("baseInfo", baseInfo) + if (mode != MODE_MINIMAL) { + val textInfo = JSONObject() + .put("frontTitle", dirText) + .put("title", islandTitle) + .put("content", islandContent) + .put("useHighLight", false) + + val picInfo = JSONObject().put("type", 1).put("pic", "miui.focus.pic_imageText") + val bigLeft = JSONObject() + .put("type", 1) + .put("picInfo", picInfo) + .put("textInfo", textInfo) + + val paramIsland = JSONObject() + .put("islandProperty", 1) + .put("bigIslandArea", JSONObject().put("imageTextInfoLeft", bigLeft).put("picInfo", picInfo)) + .put("smallIslandArea", JSONObject().put("picInfo", picInfo)) + + paramV2 + .put("enableFloat", true) // 启用浮动(胶囊/浮窗形态);false 会压住岛不展示 + .put("islandFirstFloat", firstFloat) // 仅首条通知展开一次 + .put("tickerPic", "miui.focus.pic_ticker") + .put("aodPic", "miui.focus.pic_ticker") + .put("param_island", paramIsland) + } + // 单一根节点 param_v2:曾测试过 param+param_v2 双轨,HyperOS 解析异常会把 // 整条通知吞掉(比不用岛更糟),故保持单一根节点 return JSONObject().put("param_v2", paramV2).toString() @@ -308,6 +317,12 @@ class TransferService : Service() { const val ACTION_CANCEL_ALL = "com.smbfast.app.transfer.CANCEL_ALL" const val ACTION_PERSISTENT = "com.smbfast.app.transfer.PERSISTENT" + /** 岛参数模式:完整岛结构(默认,会被白名单过滤) */ + const val MODE_FULL = "full" + + /** 岛参数模式:极简(仅 baseInfo/ticker,无岛结构,白名单实验二) */ + const val MODE_MINIMAL = "minimal" + /** 有任务入队时调用;须在应用处于前台时启动(前台服务启动限制) */ fun start(ctx: Context) { ContextCompat.startForegroundService(ctx, Intent(ctx, TransferService::class.java)) diff --git a/app/src/main/java/com/smbfast/app/ui/screens/SettingsScreen.kt b/app/src/main/java/com/smbfast/app/ui/screens/SettingsScreen.kt index 88a6f6d..3c92fd5 100644 --- a/app/src/main/java/com/smbfast/app/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/com/smbfast/app/ui/screens/SettingsScreen.kt @@ -191,6 +191,42 @@ fun SettingsScreen(vm: AppViewModel) { fontSize = 13.sp, ) } + if (s.islandOn) { + Spacer(Modifier.height(10.dp)) + Text("参数模式(切换后立即重发通知,看状态栏图标是否消失即可判断是否被过滤)", color = Neon.TextDim, fontSize = 11.sp, lineHeight = 15.sp) + Spacer(Modifier.height(6.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + listOf( + "full" to "完整岛", + "minimal" to "极简(实验)", + ).forEach { (mode, label) -> + FilterChip( + selected = (s.islandMode ?: "full") == mode, + onClick = { + vm.updateSettings { it.copy(islandMode = mode) } + runCatching { + com.smbfast.app.transfer.TransferService.startPersistent(ctx) + } + }, + label = { Text(label) }, + colors = FilterChipDefaults.filterChipColors( + containerColor = Color.White.copy(alpha = 0.06f), + labelColor = Neon.TextDim, + selectedContainerColor = Neon.Cyan.copy(alpha = 0.22f), + selectedLabelColor = Neon.Cyan, + ), + ) + } + } + Spacer(Modifier.height(6.dp)) + Text( + "完整岛=含大/小岛结构(已知被白名单拦截);极简=仅 baseInfo/ticker 无岛结构," + + "验证白名单是否只拦完整参数", + color = Neon.TextDim.copy(alpha = 0.7f), + fontSize = 10.sp, + lineHeight = 14.sp, + ) + } } NotificationDiagCard()