MainActivity.kt

7.2 KB · raw

1package com.peachpasswords23import android.content.Intent4import android.graphics.Color as AndroidColor5import android.graphics.drawable.ColorDrawable6import android.os.Bundle7import android.view.WindowManager8import androidx.activity.compose.setContent9import androidx.activity.enableEdgeToEdge10import androidx.compose.runtime.SideEffect11import androidx.compose.ui.unit.dp12import androidx.compose.ui.unit.Density13import androidx.compose.ui.platform.LocalDensity14import androidx.compose.ui.Modifier15import androidx.compose.runtime.CompositionLocalProvider16import androidx.compose.runtime.Composable17import androidx.compose.foundation.layout.fillMaxSize18import androidx.compose.foundation.layout.BoxWithConstraints19import androidx.compose.runtime.collectAsState20import androidx.compose.runtime.getValue21import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen22import androidx.core.view.WindowCompat23import androidx.fragment.app.FragmentActivity24import dagger.hilt.android.AndroidEntryPoint25import javax.inject.Inject26import com.peachpasswords.autofill.AutofillIntents27import com.peachpasswords.autofill.AutofillNavRepository28import com.peachpasswords.ui.PeachAppNavHost29import com.peachpasswords.ui.components.QaFlags30import 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        if (intent.getBooleanExtra(EXTRA_OPEN_UPDATE, false)) updates.requestOpen()62        forwardNewEntryRequest(intent)63        if (BuildConfig.DEBUG) {64            QaFlags.disableSecureCaptureForQa = intent.getBooleanExtra("qa_disable_secure_flag", false)65        }66    }6768    override fun onCreate(savedInstanceState: Bundle?) {69        installSplashScreen()70        super.onCreate(savedInstanceState)7172        // Cold launch from the update notification (the warm case is onNewIntent).73        if (intent.getBooleanExtra(EXTRA_OPEN_UPDATE, false)) updates.requestOpen()74        forwardNewEntryRequest(intent)7576        // Match the window fill to --bg so launch never flashes a lighter colour77        // before Compose draws, whatever the device's own day/night mode is.78        window.setBackgroundDrawable(ColorDrawable(AndroidColor.parseColor("#0C0A06")))7980        enableEdgeToEdge()8182        // Block screenshots / recents thumbnails of the vault in release builds.83        if (!BuildConfig.DEBUG) {84            window.setFlags(85                WindowManager.LayoutParams.FLAG_SECURE,86                WindowManager.LayoutParams.FLAG_SECURE,87            )88        } else {89            // QA-only, debug-build-only: `adb shell am start -n <pkg>/.MainActivity90            // --ez qa_disable_secure_flag true` lets automated visual QA screenshot91            // otherwise-FLAG_SECURE screens (SecureFlagEffect). Off unless asked.92            QaFlags.disableSecureCaptureForQa = intent.getBooleanExtra("qa_disable_secure_flag", false)93        }9495        // Onboarding runs once, only on a fresh install with no vault yet.96        val onboarded = getSharedPreferences(ONBOARD_PREFS, MODE_PRIVATE)97            .getBoolean(ONBOARD_KEY, false)98        val start = onboardingStartDestination(hasVault = vault.hasVault(), onboarded = onboarded)99100        setContent {101            val settings by vault.settings.collectAsState()102            // enableEdgeToEdge() picks bar icon appearance from the device's own103            // day/night mode. Peach is always dark, so on a device in light mode104            // the icons would render dark-on-dark and vanish against our105            // background — reading as if the app ran under the status bar.106            SideEffect {107                WindowCompat.getInsetsController(window, window.decorView).apply {108                    isAppearanceLightStatusBars = false109                    isAppearanceLightNavigationBars = false110                }111            }112            val accent = ACCENT_COLORS[settings.accentColor] ?: DEFAULT_ACCENT113            PeachTheme(114                accent = accent,115                reduceMotion = settings.reduceMotion,116            ) {117                WideScreenScale {118                    PeachAppNavHost(startDestination = start, isUnlocked = vault.isUnlocked)119                }120            }121        }122    }123}124125/** Layout width a phone gives us; anything past this is "extra" to spend. */126private val BASE_WIDTH = 411.dp127128/**129 * Pick up the "+ Create new entry" launcher from the autofill picker and stash130 * the prefill payload in [autofillNav]. The NavHost observes that flow and131 * navigates to the new-entry editor, mirroring the UpdateRepository pattern.132 */133private fun MainActivity.forwardNewEntryRequest(intent: Intent) {134    if (!intent.getBooleanExtra(AutofillIntents.EXTRA_OPEN_NEW_ENTRY, false)) return135    autofillNav.requestNewEntry(136        url = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_URL),137        username = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_USERNAME),138        email = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_EMAIL),139    )140}141142/** Cap on the up-scale — past this, big text stops reading as deliberate. */143private const val MAX_SCALE = 1.35f144145/**146 * On a foldable's inner screen the app was a stretched phone: phone-sized type147 * and rows with the surplus width absorbed as empty space. Scaling the density148 * grows every dp and sp together, so the same single-column layout simply reads149 * as a bigger screen — no per-screen breakpoints to keep in sync.150 *151 * Ceiling: a uniform scale, not real adaptive layout. If a screen ever wants a152 * genuinely different wide-screen arrangement it needs its own breakpoint.153 * No-op at or below [BASE_WIDTH], so phones are untouched.154 */155@Composable156private fun WideScreenScale(content: @Composable () -> Unit) {157    BoxWithConstraints(Modifier.fillMaxSize()) {158        val base = LocalDensity.current159        val scale = (maxWidth / BASE_WIDTH).coerceIn(1f, MAX_SCALE)160        if (scale == 1f) {161            content()162        } else {163            CompositionLocalProvider(164                LocalDensity provides Density(base.density * scale, base.fontScale),165                content = content,166            )167        }168    }169}