-
-
Notifications
You must be signed in to change notification settings - Fork 80
Cut idle battery drain on Android + replace hand-rolled logic with platform/stdlib equivalents #262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
cbarrado
wants to merge
8
commits into
OpenStrap:main
Choose a base branch
from
cbarrado:perf/battery-audit
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
603d38f
perf: cut idle battery drain from background BLE, derive, and wakes
cbarrado bb91ecf
refactor: reuse stdlib/native/in-repo helpers over hand-rolled copies
cbarrado 56b8293
docs: record 2026-08 battery audit; add repo-local ponytail skill
cbarrado 6f9561b
fix: unblock bulk health export + serialize bg live downgrade (CR #262)
cbarrado aa2bcda
docs: address CodeRabbit review notes on audit + ponytail skill
cbarrado fb7a5a9
fix: address Codex review — revert step latency, day in widget finger…
cbarrado 211605d
docs: reconcile audit with Codex fixes (step latency, widget updated_at)
cbarrado 31e03a3
docs: carry CR-001 m5 (derive-trigger debouncing) to audit follow-ups
cbarrado File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| --- | ||
| name: ponytail | ||
| description: Lazy-senior-dev discipline for all code written in this repo — YAGNI, reuse-before-write, stdlib/native/dependency before custom, shortest working diff after understanding the real flow. Use whenever writing, modifying, refactoring, or reviewing code in edge. The `ponytail:` comment marker (18 sites in lib/) flags deliberate simplifications with a known ceiling; this skill defines that convention. | ||
| --- | ||
|
|
||
| # Ponytail, lazy senior dev mode | ||
|
|
||
| You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written. | ||
|
|
||
| Before writing any code, stop at the first rung that holds: | ||
|
|
||
| 1. Does this need to be built at all? (YAGNI) | ||
| 2. Does it already exist in this codebase? Reuse the helper, util, or pattern that's already here, don't re-write it. | ||
| 3. Does the standard library already do this? Use it. | ||
| 4. Does a native platform feature cover it? Use it. | ||
| 5. Does an already-installed dependency solve it? Use it. | ||
| 6. Can this be one line? Make it one line. | ||
| 7. Only then: write the minimum code that works. | ||
|
|
||
| The ladder runs after you understand the problem, not instead of it: read the task and the code it touches, trace the real flow end to end, then climb. | ||
|
|
||
| Bug fix = root cause, not symptom: a report names a symptom. Grep every caller of the function you touch and fix the shared function once — one guard there is a smaller diff than one per caller, and patching only the path the ticket names leaves a sibling caller still broken. | ||
|
|
||
| Rules: | ||
|
|
||
| - No abstractions that weren't explicitly requested. | ||
| - No new dependency if it can be avoided. | ||
| - No boilerplate nobody asked for. | ||
| - Deletion over addition. Boring over clever. Fewest files possible. | ||
| - Shortest working diff wins, but only once you understand the problem. The smallest change in the wrong place isn't lazy, it's a second bug. | ||
| - Question complex requests: "Do you actually need X, or does Y cover it?" | ||
| - Pick the edge-case-correct option when two stdlib approaches are the same size, lazy means less code, not the flimsier algorithm. | ||
| - Mark deliberate simplifications that cut a real corner with a known ceiling (global lock, O(n²) scan, naive heuristic) with a `ponytail:` comment naming the ceiling and upgrade path. | ||
|
|
||
| Not lazy about: understanding the problem (read it fully and trace the real flow before picking a rung, a small diff you don't understand is just laziness dressed up as efficiency), input validation at trust boundaries, error handling that prevents data loss, security, accessibility, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: non-trivial logic leaves ONE runnable check behind, the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test. | ||
|
|
||
| ## Repo-specific notes (edge) | ||
|
|
||
| - This repo already carries `ponytail:` markers (grep `ponytail:` under `lib/` and `android/`). Treat each as a documented, deliberate ceiling — do not "fix" one without a real-world trigger, and when you cut a corner yourself, leave the marker. | ||
| - Rung 2 is load-bearing here: pure policies live in `lib/ble/ble_state.dart` and `lib/sync/sync_policy.dart`, the single day-label helper is `lib/data/day_label.dart`, the single notification emitter is `lib/notify/notification_center.dart`. Check those before writing a new detector, policy, or helper. | ||
| - Rung 5 candidates already installed: `clock` (injectable time), `archive` (zip), `pointycastle` (AEAD), `collection` (transitive), `latlong2` (geo math), `workmanager` (background jobs), `flutter_local_notifications` + `timezone` (scheduling). | ||
| - Semantics this repo protects that a "simpler" version must never change: the safe-trim invariant (commit before HISTORY_END ACK), ACK seq discipline, DST-correct day windows, the dangerous-opcode block, and the analytics/protocol pin gates in `lib/compute/derivation_engine.dart`. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 58 additions & 26 deletions
84
android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/EdgeApplication.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,46 +1,78 @@ | ||
| package wtf.openstrap.openstrap_edge | ||
|
|
||
| import android.app.Application | ||
| import android.content.Context | ||
| import io.flutter.embedding.engine.FlutterEngine | ||
| import io.flutter.embedding.engine.FlutterEngineCache | ||
| import io.flutter.embedding.engine.dart.DartExecutor | ||
|
|
||
| /** | ||
| * Pre-warms a single long-lived [FlutterEngine] and caches it. MainActivity attaches | ||
| * to THIS engine (via getCachedEngineId) instead of creating its own, and does NOT | ||
| * destroy it when the Activity is finished (shouldDestroyEngineWithHost = false). | ||
| * Owns the single long-lived [FlutterEngine], created LAZILY via [ensureEngine]. | ||
| * MainActivity attaches to it (via getCachedEngineId) instead of creating its own, | ||
| * and does NOT destroy it when the Activity is finished (shouldDestroyEngineWithHost | ||
| * = false). | ||
| * | ||
| * Why: when the user swipes the app from recents, Android destroys the Activity and, | ||
| * with a default Activity-owned engine, tears the engine down too (onDetachedFromEngine | ||
| * in logcat). That kills the Dart VM — and with it flutter_blue_plus's BLE connection | ||
| * AND the notification-relay stream (the native listener keeps firing but "FlutterJNI | ||
| * detached … could not send"). By retaining the engine here and keeping the process | ||
| * alive with the EdgeTracking foreground service, the Dart side keeps running headless | ||
| * after task removal, so the relay can still buzz the band. | ||
| * Why retained: when the user swipes the app from recents, Android destroys the | ||
| * Activity and, with a default Activity-owned engine, tears the engine down too | ||
| * (onDetachedFromEngine in logcat). That kills the Dart VM — and with it | ||
| * flutter_blue_plus's BLE connection AND the notification-relay stream. By retaining | ||
| * the engine and keeping the process alive with the EdgeTracking foreground service, | ||
| * the Dart side keeps running headless after task removal. | ||
| * | ||
| * The trade-off is RAM: the app stays warm in memory. That's the intended cost of a | ||
| * persistent foreground BLE companion, and matches the existing foreground-service model. | ||
| * Why LAZY (moved out of Application.onCreate): the process is also started by | ||
| * widget update alarms, KeepAliveWorker runs, CDM device-presence binds and Tasker | ||
| * broadcasts — wakes that need ZERO Dart. Widgets render native snapshots from | ||
| * prefs; the worker/CDM paths just start EdgeTrackingService, whose own onCreate | ||
| * calls [ensureEngine]; TaskerReceiver has an engine-dead fallback. Cold-booting a | ||
| * full FlutterEngine + Dart main() for each of those wakes was pure battery burn on | ||
| * exactly the devices (background-restricted / low-RAM) that kill the process most | ||
| * often. Accepted trade-off: the system-bound NotificationListener can cold-start | ||
| * the process engine-less, so relayed notification buzzes drop until the tracking | ||
| * service starts (CDM presence when the band is in range — the only time a buzz | ||
| * can land anyway — or the ≤15 min KeepAliveWorker). | ||
| * | ||
| * The trade-off is RAM while the engine IS up: intended cost of a persistent | ||
| * foreground BLE companion, matching the foreground-service model. | ||
| */ | ||
| class EdgeApplication : Application() { | ||
| companion object { | ||
| const val ENGINE_ID = "openstrap_main_engine" | ||
|
|
||
| /** | ||
| * Create, register, run and cache the shared engine if it doesn't exist | ||
| * yet; return the cached one otherwise. Idempotent. Main-thread only — | ||
| * every caller (Activity/Service onCreate) already is. | ||
| */ | ||
| @JvmStatic | ||
| fun ensureEngine(context: Context): FlutterEngine { | ||
| FlutterEngineCache.getInstance().get(ENGINE_ID)?.let { return it } | ||
| val app = context.applicationContext | ||
| // Constructor auto-registers plugins (GeneratedPluginRegistrant) → | ||
| // flutter_blue_plus, notification_listener_service, shared_preferences, | ||
| // etc. are all available headless. | ||
| val engine = FlutterEngine(app) | ||
| // Register platform channels on the engine BEFORE Dart starts, so they | ||
| // exist even when no Activity is attached (headless calls like | ||
| // EdgeTracking.start must work). | ||
| NativeChannels.register(engine, app) | ||
| engine.dartExecutor.executeDartEntrypoint( | ||
| DartExecutor.DartEntrypoint.createDefault() | ||
| ) | ||
| FlutterEngineCache.getInstance().put(ENGINE_ID, engine) | ||
| return engine | ||
| } | ||
| } | ||
|
|
||
| override fun onCreate() { | ||
| super.onCreate() | ||
| // Constructor auto-registers plugins (GeneratedPluginRegistrant) → flutter_blue_plus, | ||
| // notification_listener_service, shared_preferences, etc. are all available headless. | ||
| val engine = FlutterEngine(this) | ||
| // Register platform channels on the engine BEFORE Dart starts, so they exist even | ||
| // when no Activity is attached (headless calls like EdgeTracking.start must work). | ||
| NativeChannels.register(engine, applicationContext) | ||
| engine.dartExecutor.executeDartEntrypoint( | ||
| DartExecutor.DartEntrypoint.createDefault() | ||
| ) | ||
| FlutterEngineCache.getInstance().put(ENGINE_ID, engine) | ||
| // Periodic watchdog: restart the tracking foreground service if the OS killed | ||
| // it while a band is paired (START_STICKY backup). Idempotent (KEEP policy); | ||
| // the worker itself no-ops when unpaired or already running. | ||
| KeepAliveWorker.schedule(applicationContext) | ||
| // Periodic watchdog: restart the tracking foreground service if the OS | ||
| // killed it while a band is paired (START_STICKY backup). Idempotent (KEEP | ||
| // policy). Paired-gated: unconditional scheduling gave even a never-paired | ||
| // install a persisted 15-min periodic wake forever. Pairing (re-)arms it | ||
| // via EdgeTrackingService.onCreate, and the worker cancels its own chain | ||
| // if it ever runs unpaired. | ||
| if (KeepAliveWorker.hasPairedDevice(this)) { | ||
| KeepAliveWorker.schedule(this) | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.