BatteryExemption.kt

3.8 KB · raw

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}