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.
This commit is contained in:
sssnake
2026-07-12 11:10:20 -07:00
parent 38719683c7
commit 340df5a05c

View File

@@ -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<String, SeenTower>()
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<Tower>, serving: Tower?): List<Tower> {
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<Tower>,
@@ -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)