feat: 灵动岛参数模式(完整/极简)——极简模式验证白名单是否只拦完整岛结构

This commit is contained in:
cbb
2026-10-12 01:47:22 +08:00
parent 4eba6ad687
commit 9fd2287396
4 changed files with 92 additions and 38 deletions
+2 -2
View File
@@ -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 {
@@ -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) {
@@ -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<Boolean, String>? = 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<TransferItem>?, firstFloat: Boolean): String {
private fun islandParam(running: List<TransferItem>?, 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))
@@ -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()