Critical fixes from logical-flaw sweep (round 2)
The evaluator test caught a real critical bug and the independent review found more:
- CoMovementEvaluator: DEDUP seed was Long.MIN_VALUE; timestamp - MIN_VALUE overflows negative, so the first sighting never counted, effective was ALWAYS 0, and the detector could NEVER alert. Now nullable-seeded. (Unit test now passes.)
- ScanService: alerts used the LOW-importance foreground channel, so on Android 8+ a real 'following you' warning made NO sound/heads-up and overwrote the ongoing notification. Added a dedicated HIGH-importance alert channel + distinct, dismissable notification IDs.
- TrackerParser (Apple): required the full offline-finding frame (>=25B). Short 'nearby' frames were falling back to using the type/status bytes as identity, merging many devices into one phantom 'tracker' that could false-alert forever.
- TrackerRepository: assess co-movement BEFORE baseline; NEVER baseline-trust a co-moving tag (a planted stalker tag co-moves), and auto-revoke trust if a trusted tag starts moving with you. setApproved/clearBaseline now under the same Mutex so record() can't clobber the user's choice.
- BaselineManager already switched to dwell (round 1); UI now offers an un-trust ('Not mine') control for baseline rows (clearBaseline DAO).
- BleTrackerScanner: match by service DATA presence, not service-UUID list (Samsung/Tile may not advertise the UUID entry — were potentially invisible).
- PresenceEngine: RSSI coherence now GATES the CONFIRMED tier (crowd/transit can't false-confirm).
- ScanService: fixed location-update leak on failed scan start (idempotent end()); prune runs at start not only after 6h.
- MainActivity: POST_NOTIFICATIONS denial no longer blocks scanning (BLE+location are essential; notifications optional).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187UiEtasyowhEBYs6s9iF5
This commit is contained in:
@@ -59,11 +59,14 @@ object CoMovementEvaluator {
|
||||
val sorted = sightings.sortedBy { it.timestamp }
|
||||
|
||||
// Debounce: one effective sighting per DEDUP_MS so a chatty tag at 2s
|
||||
// intervals doesn't trivially clear the count.
|
||||
// intervals doesn't trivially clear the count. lastCounted is NULLABLE and
|
||||
// always counts the first sighting — the old `Long.MIN_VALUE` seed overflowed
|
||||
// (timestamp - Long.MIN_VALUE wraps negative), which pinned `effective` at 0
|
||||
// and made the detector unable to EVER alert. (Caught by unit test.)
|
||||
var effective = 0
|
||||
var lastCounted = Long.MIN_VALUE
|
||||
var lastCounted: Long? = null
|
||||
for (s in sorted) {
|
||||
if (s.timestamp - lastCounted >= DEDUP_MS) {
|
||||
if (lastCounted == null || s.timestamp - lastCounted >= DEDUP_MS) {
|
||||
effective++
|
||||
lastCounted = s.timestamp
|
||||
}
|
||||
|
||||
@@ -137,8 +137,11 @@ class PresenceEngine {
|
||||
score += (cells.size.toDouble() / (2.0 * K_CELLS)).coerceIn(0.0, 1.0) * 20
|
||||
score += (if (coherent) 1.0 else 0.4) * 20
|
||||
val s = score.roundToInt()
|
||||
// Coherence GATES the top tier: a crowd of many radios at many distances is
|
||||
// incoherent (high RSSI variance), so it can reach PROBABLE at most, never
|
||||
// CONFIRMED — this stops packed-transit false clone alarms.
|
||||
val tier = when {
|
||||
s >= 85 -> Tier.CONFIRMED
|
||||
s >= 85 && coherent -> Tier.CONFIRMED
|
||||
s >= 70 -> Tier.PROBABLE
|
||||
s >= 40 -> Tier.WATCHING
|
||||
else -> Tier.CLEAR
|
||||
|
||||
Reference in New Issue
Block a user