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" applicationId = "com.smbfast.app"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 24 versionCode = 25
versionName = "1.20.0" versionName = "1.21.0"
} }
signingConfigs { signingConfigs {
@@ -18,6 +18,7 @@ data class SpeedSettings(
var streams: Int = 4, // 并行流数(每个流独立 SMB 句柄,分块并行下载) var streams: Int = 4, // 并行流数(每个流独立 SMB 句柄,分块并行下载)
var chunkKB: Int = 1024, // 每个请求的分块大小 KB var chunkKB: Int = 1024, // 每个请求的分块大小 KB
var islandOn: Boolean = true, // HyperOS 灵动岛/焦点通知 var islandOn: Boolean = true, // HyperOS 灵动岛/焦点通知
var islandMode: String? = null, // 岛参数模式:full=完整岛结构 minimal=极简(仅baseInfo/ticker,白名单实验);null 视为 full
var updateUrl: String = "http://192.168.31.3:8080/", // 应用内快捷更新的分发服务器 var updateUrl: String = "http://192.168.31.3:8080/", // 应用内快捷更新的分发服务器
) )
@@ -51,7 +52,9 @@ class Prefs(context: Context) {
fun loadSettings(): SpeedSettings { fun loadSettings(): SpeedSettings {
val raw = loadJson("settings") ?: return SpeedSettings() val raw = loadJson("settings") ?: return SpeedSettings()
return runCatching { gson.fromJson(raw, SpeedSettings::class.java) } 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) { fun saveSettings(s: SpeedSettings) {
@@ -159,7 +159,8 @@ class TransferService : Service() {
) )
b.addAction(NotificationCompat.Action(android.R.drawable.ic_delete, "全部停止", stopIntent)) b.addAction(NotificationCompat.Action(android.R.drawable.ic_delete, "全部停止", stopIntent))
} }
if (islandOn()) { val isl = islandSettings()
if (isl != null && isl.second != MODE_MINIMAL) {
runCatching { runCatching {
val extras = Bundle() val extras = Bundle()
extras.putBundle("miui.focus.pics", islandPics()) extras.putBundle("miui.focus.pics", islandPics())
@@ -167,21 +168,23 @@ class TransferService : Service() {
} }
} }
val n = b.build() val n = b.build()
if (islandOn()) { if (isl != null) {
// 小米 HyperOS 焦点通知/超级岛:客户端本地上岛(官方 extras 协议)。 // 小米 HyperOS 焦点通知/超级岛:客户端本地上岛(官方 extras 协议)。
// 注意:HyperOS 对焦点通知有系统级白名单,侧载应用携带岛参数时可能被 // 注意:HyperOS 对焦点通知有系统级白名单,侧载应用携带岛参数时可能被
// 系统整体过滤(岛和通知都不显示)——在设置里关岛开关即可恢复普通通知。 // 系统整体过滤(岛和通知都不显示)——在设置里关岛开关即可恢复普通通知。
// islandFirstFloat 只在第一条通知为 true,否则每次更新都会重新弹出岛。 // islandFirstFloat 只在第一条通知为 true,否则每次更新都会重新弹出岛。
val first = firstIslandNotify 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 firstIslandNotify = false
} }
return n return n
} }
private fun islandOn(): Boolean = /** 读取岛设置:(开关, 参数模式);读取失败返回 null 表示不带岛参数 */
runCatching { com.smbfast.app.data.Prefs(applicationContext).loadSettings().islandOn } private fun islandSettings(): Pair<Boolean, String>? = runCatching {
.getOrDefault(true) val s = com.smbfast.app.data.Prefs(applicationContext).loadSettings()
s.islandOn to (s.islandMode ?: MODE_FULL)
}.getOrNull()
/** 岛内图标:直接复用应用图标 */ /** 岛内图标:直接复用应用图标 */
private fun islandPics(): Bundle = Bundle().apply { private fun islandPics(): Bundle = Bundle().apply {
@@ -192,12 +195,13 @@ class TransferService : Service() {
/** /**
* 超级岛/焦点通知参数(miui.focus.param): * 超级岛/焦点通知参数(miui.focus.param):
* - HyperOS 3 读 param_v2.param_island(屏幕顶部大/小岛) * - MODE_FULL:param_v2 全量(param_island 大/小岛 + pics + baseInfo),HyperOS 3 超级岛;
* - HyperOS 2 读 param.baseInfo(状态栏焦点) * 实测会被白名单整体过滤(通知都消失)
* 空闲(无任务)时展示"就绪"岛,让常驻通知不传文件也能上岛。 * - MODE_MINIMAL:param_v2 仅 baseInfo/ticker/aodTitle,无任何岛结构与图片引用,
* 每次更新同一通知 id + sequence 递增即可刷新岛内容。 * 用于验证白名单校验是否只在完整岛结构时触发(实验二)
* 空闲(无任务)时展示"就绪"内容。每次更新同一通知 id + sequence 递增即可刷新。
*/ */
private fun islandParam(running: List<TransferItem>?, firstFloat: Boolean): String { private fun islandParam(running: List<TransferItem>?, firstFloat: Boolean, mode: String): String {
islandSequence += 1 islandSequence += 1
val idle = running.isNullOrEmpty() val idle = running.isNullOrEmpty()
val up = running?.count { it.direction == Direction.UPLOAD } ?: 0 val up = running?.count { it.direction == Direction.UPLOAD } ?: 0
@@ -218,43 +222,48 @@ class TransferService : Service() {
val islandContent = if (idle) "后台就绪 · 等待任务" else "$speedText · $size 个任务" val islandContent = if (idle) "后台就绪 · 等待任务" else "$speedText · $size 个任务"
val first = running?.firstOrNull()?.name ?: "SMBFast" 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() val baseInfo = JSONObject()
.put("title", dirText) .put("title", dirText)
.put("content", if (idle) "后台就绪" else "$first · $pct% · $speedText") .put("content", if (idle) "后台就绪" else "$first · $pct% · $speedText")
.put("colorTitle", "#22D3EE") .put("colorTitle", "#22D3EE")
.put("type", 2) .put("type", 2)
val tickerText = if (idle) "SMBFast 就绪" else "$dirText $pct% · $speedText"
val paramV2 = JSONObject() val paramV2 = JSONObject()
.put("protocol", 1) .put("protocol", 1)
.put("business", "smbfast_transfer") .put("business", "smbfast_transfer")
.put("updatable", true) .put("updatable", true)
.put("enableFloat", true) // 启用浮动(胶囊/浮窗形态);false 会压住岛不展示
.put("islandFirstFloat", firstFloat) // 仅首条通知展开一次
.put("sequence", islandSequence) .put("sequence", islandSequence)
.put("ticker", if (idle) "SMBFast 就绪" else "$dirText $pct% · $speedText") .put("ticker", tickerText)
.put("aodTitle", if (idle) "SMBFast 就绪" else "$dirText $pct% · $speedText") .put("aodTitle", tickerText)
.put("tickerPic", "miui.focus.pic_ticker")
.put("aodPic", "miui.focus.pic_ticker")
.put("param_island", paramIsland)
.put("baseInfo", baseInfo) .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 解析异常会把 // 单一根节点 param_v2:曾测试过 param+param_v2 双轨,HyperOS 解析异常会把
// 整条通知吞掉(比不用岛更糟),故保持单一根节点 // 整条通知吞掉(比不用岛更糟),故保持单一根节点
return JSONObject().put("param_v2", paramV2).toString() 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_CANCEL_ALL = "com.smbfast.app.transfer.CANCEL_ALL"
const val ACTION_PERSISTENT = "com.smbfast.app.transfer.PERSISTENT" 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) { fun start(ctx: Context) {
ContextCompat.startForegroundService(ctx, Intent(ctx, TransferService::class.java)) ContextCompat.startForegroundService(ctx, Intent(ctx, TransferService::class.java))
@@ -191,6 +191,42 @@ fun SettingsScreen(vm: AppViewModel) {
fontSize = 13.sp, 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() NotificationDiagCard()