package com.peachpasswords.sync import android.content.Context import android.content.Intent import android.net.Uri import android.os.PowerManager import android.provider.Settings /** * Asks once to be left out of Doze / App Standby. * * Without it, a phone that has been idle for a while stops answering, and the * failure is invisible: a browser dialing a cached address just times out, which * is indistinguishable from the phone being off the network. The foreground * service keeps the process alive but does not by itself stop Doze from * deferring network work. * * Deliberately only offered once and never nagged — the system dialog is * alarming ("let this app run in the background all the time?"), and a user who * says no should get a phone that syncs when they open the app rather than one * that keeps asking. [isExempt] lets settings show the real state instead of * assuming the request worked. * * ## Distribution decision (L-05) * * `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` is a Play-policy-restricted permission: * Google requires the app's core function to genuinely break under Doze, and * rejects apps that prompt for it unprompted or treat it as a prerequisite. The * position taken here, deliberately: * * - Peach is distributed outside Play (Kepr-published APK + in-app updater), so * the permission is not currently a listing risk. If Peach is ever submitted * to Play, this is the declaration form to fill in, not a permission to strip. * - The request is only ever reachable from a user tap in the pairing flow's * background-sync dialog — [requestIntent] refuses to build an intent without * that explicit confirmation, so no code path can prompt on its own. * - The exemption is an optimisation, never a requirement: [syncOnce] is the * supported degraded mode for a user who declines, and everything keeps * working with it off. */ object BatteryExemption { private const val PREFS = "peach_sync_battery" private const val ASKED_KEY = "asked_exemption" fun isExempt(context: Context): Boolean { val pm = context.getSystemService(Context.POWER_SERVICE) as? PowerManager ?: return false return pm.isIgnoringBatteryOptimizations(context.packageName) } fun shouldAsk(context: Context): Boolean = !isExempt(context) && !context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) .getBoolean(ASKED_KEY, false) /** * Returns the intent to launch, or null when there is nothing to ask for. * The caller launches it so the result lands on a real Activity. * * [userConfirmed] is not a formality: it is the code-level guarantee behind * the distribution decision above. Pass true only from a handler for an * affirmative tap the user just made — never from a lifecycle callback, a * coordinator, or "we noticed sync is unreliable" logic. */ fun requestIntent(context: Context, userConfirmed: Boolean): Intent? { if (!userConfirmed) return null if (!shouldAsk(context)) return null context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) .edit().putBoolean(ASKED_KEY, true).apply() return Intent( Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, Uri.parse("package:${context.packageName}"), ) } /** * Degraded sync for a phone without the exemption: bring the sync service up * now, while the app is in the foreground and Doze is not deferring anything. * The user gets a vault that syncs whenever they open Peach — later than an * exempt device, but never stale by the time they look at it. * * Same entry point the pairing and settings screens already use; the only * thing the exemption buys is that path firing while the screen is off. */ fun syncOnce(context: Context) = PeachSyncService.start(context) }