Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
42 changes: 42 additions & 0 deletions .claude/skills/ponytail/SKILL.md
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` (direct dep — `DeepCollectionEquality` etc.), `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`.
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,20 @@ import android.os.Build
* prefixed "flutter.". Our PairedDevice uses the key "paired_remote_id", so the XML
* key is "flutter.paired_remote_id".
*
* Starting the service also triggers EdgeApplication.onCreate, which pre-warms the
* cached FlutterEngine. The Dart main() runs in that engine — it sees no Activity
* (isHeadlessBoot path) and calls headlessBoot() which starts EdgeTracking + connects
* to the paired band.
* Starting the service warms the cached FlutterEngine (EdgeTrackingService.onCreate
* → EdgeApplication.ensureEngine). The Dart main() runs in that engine — it sees no
* Activity (isHeadlessBoot path) and calls headlessBoot() which starts EdgeTracking +
* connects to the paired band.
*/
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
if (action != Intent.ACTION_BOOT_COMPLETED &&
action != "android.intent.action.QUICKBOOT_POWERON") return

if (!hasPairedDevice(context)) return
if (!KeepAliveWorker.hasPairedDevice(context)) return
markPendingHeadlessBoot(context)

val svcIntent = Intent(context, EdgeTrackingService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(svcIntent)
} else {
context.startService(svcIntent)
}
}

private fun hasPairedDevice(context: Context): Boolean {
// Flutter SharedPreferences file name + key prefix.
val prefs: SharedPreferences = context.getSharedPreferences(
"FlutterSharedPreferences",
Context.MODE_PRIVATE
)
val id = prefs.getString("flutter.paired_remote_id", null)
return !id.isNullOrEmpty()
EdgeTrackingService.start(context)
}

private fun markPendingHeadlessBoot(context: Context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ class EdgeCompanionService : CompanionDeviceService() {
}

private fun onBandAppeared() {
// Routine re-appearances (arm-swing / body-block dropouts, many per hour
// mid-workout) must not churn an already-running service — a restart just
// rebuilds and re-posts the notification. `running` is exact in-process,
// and a dead process initializes it false, so the cold path still starts.
if (EdgeTrackingService.running) return
try {
EdgeTrackingService.start(this)
} catch (e: Exception) {
Expand Down
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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat

/**
* Foreground service that keeps the app process alive while backgrounded so the live
Expand Down Expand Up @@ -77,15 +78,19 @@ class EdgeTrackingService : Service() {
var running: Boolean = false
private set

/** Start the foreground service (idempotent). */
/**
* Start the foreground service (idempotent). The ONE entry point —
* BootReceiver / TaskerReceiver / NativeChannels route through here
* instead of hand-rolling the SDK_INT >= O branch (ContextCompat owns
* that check). [location] non-null sets EXTRA_LOCATION (authoritative,
* see its tri-state doc); null omits the extra so a live session's
* mode is inherited.
*/
@JvmStatic
fun start(context: Context) {
fun start(context: Context, location: Boolean? = null) {
val intent = Intent(context, EdgeTrackingService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
if (location != null) intent.putExtra(EXTRA_LOCATION, location)
ContextCompat.startForegroundService(context, intent)
}
}

Expand Down Expand Up @@ -148,6 +153,23 @@ class EdgeTrackingService : Service() {
// crash the process over a keep-alive notification.
Log.w(TAG, "startForeground failed: $e")
}
// AFTER startForeground, so the 5 s foreground-service deadline is met
// before any heavier work runs on the main thread:
//
// Engine warm-up — the tracking service is the one headless path that
// genuinely needs Dart (the BLE session lives there); moved here from
// EdgeApplication.onCreate so widget alarms / worker runs / CDM binds in
// a dead process stay lightweight broadcasts instead of each cold-booting
// a full FlutterEngine + Dart main(). Idempotent.
EdgeApplication.ensureEngine(this)
// Watchdog (re-)schedule. In onStartCommand, not onCreate, deliberately:
// it must also run when a start lands on an ALREADY-RUNNING service —
// e.g. re-pairing after an unpair whose EdgeTracking.stop() silently
// failed, where the worker has already cancelled its own chain and a
// fresh onCreate never fires. KEEP policy makes the repeat free.
// (EdgeApplication gates its own schedule on the paired flag; the worker
// cancels itself when unpaired.)
KeepAliveWorker.schedule(applicationContext)
// STICKY: recreate after an OS kill so the headless engine reconnects the
// band without waiting for the user to reopen the app.
return START_STICKY
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,13 @@ class KeepAliveWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, para
}
}

private fun hasPairedDevice(context: Context): Boolean {
// Flutter SharedPreferences file name + key prefix (see BootReceiver).
/**
* Whether Dart has a band paired. Flutter SharedPreferences writes to
* "FlutterSharedPreferences.xml" with keys prefixed "flutter.".
* Internal: EdgeApplication and BootReceiver gate on the same check.
*/
@JvmStatic
internal fun hasPairedDevice(context: Context): Boolean {
val prefs = context.getSharedPreferences(
"FlutterSharedPreferences",
Context.MODE_PRIVATE,
Expand All @@ -60,7 +65,17 @@ class KeepAliveWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, para

override fun doWork(): Result {
val ctx = applicationContext
if (!hasPairedDevice(ctx)) return Result.success() // nothing to keep alive
if (!hasPairedDevice(ctx)) {
// Unpaired: there is nothing to keep alive, ever — cancel the chain
// rather than waking a dead process every ~15 min for the life of the
// install. Pairing re-schedules via EdgeTrackingService.onCreate.
try {
WorkManager.getInstance(ctx).cancelUniqueWork(WORK_NAME)
} catch (e: Exception) {
Log.w(TAG, "cancel failed: $e")
}
return Result.success()
}
if (EdgeTrackingService.running) return Result.success() // healthy
return try {
Log.i(TAG, "paired but service not running — restarting EdgeTrackingService")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ class MainActivity : FlutterFragmentActivity() {
override fun shouldDestroyEngineWithHost(): Boolean = false

override fun onCreate(savedInstanceState: Bundle?) {
// The shared engine is created lazily now (EdgeApplication.ensureEngine).
// getCachedEngineId() is consulted during super.onCreate, so the cache
// entry must exist before it runs.
EdgeApplication.ensureEngine(applicationContext)
activityAttached = true
clearPendingHeadlessBoot()
super.onCreate(savedInstanceState)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,13 @@ object NativeChannels {
.setMethodCallHandler { call, result ->
when (call.method) {
"start" -> {
val intent = Intent(app, EdgeTrackingService::class.java)
// Route workout live → the FGS also claims the location type
// (see EdgeTrackingService.EXTRA_LOCATION).
val location = call.argument<Boolean>("location") == true
intent.putExtra(EdgeTrackingService.EXTRA_LOCATION, location)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
app.startForegroundService(intent)
} else {
app.startService(intent)
}
// (see EdgeTrackingService.EXTRA_LOCATION). Dart always sends
// the flag, so the extra is always set (authoritative).
EdgeTrackingService.start(
app,
call.argument<Boolean>("location") == true,
)
result.success(null)
}
"stop" -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import es.antonborri.home_widget.HomeWidgetProvider
* while the band was connected. It says so in words until we have ever seen the
* band, and the reading is muted once it's > 24 h old — we genuinely don't know the current
* level if we haven't talked to the band. updatePeriodMillis re-renders every
* ~30 min so the staleness flip happens without the app's help.
* ~6 h so the staleness flip happens without the app's help (it moves at most
* once a day; see widget_band_battery_info.xml).
*/
class OpenStrapBatteryWidgetProvider : HomeWidgetProvider() {

Expand Down
Loading