From 5d2944ac2600af6ddbb049b8a95daae5153c1b37 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Mon, 12 Oct 2026 02:08:50 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=80=9A=E7=9F=A5=E4=B8=8E?= =?UTF-8?q?=E5=89=8D=E5=8F=B0=E6=9C=8D=E5=8A=A1=E8=A7=A3=E8=80=A6=E2=80=94?= =?UTF-8?q?=E2=80=94Notifier=20=E7=9B=B4=E5=8F=91=E8=BF=9B=E5=BA=A6/?= =?UTF-8?q?=E5=B8=B8=E9=A9=BB=E9=80=9A=E7=9F=A5=EF=BC=8CFGS=20=E4=BB=85?= =?UTF-8?q?=E4=BF=9D=E6=B4=BB=EF=BC=88=E8=A7=84=E9=81=BF=20dataSync=20?= =?UTF-8?q?=E9=85=8D=E9=A2=9D/=E4=BE=A7=E8=BD=BD=E9=99=90=E5=88=B6?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E9=80=9A=E7=9F=A5=E5=85=A8=E7=81=AD=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/java/com/smbfast/app/App.kt | 13 +- .../java/com/smbfast/app/transfer/Notifier.kt | 244 ++++++++++++++ .../smbfast/app/transfer/TransferManager.kt | 26 ++ .../smbfast/app/transfer/TransferService.kt | 307 ++---------------- .../smbfast/app/ui/screens/SettingsScreen.kt | 8 +- 6 files changed, 308 insertions(+), 294 deletions(-) create mode 100644 app/src/main/java/com/smbfast/app/transfer/Notifier.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2629020..6c1789d 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 = 26 - versionName = "1.21.1" + versionCode = 27 + versionName = "1.22.0" } signingConfigs { diff --git a/app/src/main/java/com/smbfast/app/App.kt b/app/src/main/java/com/smbfast/app/App.kt index b90484a..931c3ac 100644 --- a/app/src/main/java/com/smbfast/app/App.kt +++ b/app/src/main/java/com/smbfast/app/App.kt @@ -2,8 +2,9 @@ package com.smbfast.app import android.app.Application import com.smbfast.app.smb.SmbClientPool +import com.smbfast.app.transfer.Notifier +import com.smbfast.app.transfer.Status import com.smbfast.app.transfer.TransferManager -import com.smbfast.app.transfer.TransferService import com.smbfast.app.util.CrashLog import org.bouncycastle.jce.provider.BouncyCastleProvider import java.security.Security @@ -22,10 +23,12 @@ class App : Application() { Security.addProvider(BouncyCastleProvider()) } } - // 常驻前台通知:应用一开就显示(含灵动岛自检),无任务时保持"就绪", - // 不再要求必须有传输任务才能看到岛。后台进程被拉起时启动前台服务会被 - // 系统拒绝,静默忽略即可(用户打开应用时自然会成功)。 - runCatching { TransferService.startPersistent(this) } + // 常驻"就绪"通知(含灵动岛自检):普通通知直发,不启动前台服务—— + // 避免在 FGS 被系统限制(dataSync 配额/侧载策略)时整条链路静默失效 + runCatching { + val running = transferManager.items.value.filter { it.status == Status.RUNNING } + Notifier.notifyState(this, running) + } } override fun onTerminate() { diff --git a/app/src/main/java/com/smbfast/app/transfer/Notifier.kt b/app/src/main/java/com/smbfast/app/transfer/Notifier.kt new file mode 100644 index 0000000..b4b841d --- /dev/null +++ b/app/src/main/java/com/smbfast/app/transfer/Notifier.kt @@ -0,0 +1,244 @@ +package com.smbfast.app.transfer + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Icon +import android.os.Build +import android.os.Bundle +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.smbfast.app.MainActivity +import com.smbfast.app.R +import com.smbfast.app.data.Prefs +import org.json.JSONObject + +/** + * 传输通知中心:进度/空闲通知统一由这里构建与发送,不依赖前台服务存活。 + * 这样即使 HyperOS 拒绝启动前台服务(如 dataSync 24h 配额耗尽、侧载限制), + * 下载/上传进度通知依然可见;服务只负责保活。 + */ +object Notifier { + + const val CHANNEL_ID = "smbfast_transfers" + const val NOTIF_ID = 1001 + const val NOTIF_ID_TEST = 1002 + + /** 岛参数模式:完整岛结构(默认,会被白名单过滤) */ + const val MODE_FULL = "full" + + /** 岛参数模式:极简(仅 baseInfo/ticker,无岛结构,白名单实验二) */ + const val MODE_MINIMAL = "minimal" + + private var islandSequence = 0L + + fun createChannel(ctx: Context) { + val ch = NotificationChannel(CHANNEL_ID, "传输进度", NotificationManager.IMPORTANCE_LOW) + ch.description = "SMB 后台传输进行中" + ch.setShowBadge(false) + ctx.getSystemService(NotificationManager::class.java).createNotificationChannel(ch) + } + + /** (岛开关, 参数模式);读取失败视为关岛 */ + fun islandSettings(ctx: Context): Pair? = runCatching { + val s = Prefs(ctx).loadSettings() + s.islandOn to (s.islandMode ?: MODE_FULL) + }.getOrNull() + + /** 发送当前状态的通知:running 为空即"就绪"常驻态 */ + fun notifyState(ctx: Context, running: List) { + if (Build.VERSION.SDK_INT >= 26) createChannel(ctx) + val n = build(ctx, running) + runCatching { NotificationManagerCompat.from(ctx).notify(NOTIF_ID, n) } + } + + /** 按当前任务状态立即重发一条通知(设置切换岛开关/模式后调用) */ + fun refresh(ctx: Context) { + val running = runCatching { + (ctx.applicationContext as? com.smbfast.app.App) + ?.transferManager?.items?.value + ?.filter { it.status == Status.RUNNING } + }.getOrNull() ?: emptyList() + notifyState(ctx, running) + } + + /** 发一条独立 id 的测试通知,验证通道可见性。返回是否成功 */ + fun sendTestNotification(ctx: Context): Boolean { + createChannel(ctx) + val time = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.CHINA) + .format(java.util.Date()) + val n = NotificationCompat.Builder(ctx, CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle("SMBFast 测试通知") + .setContentText("看到这条即通道可见 · $time") + .setOngoing(false) + .setAutoCancel(true) + .build() + return runCatching { + NotificationManagerCompat.from(ctx).notify(NOTIF_ID_TEST, n) + true + }.getOrDefault(false) + } + + /** 通道一旦被用户/系统屏蔽,应用无法改回;删除重建可恢复默认重要性 */ + fun repairChannel(ctx: Context) { + val nm = ctx.getSystemService(NotificationManager::class.java) + runCatching { nm.deleteNotificationChannel(CHANNEL_ID) } + createChannel(ctx) + } + + fun build(ctx: Context, running: List): Notification { + val contentIntent = PendingIntent.getActivity( + ctx, 0, Intent(ctx, MainActivity::class.java), + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + val up = running.count { it.direction == Direction.UPLOAD } + val down = running.count { it.direction == Direction.DOWNLOAD } + val speed = running.sumOf { it.speedBps } + val title = when { + running.isEmpty() -> "SMBFast 常驻服务" + up > 0 && down > 0 -> "后台传输中(↑$up ↓$down)" + up > 0 -> "后台上传中 ×$up" + else -> "后台下载中 ×$down" + } + val text = when { + running.isEmpty() -> "灵动岛/通知自检 · 等待传输任务" + running.size == 1 -> "${running[0].name} · ${human(speed)}" + else -> "${running[0].name} 等 ${running.size} 个任务 · ${human(speed)}" + } + val b = NotificationCompat.Builder(ctx, CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(title) + .setContentText(text) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setContentIntent(contentIntent) + if (running.isNotEmpty()) { + val total = running.sumOf { it.total } + val done = running.sumOf { it.done } + b.setProgress( + 100, + if (total > 0) ((done.toDouble() / total).coerceIn(0.0, 1.0) * 100).toInt() else 0, + total <= 0, + ) + val stopIntent = PendingIntent.getService( + ctx, 1, + Intent(ctx, TransferService::class.java).setAction(TransferService.ACTION_CANCEL_ALL), + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + b.addAction(NotificationCompat.Action(android.R.drawable.ic_delete, "全部停止", stopIntent)) + } + val isl = islandSettings(ctx) + if (isl != null && isl.second != MODE_MINIMAL) { + runCatching { + val extras = Bundle() + extras.putBundle("miui.focus.pics", islandPics(ctx)) + b.addExtras(extras) + } + } + val n = b.build() + if (isl != null) { + // 小米 HyperOS 焦点通知/超级岛:客户端本地上岛(官方 extras 协议)。 + // 注意:HyperOS 对焦点通知有系统级白名单,侧载应用携带岛参数时可能被 + // 系统整体过滤(岛和通知都不显示)——在设置里关岛开关即可恢复普通通知。 + runCatching { n.extras.putString("miui.focus.param", islandParam(running, isl.second)) } + } + return n + } + + /** 岛内图标:直接复用应用图标 */ + private fun islandPics(ctx: Context): Bundle = Bundle().apply { + val icon = Icon.createWithResource(ctx, R.mipmap.ic_launcher) + putParcelable("miui.focus.pic_ticker", icon) + putParcelable("miui.focus.pic_imageText", icon) + } + + /** + * 超级岛/焦点通知参数(miui.focus.param): + * - MODE_FULL:param_v2 全量(param_island 大/小岛 + pics + baseInfo),HyperOS 3 超级岛; + * 实测会被白名单整体过滤(通知都消失) + * - MODE_MINIMAL:param_v2 仅 baseInfo/ticker/aodTitle,无任何岛结构与图片引用, + * 用于验证白名单校验是否只在完整岛结构时触发(实验二) + */ + private fun islandParam(running: List, mode: String): String { + islandSequence += 1 + val idle = running.isEmpty() + val up = running.count { it.direction == Direction.UPLOAD } + val down = running.count { it.direction == Direction.DOWNLOAD } + val speed = running.sumOf { it.speedBps } + val total = running.sumOf { it.total } + val done = running.sumOf { it.done } + val pct = if (total > 0) ((done.toDouble() / total).coerceIn(0.0, 1.0) * 100).toInt() else 0 + val dirText = when { + idle -> "SMBFast" + up > 0 && down > 0 -> "SMB 传输中" + up > 0 -> "SMB 上传中" + else -> "SMB 下载中" + } + val speedText = human(speed) + val islandTitle = if (idle) "就绪" else "$pct%" + val islandContent = if (idle) "后台就绪 · 等待任务" else "$speedText · ${running.size} 个任务" + val first = running.firstOrNull()?.name ?: "SMBFast" + + 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("sequence", islandSequence) + .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", true) // 每次全新通知展开一次 + .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() + } + + private fun human(bps: Long): String { + val kb = bps / 1024.0 + val mb = kb / 1024.0 + val gb = mb / 1024.0 + return when { + gb >= 1 -> String.format("%.1f GB/s", gb) + mb >= 1 -> String.format("%.1f MB/s", mb) + kb >= 1 -> String.format("%.0f KB/s", kb) + else -> "$bps B/s" + } + } +} diff --git a/app/src/main/java/com/smbfast/app/transfer/TransferManager.kt b/app/src/main/java/com/smbfast/app/transfer/TransferManager.kt index 2e9f82e..72338e5 100644 --- a/app/src/main/java/com/smbfast/app/transfer/TransferManager.kt +++ b/app/src/main/java/com/smbfast/app/transfer/TransferManager.kt @@ -62,6 +62,32 @@ class TransferManager(private val appContext: Context) { val downloadDir: File by lazy { Files.resolveDownloadDir(appContext) } + init { + // 进度通知与前台服务解耦:即使系统拒绝启动 FGS(dataSync 配额/侧载限制), + // 通知依然由这里直发,下载照常进行 + scope.launch { + var lastNotify = 0L + var hadRunning = false + items.collect { list -> + val running = list.filter { it.status == Status.RUNNING } + if (running.isNotEmpty()) { + hadRunning = true + val now = System.currentTimeMillis() + if (now - lastNotify >= 1500) { + lastNotify = now + runCatching { Notifier.notifyState(appContext, running) } + .onFailure { com.smbfast.app.util.CrashLog.append("notify", it) } + } + } else if (hadRunning) { + // 全部结束:恢复"就绪"常驻态 + hadRunning = false + lastNotify = 0 + runCatching { Notifier.notifyState(appContext, emptyList()) } + } + } + } + } + fun enqueueDownload( profile: ConnectionProfile, shareName: String, 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 9c3ac17..12eba47 100644 --- a/app/src/main/java/com/smbfast/app/transfer/TransferService.kt +++ b/app/src/main/java/com/smbfast/app/transfer/TransferService.kt @@ -1,97 +1,65 @@ package com.smbfast.app.transfer -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent import android.app.Service import android.content.Context import android.content.Intent import android.content.pm.ServiceInfo -import android.graphics.drawable.Icon import android.net.wifi.WifiManager import android.net.wifi.WifiManager.WifiLock import android.os.Build -import android.os.Bundle import android.os.IBinder import android.os.PowerManager -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat import androidx.core.app.ServiceCompat import androidx.core.content.ContextCompat import com.smbfast.app.App -import com.smbfast.app.MainActivity -import com.smbfast.app.R +import com.smbfast.app.util.CrashLog import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import org.json.JSONObject /** - * 传输前台服务:应用退到后台时保活进程并持有 WiFi/CPU 锁, - * 避免系统省电机制掐断 SMB 连接(SocketException: Software caused connection abort)。 - * 常驻模式(startPersistent,应用启动即拉起):无任务时显示"就绪"通知并保持服务, - * 用于常显灵动岛/通知自检;普通模式(start)全部任务结束后自动 stopSelf()。 + * 传输保活前台服务:应用退到后台时持有 WiFi/CPU 锁,避免系统省电机制掐断 SMB 连接。 + * + * 注意:通知不在这里发——进度/空闲通知统一由 [Notifier] 直发(与前台服务解耦)。 + * Android 15 对 dataSync 类型前台服务有 24h 累计 6h 的系统配额,侧载应用也可能被 + * HyperOS 拒绝启动;这些情况下传输与通知照常,只是没有保活。 */ class TransferService : Service() { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private var wifiLock: WifiLock? = null private var wakeLock: PowerManager.WakeLock? = null - private var islandSequence = 0L - private var lastNotify = 0L - private var firstIslandNotify = true - - /** 常驻模式:无任务时保留前台通知(灵动岛/通知自检用),不自动停服 */ - @Volatile private var persistent = false private var locksHeld = false override fun onCreate() { super.onCreate() - createChannel() + Notifier.createChannel(this) + val running = App.transferManager.items.value.filter { it.status == Status.RUNNING } val type = if (Build.VERSION.SDK_INT >= 29) ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else 0 - val initial = App.transferManager.items.value.filter { it.status == Status.RUNNING } - ServiceCompat.startForeground(this, NOTIF_ID, buildNotification(initial.ifEmpty { null }), type) + try { + ServiceCompat.startForeground(this, Notifier.NOTIF_ID, Notifier.build(this, running), type) + } catch (e: Exception) { + // 配额耗尽/类型被禁:放弃保活,传输与通知由 TransferManager 继续 + CrashLog.append("startForeground", e) + stopSelf() + return + } acquireLocks() - val tm = App.transferManager scope.launch { - tm.items.collect { items -> - val running = items.filter { it.status == Status.RUNNING } - val now = System.currentTimeMillis() - if (running.isEmpty()) { - firstIslandNotify = true + App.transferManager.items.collect { items -> + if (items.none { it.status == Status.RUNNING }) { releaseLocks() - if (!persistent) { - stopSelf() - return@collect - } - if (now - lastNotify >= NOTIFY_INTERVAL_MS) { - lastNotify = now - notify(buildNotification(null)) - } - return@collect - } - acquireLocks() - // 限频刷新:高频 notify 会不停触发横幅/岛动画 - if (now - lastNotify >= NOTIFY_INTERVAL_MS) { - lastNotify = now - notify(buildNotification(running)) + stopSelf() } } } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - when (intent?.action) { - ACTION_PERSISTENT -> { - persistent = true - // 用当前设置立即重发一条,便于开关岛后即时验证 - val running = App.transferManager.items.value.filter { it.status == Status.RUNNING } - notify(buildNotification(running.ifEmpty { null })) - } - ACTION_CANCEL_ALL -> App.transferManager.cancelAllRunning() + if (intent?.action == ACTION_CANCEL_ALL) { + App.transferManager.cancelAllRunning() } return START_STICKY } @@ -104,168 +72,6 @@ class TransferService : Service() { override fun onBind(intent: Intent?): IBinder? = null - private fun createChannel() { - if (Build.VERSION.SDK_INT >= 26) { - createChannelStatic(this) - } - } - - private fun notify(n: Notification) { - // 未授予通知权限时任务照跑,只是通知条不显示 - runCatching { NotificationManagerCompat.from(this).notify(NOTIF_ID, n) } - } - - private fun buildNotification(running: List?): Notification { - val contentIntent = PendingIntent.getActivity( - this, 0, Intent(this, MainActivity::class.java), - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, - ) - val up = running?.count { it.direction == Direction.UPLOAD } ?: 0 - val down = running?.count { it.direction == Direction.DOWNLOAD } ?: 0 - val speed = running?.sumOf { it.speedBps } ?: 0L - val title = when { - running.isNullOrEmpty() -> "SMBFast 常驻服务" - up > 0 && down > 0 -> "后台传输中(↑$up ↓$down)" - up > 0 -> "后台上传中 ×$up" - else -> "后台下载中 ×$down" - } - val text = when { - running.isNullOrEmpty() -> "灵动岛/通知自检 · 等待传输任务" - running.size == 1 -> "${running[0].name} · ${human(speed)}" - else -> "${running[0].name} 等 ${running.size} 个任务 · ${human(speed)}" - } - val b = NotificationCompat.Builder(this, CHANNEL_ID) - .setSmallIcon(R.mipmap.ic_launcher) - .setContentTitle(title) - .setContentText(text) - .setOngoing(true) - .setOnlyAlertOnce(true) - .setContentIntent(contentIntent) - if (!running.isNullOrEmpty()) { - val total = running.sumOf { it.total } - val done = running.sumOf { it.done } - b.setProgress( - 100, - if (total > 0) ((done.toDouble() / total).coerceIn(0.0, 1.0) * 100).toInt() else 0, - total <= 0, - ) - val stopIntent = PendingIntent.getService( - this, 1, - Intent(this, TransferService::class.java).setAction(ACTION_CANCEL_ALL), - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, - ) - b.addAction(NotificationCompat.Action(android.R.drawable.ic_delete, "全部停止", stopIntent)) - } - val isl = islandSettings() - if (isl != null && isl.second != MODE_MINIMAL) { - runCatching { - val extras = Bundle() - extras.putBundle("miui.focus.pics", islandPics()) - b.addExtras(extras) - } - } - val n = b.build() - if (isl != null) { - // 小米 HyperOS 焦点通知/超级岛:客户端本地上岛(官方 extras 协议)。 - // 注意:HyperOS 对焦点通知有系统级白名单,侧载应用携带岛参数时可能被 - // 系统整体过滤(岛和通知都不显示)——在设置里关岛开关即可恢复普通通知。 - // islandFirstFloat 只在第一条通知为 true,否则每次更新都会重新弹出岛。 - val first = firstIslandNotify - runCatching { n.extras.putString("miui.focus.param", islandParam(running, first, isl.second)) } - firstIslandNotify = false - } - return n - } - - /** 读取岛设置:(开关, 参数模式);读取失败返回 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 { - val icon = Icon.createWithResource(applicationContext, R.mipmap.ic_launcher) - putParcelable("miui.focus.pic_ticker", icon) - putParcelable("miui.focus.pic_imageText", icon) - } - - /** - * 超级岛/焦点通知参数(miui.focus.param): - * - 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, mode: String): String { - islandSequence += 1 - val idle = running.isNullOrEmpty() - val up = running?.count { it.direction == Direction.UPLOAD } ?: 0 - val down = running?.count { it.direction == Direction.DOWNLOAD } ?: 0 - val speed = running?.sumOf { it.speedBps } ?: 0L - val total = running?.sumOf { it.total } ?: 0L - val done = running?.sumOf { it.done } ?: 0L - val size = running?.size ?: 0 - val pct = if (total > 0) ((done.toDouble() / total).coerceIn(0.0, 1.0) * 100).toInt() else 0 - val dirText = when { - idle -> "SMBFast" - up > 0 && down > 0 -> "SMB 传输中" - up > 0 -> "SMB 上传中" - else -> "SMB 下载中" - } - val speedText = human(speed) - val islandTitle = if (idle) "就绪" else "$pct%" - val islandContent = if (idle) "后台就绪 · 等待任务" else "$speedText · $size 个任务" - val first = running?.firstOrNull()?.name ?: "SMBFast" - - 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("sequence", islandSequence) - .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() - } - private fun acquireLocks() { if (locksHeld) return locksHeld = true @@ -295,79 +101,14 @@ class TransferService : Service() { wifiLock = null } - private fun human(bps: Long): String { - val kb = bps / 1024.0 - val mb = kb / 1024.0 - val gb = mb / 1024.0 - return when { - gb >= 1 -> String.format("%.1f GB/s", gb) - mb >= 1 -> String.format("%.1f MB/s", mb) - kb >= 1 -> String.format("%.0f KB/s", kb) - else -> "$bps B/s" - } - } - companion object { - private const val CHANNEL_ID = "smbfast_transfers" - private const val NOTIF_ID = 1001 - private const val NOTIFY_INTERVAL_MS = 1500L 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" - - private const val NOTIF_ID_TEST = 1002 - - private fun createChannelStatic(ctx: Context) { - val ch = NotificationChannel(CHANNEL_ID, "传输进度", NotificationManager.IMPORTANCE_LOW) - ch.description = "SMB 后台传输进行中" - ch.setShowBadge(false) - ctx.getSystemService(NotificationManager::class.java).createNotificationChannel(ch) - } - - /** - * 发一条独立 id 的测试通知:验证通道级可见性(应用权限"已授予"不代表通道没被系统屏蔽)。 - * 返回是否发送成功。 - */ - fun sendTestNotification(ctx: Context): Boolean { - if (Build.VERSION.SDK_INT >= 26) createChannelStatic(ctx) - val time = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.CHINA) - .format(java.util.Date()) - val n = NotificationCompat.Builder(ctx, CHANNEL_ID) - .setSmallIcon(R.mipmap.ic_launcher) - .setContentTitle("SMBFast 测试通知") - .setContentText("看到这条即通道可见 · $time") - .setOngoing(false) - .setAutoCancel(true) - .build() - return runCatching { - NotificationManagerCompat.from(ctx).notify(NOTIF_ID_TEST, n) - true - }.getOrDefault(false) - } - - /** 通道一旦被用户/系统屏蔽,应用无法改回;删除重建可恢复默认重要性 */ - fun repairChannel(ctx: Context) { - val nm = ctx.getSystemService(NotificationManager::class.java) - runCatching { nm.deleteNotificationChannel(CHANNEL_ID) } - if (Build.VERSION.SDK_INT >= 26) createChannelStatic(ctx) - } /** 有任务入队时调用;须在应用处于前台时启动(前台服务启动限制) */ fun start(ctx: Context) { - ContextCompat.startForegroundService(ctx, Intent(ctx, TransferService::class.java)) - } - - /** 应用启动即拉起常驻前台通知(含灵动岛自检),无需有传输任务 */ - fun startPersistent(ctx: Context) { - ContextCompat.startForegroundService( - ctx, - Intent(ctx, TransferService::class.java).setAction(ACTION_PERSISTENT), - ) + runCatching { + ContextCompat.startForegroundService(ctx, Intent(ctx, TransferService::class.java)) + }.onFailure { CrashLog.append("startFgs", it) } } } } 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 b54a045..bc865f1 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 @@ -175,7 +175,7 @@ fun SettingsScreen(vm: AppViewModel) { vm.updateSettings { it.copy(islandOn = checked) } // 用新设置立即重发常驻通知,便于当场验证岛参数是否被系统过滤 runCatching { - com.smbfast.app.transfer.TransferService.startPersistent(ctx) + com.smbfast.app.transfer.Notifier.refresh(ctx) } }, colors = SwitchDefaults.colors( @@ -205,7 +205,7 @@ fun SettingsScreen(vm: AppViewModel) { onClick = { vm.updateSettings { it.copy(islandMode = mode) } runCatching { - com.smbfast.app.transfer.TransferService.startPersistent(ctx) + com.smbfast.app.transfer.Notifier.refresh(ctx) } }, label = { Text(label) }, @@ -347,7 +347,7 @@ private fun NotificationDiagCard() { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { MiniChip("发测试通知") { msg = runCatching { - com.smbfast.app.transfer.TransferService.sendTestNotification(ctx) + com.smbfast.app.transfer.Notifier.sendTestNotification(ctx) "已发出,看状态栏/通知栏是否出现(时间戳区分新旧)" }.getOrElse { "发送失败:${it.message}" } refresh++ @@ -355,7 +355,7 @@ private fun NotificationDiagCard() { if (hasChannel && importance <= 1) { MiniChip("修复通知通道") { msg = runCatching { - com.smbfast.app.transfer.TransferService.repairChannel(ctx) + com.smbfast.app.transfer.Notifier.repairChannel(ctx) "通道已删除并按默认重建,请再点「发测试通知」验证" }.getOrElse { "修复失败:${it.message}" } refresh++