BatteryExemption.kt
1package com.peachpasswords.sync23import android.content.Context4import android.content.Intent5import android.net.Uri6import android.os.PowerManager7import android.provider.Settings89/**10 * Asks once to be left out of Doze / App Standby.11 *12 * Without it, a phone that has been idle for a while stops answering, and the13 * failure is invisible: a browser dialing a cached address just times out, which14 * is indistinguishable from the phone being off the network. The foreground15 * service keeps the process alive but does not by itself stop Doze from16 * deferring network work.17 *18 * Deliberately only offered once and never nagged — the system dialog is19 * alarming ("let this app run in the background all the time?"), and a user who20 * says no should get a phone that syncs when they open the app rather than one21 * that keeps asking. [isExempt] lets settings show the real state instead of22 * assuming the request worked.23 *24 * ## Distribution decision (L-05)25 *26 * `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` is a Play-policy-restricted permission:27 * Google requires the app's core function to genuinely break under Doze, and28 * rejects apps that prompt for it unprompted or treat it as a prerequisite. The29 * position taken here, deliberately:30 *31 * - Peach is distributed outside Play (Kepr-published APK + in-app updater), so32 * the permission is not currently a listing risk. If Peach is ever submitted33 * to Play, this is the declaration form to fill in, not a permission to strip.34 * - The request is only ever reachable from a user tap in the pairing flow's35 * background-sync dialog — [requestIntent] refuses to build an intent without36 * that explicit confirmation, so no code path can prompt on its own.37 * - The exemption is an optimisation, never a requirement: [syncOnce] is the38 * supported degraded mode for a user who declines, and everything keeps39 * working with it off.40 */41object BatteryExemption {4243 private const val PREFS = "peach_sync_battery"44 private const val ASKED_KEY = "asked_exemption"4546 fun isExempt(context: Context): Boolean {47 val pm = context.getSystemService(Context.POWER_SERVICE) as? PowerManager ?: return false48 return pm.isIgnoringBatteryOptimizations(context.packageName)49 }5051 fun shouldAsk(context: Context): Boolean =52 !isExempt(context) && !context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)53 .getBoolean(ASKED_KEY, false)5455 /**56 * Returns the intent to launch, or null when there is nothing to ask for.57 * The caller launches it so the result lands on a real Activity.58 *59 * [userConfirmed] is not a formality: it is the code-level guarantee behind60 * the distribution decision above. Pass true only from a handler for an61 * affirmative tap the user just made — never from a lifecycle callback, a62 * coordinator, or "we noticed sync is unreliable" logic.63 */64 fun requestIntent(context: Context, userConfirmed: Boolean): Intent? {65 if (!userConfirmed) return null66 if (!shouldAsk(context)) return null67 context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)68 .edit().putBoolean(ASKED_KEY, true).apply()69 return Intent(70 Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,71 Uri.parse("package:${context.packageName}"),72 )73 }7475 /**76 * Degraded sync for a phone without the exemption: bring the sync service up77 * now, while the app is in the foreground and Doze is not deferring anything.78 * The user gets a vault that syncs whenever they open Peach — later than an79 * exempt device, but never stale by the time they look at it.80 *81 * Same entry point the pairing and settings screens already use; the only82 * thing the exemption buys is that path firing while the screen is off.83 */84 fun syncOnce(context: Context) = PeachSyncService.start(context)85}