From 340df5a05cbc461c9ba37ce1e021357f8eeacac4 Mon Sep 17 00:00:00 2001 From: sssnake Date: Sun, 12 Jul 2026 11:10:20 -0700 Subject: [PATCH] Fix notification spam, stale frame count, and neighbor flicker - Post the ongoing status on a fresh LOW channel (cellguard_status_v2) and delete the legacy channel, whose importance was stuck at HIGH (a channel's importance is immutable after creation) and heads-up'd on every 4s poll. - Read the live capture total from the /proc/cgcap/frames header instead of the detector's snapshot, which could lag behind. - Accumulate neighbor towers across polls with a 120s TTL instead of publishing the raw per-poll getAllCellInfo list, which blinked empty whenever a scan returned only the serving cell. --- .../cellguard/service/DetectorService.kt | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/seteclabs/cellguard/service/DetectorService.kt b/app/src/main/java/com/seteclabs/cellguard/service/DetectorService.kt index a71ac5c..19e83a6 100644 --- a/app/src/main/java/com/seteclabs/cellguard/service/DetectorService.kt +++ b/app/src/main/java/com/seteclabs/cellguard/service/DetectorService.kt @@ -105,7 +105,8 @@ class DetectorService : Service() { val frameTowers = FrameDecoder.decodeFrames(framesText) val towers = if (fwTowers.isNotEmpty()) fwTowers else frameTowers val serving = towers.firstOrNull { it.registered } ?: towers.firstOrNull() - val neighbors = towers.filter { it !== serving && !it.registered }.distinct() + val currentNeighbors = towers.filter { it !== serving && !it.registered }.distinct() + val neighbors = mergeNeighbors(currentNeighbors, serving) val servingPlmn = status.plmn.takeIf { it.isNotBlank() && it != "?" } ?: FrameDecoder.firstPlmn(framesText) @@ -128,7 +129,7 @@ class DetectorService : Service() { plmn = plmn, operator = operator, rat = rat, - frames = status.frames, + frames = parseTotal(framesText) ?: status.frames, blockEnabled = status.block, detectEnabled = status.detect, ratLock = status.ratLock, @@ -137,6 +138,35 @@ class DetectorService : Service() { ) } + private data class SeenTower(val tower: Tower, val lastSeen: Long) + private val neighborCache = LinkedHashMap() + + private fun towerKey(t: Tower): String = + "${t.rat}|${t.mcc}${t.mnc}|${t.ci}|${t.pci}|${t.earfcn}" + + /** + * Neighbor towers arrive per-poll from getAllCellInfo(), which frequently + * reports only the serving cell on any given scan. Publishing the raw per-poll + * list makes the table blink empty every 4s. Instead accumulate seen neighbors + * keyed by identity and age them out after [NEIGHBOR_TTL_MS], so a tower stays + * listed (with its most recent signal) until it is genuinely gone. + */ + private fun mergeNeighbors(current: List, serving: Tower?): List { + val now = System.currentTimeMillis() + current.forEach { neighborCache[towerKey(it)] = SeenTower(it, now) } + val servingKey = serving?.let { towerKey(it) } + neighborCache.entries.removeAll { (k, v) -> + k == servingKey || now - v.lastSeen > NEIGHBOR_TTL_MS + } + return neighborCache.values.sortedByDescending { it.lastSeen }.map { it.tower } + } + + /** True live capture count from the `# total=N` header line of /proc/cgcap/frames. */ + private fun parseTotal(framesText: String): Long? { + val line = framesText.lineSequence().firstOrNull { it.startsWith("# total=") } ?: return null + return Regex("total=(\\d+)").find(line)?.groupValues?.get(1)?.toLongOrNull() + } + /** On-device heuristics merged with the shell detector's `suspect` token. */ private fun buildSuspects( towers: List, @@ -192,6 +222,11 @@ class DetectorService : Service() { private fun createChannel() { val mgr = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + // A channel's importance is immutable after creation. Earlier builds created + // "cellguard" at HIGH, so the ongoing status notification heads-up/sounds on + // every 4s poll (the reported spam). The only fix is to abandon that id: + // delete the legacy channel and post the silent status on a fresh LOW one. + runCatching { mgr.deleteNotificationChannel(LEGACY_CHANNEL_ID) } // Ongoing status: LOW importance -> silent, no heads-up, no per-poll spam. if (mgr.getNotificationChannel(CHANNEL_ID) == null) { mgr.createNotificationChannel( @@ -315,11 +350,13 @@ class DetectorService : Service() { PackageManager.PERMISSION_GRANTED companion object { - const val CHANNEL_ID = "cellguard" + const val CHANNEL_ID = "cellguard_status_v2" + private const val LEGACY_CHANNEL_ID = "cellguard" const val ALERT_CHANNEL_ID = "cellguard_alert" private const val NOTIF_ID = 0x0C6D private const val ALERT_NOTIF_ID = 0x0C6E private const val POLL_MS = 4_000L + private const val NEIGHBOR_TTL_MS = 120_000L private val ALERT_FMT = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)