Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions android/app/src/main/kotlin/com/exptech/dpip/BgLocationStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ object BgLocationStore {
const val KEY_LAST_REPORT_OK = "last_report_ok"
const val KEY_LAST_REPORT_CODE = "last_report_code"

/** When a report was last *sent*, which is what the throttle measures. */
const val KEY_LAST_SENT_AT = "last_sent_at"

/** How many triggers the throttle has dropped, for the diagnostics page. */
const val KEY_THROTTLED_N = "throttled_n"

fun prefs(context: Context): SharedPreferences =
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)

Expand Down Expand Up @@ -83,6 +89,29 @@ object BgLocationStore {
*/
fun report(context: Context, lat: Double, lng: Double) {
val prefs = prefs(context)

// At most one report a minute, across every trigger.
//
// Four callers fire this independently — the geofence, the alarm
// fallback, and two paths from Dart — and nothing coordinated them, so
// a crossing that arrived alongside an alarm sent two reports seconds
// apart. The server answers 429, the location never lands, and the app
// looks like it stopped reporting: on the device this was found on,
// `last_report_code` was 429 with the geofence armed and a fix in hand.
//
// Measured from KEY_LAST_SENT_AT, not KEY_LAST_REPORT_AT: the latter is
// stamped on every outcome including this one, so gating on it would
// let a burst of triggers push the window ahead of itself and starve
// reporting entirely.
val now = System.currentTimeMillis()
val sent = prefs.getLong(KEY_LAST_SENT_AT, 0L)
if (sent > 0L && now - sent < MIN_REPORT_INTERVAL_MS) {
prefs.edit()
.putInt(KEY_THROTTLED_N, prefs.getInt(KEY_THROTTLED_N, 0) + 1)
.apply()
stamp(prefs, THROTTLED)
return
}
// Stamped even on the way out. These two returns sat *above* the stamp,
// so a run that never had a token was indistinguishable from one that
// never happened — and "never happened" is what the developer page
Expand All @@ -106,14 +135,28 @@ object BgLocationStore {
code = responseCode // fire the request
disconnect()
}
prefs.edit().putLong(KEY_LAST_SENT_AT, now).apply()
} catch (e: Exception) {
// Best-effort; the next trigger retries. The outcome is still
// recorded below — "tried at T and failed" is the diagnostic that
// separates "never fired" from "fires but cannot reach the server".
//
// The exception's name is kept, because -1 on its own is not a
// diagnosis. On a device where the alarm path failed three times
// running while the app's own SSE connections were live, -1 was
// everything the app could say, and it fits UnknownHostException,
// a timeout, a TLS failure and a Doze network block equally well.
noteWake(context, "report failed: ${e.javaClass.simpleName}")
}
stamp(prefs, code)
}

/** Dropped by the throttle — a report went out less than a minute ago. */
const val THROTTLED = -4

/** The floor between two reports, whichever trigger asks. */
const val MIN_REPORT_INTERVAL_MS = 60_000L

/** No push token stored — the report has nowhere to go. */
const val NO_TOKEN = -2

Expand Down
18 changes: 18 additions & 0 deletions android/app/src/main/res/raw/keep.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Keeps the notification sounds in a shrunk build.

Every channel names its sound as a runtime string — `resource://raw/eew` in
lib/core/notifications/notification_channels.dart — so nothing in code or XML
ever references @raw/*, the resource shrinker concludes they are unused, and
a release APK ships without them. Android then refuses the channel outright:

PlatformException(INVALID_ARGUMENTS, Audio media is not valid,
arguments.invalid.channel.sound, null)

On the device this was found on, that rejected 16 of 24 channels — every
earthquake early-warning channel among them — while the debug build, which is
not shrunk, registered all 24. tool/check/notification_sounds.sh could not
catch it: it reads the source tree, and the source tree is correct.
-->
<resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@raw/*" />
4 changes: 4 additions & 0 deletions lib/core/diagnostics/diagnostics_report.dart
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ String _lastReport(Map<String, Object?> d) {
String _outcome(bool ok, Object? code) => switch (code) {
-2 => 'no push token',
-3 => 'no app version',
// Not a failure: a report went out inside the last minute and this trigger
// was dropped on purpose. Without a name it reads as an error code, and the
// page exists so nobody has to guess what a number means.
-4 => 'throttled (a report went out under a minute ago)',
-1 => 'could not reach the server',
final int c when c > 0 => ok ? 'ok ($c)' : 'failed ($c)',
_ => ok ? 'ok' : 'failed',
Expand Down
Loading