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.unit.Dp13import androidx.compose.ui.platform.LocalDensity14import androidx.compose.ui.Modifier15import androidx.compose.runtime.CompositionLocalProvider16import androidx.compose.runtime.Composable17import androidx.compose.runtime.remember18import androidx.compose.foundation.layout.fillMaxSize19import androidx.compose.foundation.layout.BoxWithConstraints20import androidx.compose.runtime.collectAsState21import androidx.compose.runtime.getValue22import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen23import androidx.core.view.WindowCompat24import androidx.fragment.app.FragmentActivity25import dagger.hilt.android.AndroidEntryPoint26import javax.inject.Inject27import com.peachpasswords.autofill.AutofillIntents28import com.peachpasswords.autofill.AutofillNavRepository29import com.peachpasswords.ui.PeachAppNavHost30import com.peachpasswords.ui.components.Favicons31import com.peachpasswords.ui.components.LocalFavicons32import com.peachpasswords.ui.components.LocalFaviconsEnabled33import com.peachpasswords.ui.components.QaFlags34import com.peachpasswords.ui.components.SecureWindowPolicy35import com.peachpasswords.ui.onboarding.ONBOARD_KEY36import com.peachpasswords.ui.onboarding.ONBOARD_PREFS37import com.peachpasswords.ui.onboarding.onboardingStartDestination38import com.peachpasswords.ui.theme.ACCENT_COLORS39import com.peachpasswords.ui.theme.DEFAULT_ACCENT40import com.peachpasswords.ui.theme.PeachTheme41import com.peachpasswords.updates.EXTRA_OPEN_UPDATE42import com.peachpasswords.updates.UpdateRepository43import com.peachpasswords.vault.VaultLockTimer44import com.peachpasswords.vault.VaultRepository45import com.peachpasswords.vault.faviconsAllowed464748@AndroidEntryPoint49class MainActivity : FragmentActivity() {50 @Inject lateinit var vault: VaultRepository51 @Inject lateinit var favicons: Favicons52 @Inject lateinit var lockTimer: VaultLockTimer53 @Inject lateinit var updates: UpdateRepository54 @Inject lateinit var autofillNav: AutofillNavRepository5556 57 override fun onUserInteraction() {58 super.onUserInteraction()59 lockTimer.poke()60 }6162 63 64 65 override fun onStop() {66 (application as PeachApplication).stopWasConfigurationChange = isChangingConfigurations()67 super.onStop()68 }6970 71 72 73 override fun onNewIntent(intent: Intent) {74 super.onNewIntent(intent)75 setIntent(intent)76 routeIntent(intent)77 if (BuildConfig.DEBUG) {78 QaFlags.disableSecureCaptureForQa = intent.getBooleanExtra(79 QaFlags.EXTRA_DISABLE_SECURE_CAPTURE,80 false,81 )82 }83 }8485 override fun onCreate(savedInstanceState: Bundle?) {86 installSplashScreen()87 super.onCreate(savedInstanceState)8889 90 routeIntent(intent)9192 93 94 window.setBackgroundDrawable(ColorDrawable(AndroidColor.parseColor("#0C0A06")))9596 enableEdgeToEdge()9798 99 100 101 if (!BuildConfig.DEBUG) {102 SecureWindowPolicy.setUserSecure(window, true)103 } else {104 QaFlags.disableSecureCaptureForQa = intent.getBooleanExtra(105 QaFlags.EXTRA_DISABLE_SECURE_CAPTURE,106 false,107 )108 }109110 111 val onboarded = getSharedPreferences(ONBOARD_PREFS, MODE_PRIVATE)112 .getBoolean(ONBOARD_KEY, false)113 val start = onboardingStartDestination(hasVault = vault.hasVault(), onboarded = onboarded)114115 setContent {116 val settings by vault.settings.collectAsState()117 118 119 120 121 SideEffect {122 WindowCompat.getInsetsController(window, window.decorView).apply {123 isAppearanceLightStatusBars = false124 isAppearanceLightNavigationBars = false125 }126 }127 val accent = ACCENT_COLORS[settings.accentColor] ?: DEFAULT_ACCENT128 SideEffect {129 if (!(BuildConfig.DEBUG && QaFlags.disableSecureCaptureForQa)) {130 SecureWindowPolicy.setUserSecure(window, !settings.allowScreenshots)131 }132 }133 CompositionLocalProvider(134 LocalFavicons provides favicons,135 LocalFaviconsEnabled provides faviconsAllowed(settings),136 ) {137 PeachTheme(138 accent = accent,139 reduceMotion = settings.reduceMotion,140 textSize = settings.textSize,141 ) {142 WideScreenScale(scalePreference = settings.uiScale) {143 PeachAppNavHost(startDestination = start, isUnlocked = vault.isUnlocked, uiGateLocked = vault.uiGateLocked)144 }145 }146 }147 }148 }149}150151152private val BASE_WIDTH = 411.dp153154155156157158159160161162163private fun MainActivity.routeIntent(intent: Intent) {164 if (intent.getBooleanExtra(EXTRA_OPEN_UPDATE, false)) updates.requestOpen()165 forwardNewEntryRequest(intent)166 routeAction(intent.action)?.let(PendingActionQueue::request)167}168169170171172173174private fun MainActivity.forwardNewEntryRequest(intent: Intent) {175 if (!intent.getBooleanExtra(AutofillIntents.EXTRA_OPEN_NEW_ENTRY, false)) return176 177 178 if (!AutofillIntents.consumeNewEntryToken(intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_TOKEN))) return179 autofillNav.requestNewEntry(180 url = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_URL),181 username = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_USERNAME),182 email = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_EMAIL),183 )184}185186187private const val MAX_SCALE = 1.35f188189190191192193194195196197198199@Composable200private fun WideScreenScale(201 scalePreference: String = "auto",202 content: @Composable () -> Unit,203) {204 BoxWithConstraints(Modifier.fillMaxSize()) {205 val base = LocalDensity.current206 val scale = wideScreenScaleFor(scalePreference, maxWidth, maxHeight)207 208 209 210 211 212 213 214 215 val density = remember(base, scale) { Density(base.density * scale, base.fontScale) }216 CompositionLocalProvider(LocalDensity provides density, content = content)217 }218}219220internal fun wideScreenScaleFor(preference: String, width: Dp, height: Dp): Float {221 val phoneScale = (width / BASE_WIDTH).coerceIn(1f, MAX_SCALE)222 return when (preference) {223 "compact" -> 1f224 "large" -> phoneScale225 else -> if (isTabletLike(width, height)) 1f else phoneScale226 }227}228229private fun isTabletLike(width: Dp, height: Dp): Boolean {230 val shortest = minOf(width.value, height.value)231 val longest = maxOf(width.value, height.value)232 return shortest >= 720f || (shortest >= 600f && longest / shortest >= 1.45f)233}