This project only ships to Android. Every battery decision below is enforced both in the JS facades (libs/services/*) and in the Kotlin Nitro implementations under android/src/main/java/.../mibandactive/ (to be authored).
- No always-on foreground service. A foreground service is started only during an active sync window (≤ 60s typical) and stopped immediately when the sync resolves. Connecting → syncing → disconnect is one atomic job.
- No
WAKE_LOCK. WorkManager wakes the device for us. We never pin the CPU on ourselves. - No background location permission. Android's BLE-scan needs
ACCESS_FINE_LOCATIONonly while in foreground. We do not requestACCESS_BACKGROUND_LOCATION. - Periodic sync interval ≥ 30 min (
PeriodicWorkRequest, minimum 15 min by Android rules, we use 30). User can extend this but cannot shorten below 30 min. - Realtime HR streaming OFF by default. It is the single biggest power sink on the band and on the phone radio. Users must explicitly opt in per-session.
| Phase | Duration | Duty cycle | Window |
|---|---|---|---|
| Onboarding scan (foreground) | 12 s max | continuous | one-shot |
| Auto-reconnect (background, screen off) | 1.2 s active / 12 s idle | ≤ 10% | only during scheduled work window |
| Manual re-pair | 12 s max | continuous | user-triggered |
We never start a background BluetoothLeScanner that runs > 15 s. The Kotlin implementation must call stopScan() on a Handler-posted timeout, not on the assumption the OS will preempt.
| Job | Cadence | Constraints |
|---|---|---|
DailySummarySyncWorker |
every 30 min (periodic) | requiresBatteryNotLow=true, no network constraint |
NotificationFlushWorker |
one-time, expedited, on NLS event | setExpedited() only when band is already connected; otherwise enqueue normal |
WeatherPushWorker |
every 6h | requiresBatteryNotLow=true, network connected |
CalendarPushWorker |
every 6h | requiresBatteryNotLow=true |
requiresBatteryNotLow=true means: when the phone is under 15% battery, we skip the run. The user can still pull-to-refresh manually on the dashboard.
We assume Doze. We do not ask for "Ignore battery optimization" in the onboarding flow (it's a soft prompt only, skip is OK). The work pattern is designed to fire during maintenance windows that Doze itself grants.
If the OEM is aggressive (MIUI, ColorOS, EMUI), we let the user know via an in-app banner: "你的廠商可能會在背景殺掉同步,設定 → 電池 → 不限制本 App / Your OEM may kill background sync — exempt this app in system settings."
NotificationListenerService callbacks are short. We:
- Drop notifications that are not in the user's allow-list before waking the BLE stack.
- Coalesce a 250ms window of incoming notifications into a single BLE write.
- Downscale notification icons to 24×24 PNG before send (Gadgetbridge sends 40×40+; we save flash + RF time).
- ❌ Persistent foreground service "for reliability".
- ❌
JobIntentService,AlarmManager.setExactAndAllowWhileIdle()polling loops. - ❌ Background BLE scan with
SCAN_MODE_LOW_LATENCY. - ❌ Continuous heart-rate streaming as the default.
- ❌ Asking for
ACCESS_BACKGROUND_LOCATION.
There's a Logcat tag prefix MB9A_POWER on every wake-up. Run adb shell dumpsys batterystats after a 24h soak to verify the per-app wake count stays under 50.