MainActivity.kt

7.8 KB · raw

1package com.peachpasswords23import android.content.Intent4import android.graphics.Color as AndroidColor5import android.graphics.drawable.ColorDrawable6import android.os.Bundle7import androidx.activity.compose.setContent8import androidx.activity.enableEdgeToEdge9import androidx.compose.runtime.SideEffect10import androidx.compose.ui.unit.dp11import androidx.compose.ui.unit.Density12import androidx.compose.ui.platform.LocalDensity13import androidx.compose.ui.Modifier14import androidx.compose.runtime.CompositionLocalProvider15import androidx.compose.runtime.Composable16import androidx.compose.foundation.layout.fillMaxSize17import androidx.compose.foundation.layout.BoxWithConstraints18import androidx.compose.runtime.collectAsState19import androidx.compose.runtime.getValue20import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen21import androidx.core.view.WindowCompat22import androidx.fragment.app.FragmentActivity23import dagger.hilt.android.AndroidEntryPoint24import javax.inject.Inject25import com.peachpasswords.autofill.AutofillIntents26import com.peachpasswords.autofill.AutofillNavRepository27import com.peachpasswords.ui.PeachAppNavHost28import com.peachpasswords.ui.components.QaFlags29import com.peachpasswords.ui.components.SecureWindowPolicy30import com.peachpasswords.ui.onboarding.ONBOARD_KEY31import com.peachpasswords.ui.onboarding.ONBOARD_PREFS32import com.peachpasswords.ui.onboarding.onboardingStartDestination33import com.peachpasswords.ui.theme.ACCENT_COLORS34import com.peachpasswords.ui.theme.DEFAULT_ACCENT35import com.peachpasswords.ui.theme.PeachTheme36import com.peachpasswords.updates.EXTRA_OPEN_UPDATE37import com.peachpasswords.updates.UpdateRepository38import com.peachpasswords.vault.VaultLockTimer39import com.peachpasswords.vault.VaultRepository4041// FragmentActivity (not ComponentActivity) so androidx BiometricPrompt can attach.42@AndroidEntryPoint43class MainActivity : FragmentActivity() {44    @Inject lateinit var vault: VaultRepository45    @Inject lateinit var lockTimer: VaultLockTimer46    @Inject lateinit var updates: UpdateRepository47    @Inject lateinit var autofillNav: AutofillNavRepository4849    // Every touch/key event resets the idle auto-lock countdown (CF-1).50    override fun onUserInteraction() {51        super.onUserInteraction()52        lockTimer.poke()53    }5455    // launchMode="singleTask" reuses this instance across `adb shell am start`56    // relaunches, so the QA capture-bypass extra (see onCreate) needs to be57    // re-read here too, not just once at process start.58    override fun onNewIntent(intent: Intent) {59        super.onNewIntent(intent)60        setIntent(intent)61        routeIntent(intent)62        if (BuildConfig.DEBUG) {63            QaFlags.disableSecureCaptureForQa = intent.getBooleanExtra(64                QaFlags.EXTRA_DISABLE_SECURE_CAPTURE,65                false,66            )67        }68    }6970    override fun onCreate(savedInstanceState: Bundle?) {71        installSplashScreen()72        super.onCreate(savedInstanceState)7374        // Cold launch from the update notification (the warm case is onNewIntent).75        routeIntent(intent)7677        // Match the window fill to --bg so launch never flashes a lighter colour78        // before Compose draws, whatever the device's own day/night mode is.79        window.setBackgroundDrawable(ColorDrawable(AndroidColor.parseColor("#0C0A06")))8081        enableEdgeToEdge()8283        // Block screenshots / recents thumbnails of the vault in release builds.84        if (!BuildConfig.DEBUG) {85            SecureWindowPolicy.installRelease(window)86        } else {87            // QA-only, debug-build-only: `adb shell am start -n <pkg>/.MainActivity88            // --ez qa_disable_secure_flag true` lets automated visual QA screenshot89            // otherwise-FLAG_SECURE screens (SecureFlagEffect). Off unless asked.90            QaFlags.disableSecureCaptureForQa = intent.getBooleanExtra(91                QaFlags.EXTRA_DISABLE_SECURE_CAPTURE,92                false,93            )94        }9596        // Onboarding runs once, only on a fresh install with no vault yet.97        val onboarded = getSharedPreferences(ONBOARD_PREFS, MODE_PRIVATE)98            .getBoolean(ONBOARD_KEY, false)99        val start = onboardingStartDestination(hasVault = vault.hasVault(), onboarded = onboarded)100101        setContent {102            val settings by vault.settings.collectAsState()103            // enableEdgeToEdge() picks bar icon appearance from the device's own104            // day/night mode. Peach is always dark, so on a device in light mode105            // the icons would render dark-on-dark and vanish against our106            // background — reading as if the app ran under the status bar.107            SideEffect {108                WindowCompat.getInsetsController(window, window.decorView).apply {109                    isAppearanceLightStatusBars = false110                    isAppearanceLightNavigationBars = false111                }112            }113            val accent = ACCENT_COLORS[settings.accentColor] ?: DEFAULT_ACCENT114            PeachTheme(115                accent = accent,116                reduceMotion = settings.reduceMotion,117            ) {118                WideScreenScale {119                    PeachAppNavHost(startDestination = start, isUnlocked = vault.isUnlocked, uiGateLocked = vault.uiGateLocked)120                }121            }122        }123    }124}125126/** Layout width a phone gives us; anything past this is "extra" to spend. */127private val BASE_WIDTH = 411.dp128129/**130 * The single place a launch intent is interpreted, shared by cold launch131 * (`onCreate`) and warm relaunch (`onNewIntent`) — launchMode="singleTask" means132 * either can be the one that carries a shortcut tap, and the two paths silently133 * drifting apart is how the shortcut actions went unhandled in the first place.134 *135 * Everything here only records a request; nothing navigates. The nav host acts on136 * it once the vault is unlocked.137 */138private fun MainActivity.routeIntent(intent: Intent) {139    if (intent.getBooleanExtra(EXTRA_OPEN_UPDATE, false)) updates.requestOpen()140    forwardNewEntryRequest(intent)141    routeAction(intent.action)?.let(PendingActionQueue::request)142}143144/**145 * Pick up the "+ Create new entry" launcher from the autofill picker and stash146 * the prefill payload in [autofillNav]. The NavHost observes that flow and147 * navigates to the new-entry editor, mirroring the UpdateRepository pattern.148 */149private fun MainActivity.forwardNewEntryRequest(intent: Intent) {150    if (!intent.getBooleanExtra(AutofillIntents.EXTRA_OPEN_NEW_ENTRY, false)) return151    autofillNav.requestNewEntry(152        url = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_URL),153        username = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_USERNAME),154        email = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_EMAIL),155    )156}157158/** Cap on the up-scale — past this, big text stops reading as deliberate. */159private const val MAX_SCALE = 1.35f160161/**162 * On a foldable's inner screen the app was a stretched phone: phone-sized type163 * and rows with the surplus width absorbed as empty space. Scaling the density164 * grows every dp and sp together, so the same single-column layout simply reads165 * as a bigger screen — no per-screen breakpoints to keep in sync.166 *167 * Ceiling: a uniform scale, not real adaptive layout. If a screen ever wants a168 * genuinely different wide-screen arrangement it needs its own breakpoint.169 * No-op at or below [BASE_WIDTH], so phones are untouched.170 */171@Composable172private fun WideScreenScale(content: @Composable () -> Unit) {173    BoxWithConstraints(Modifier.fillMaxSize()) {174        val base = LocalDensity.current175        val scale = (maxWidth / BASE_WIDTH).coerceIn(1f, MAX_SCALE)176        if (scale == 1f) {177            content()178        } else {179            CompositionLocalProvider(180                LocalDensity provides Density(base.density * scale, base.fontScale),181                content = content,182            )183        }184    }185}