package com.peachpasswords import android.app.Application import androidx.hilt.work.HiltWorkerFactory import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner import androidx.work.Configuration import com.peachpasswords.sync.BackgroundSyncPreference import com.peachpasswords.sync.ForegroundSyncCoordinator import com.peachpasswords.sync.LockedSyncSpoolHooks import com.peachpasswords.sync.LockedSyncSpoolStore import com.peachpasswords.sync.PeachSyncService import com.peachpasswords.shared.PeachClipboard import com.peachpasswords.ui.lock.BiometricKeyStore import com.peachpasswords.updates.UpdateCheckWorker import com.peachpasswords.vault.PinUnlockStore import com.peachpasswords.vault.VaultKeyRotationHooks import com.peachpasswords.vault.VaultLockTimer import com.peachpasswords.vault.VaultMutationHooks import com.peachpasswords.vault.VaultRepository import dagger.hilt.android.HiltAndroidApp import javax.inject.Inject @HiltAndroidApp class PeachApplication : Application(), Configuration.Provider { @Inject lateinit var vault: VaultRepository @Inject lateinit var lockTimer: VaultLockTimer @Inject lateinit var foregroundSync: ForegroundSyncCoordinator @Inject lateinit var backgroundSync: BackgroundSyncPreference @Inject lateinit var lockedSyncSpool: LockedSyncSpoolStore @Inject lateinit var pinUnlock: PinUnlockStore @Inject lateinit var workerFactory: HiltWorkerFactory /** * Set by MainActivity when its stop is a configuration change — a fold, an * unfold, or a rotation — rather than a real backgrounding. ProcessLifecycleOwner * is debounced, but switching a foldable between its displays can outlast that * debounce, so without this the process-level stop locks a vault the user is * still looking at and they meet a second lock screen drawn with the new * metrics. */ var stopWasConfigurationChange = false override val workManagerConfiguration: Configuration get() = Configuration.Builder().setWorkerFactory(workerFactory).build() override fun onCreate() { super.onCreate() LockedSyncSpoolHooks.refresh = lockedSyncSpool::refresh // Both convenience escrows wrap a copy of the slot-A key, so every // rotation site has to invalidate them together — restoring a Codex or // resetting through recovery words rotated the key while leaving a // biometric wrapper the hardware would still accept (PB-26-126). VaultKeyRotationHooks.onRotated = { pinUnlock.clear() BiometricKeyStore.clear(this) } VaultMutationHooks.onCommitted = { if (backgroundSync.enabled.value && vault.syncVaultId() != null) { PeachSyncService.start(this) } } // Update checks: once a day in the background, plus one now — process // start is the honest "app open". Neither ever downloads or installs; // the most they do is post a notification. UpdateCheckWorker.schedulePeriodic(this) UpdateCheckWorker.checkNow(this) // Lock the vault the moment the whole app goes to background (CF-1). // ProcessLifecycleOwner is process-wide and debounced, so a rotation or a // brief Activity restart does not trip it — only a genuine backgrounding. ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { foregroundSync.triggerForegroundSync() } override fun onStop(owner: LifecycleOwner) { // A SAF file picker/save dialog or share sheet also stops the // process's activities while it's in the foreground — that's // an expected, momentary handoff as part of an in-app export/ // import flow, not a real backgrounding, so it's exempted via // VaultRepository.expectTransientBackground(). if (vault.consumeTransientBackgroundSuppression()) return if (stopWasConfigurationChange) { stopWasConfigurationChange = false return } if (backgroundSync.enabled.value && vault.isUnlocked.value) { // Cache only opaque encrypted sync material, then perform a // real lock. The service keeps transport authority, not the // vault root or plaintext entries. runCatching { lockedSyncSpool.refresh(vault) } vault.lock() } else { vault.lock() lockTimer.cancel() } // The 30s timer can outlive the app being visible, and a process // without window focus cannot read the clipboard to confirm its // own value — so lock is the last point at which the wipe can be // guaranteed (PB-26-119). PeachClipboard.clearNow(this@PeachApplication) } }) } }