feat: 灵动岛参数模式(完整/极简)——极简模式验证白名单是否只拦完整岛结构
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user