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.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.Favicons30import com.peachpasswords.ui.components.LocalFavicons31import com.peachpasswords.ui.components.LocalFaviconsEnabled32import com.peachpasswords.ui.components.QaFlags33import com.peachpasswords.ui.components.SecureWindowPolicy34import com.peachpasswords.ui.onboarding.ONBOARD_KEY35import com.peachpasswords.ui.onboarding.ONBOARD_PREFS36import com.peachpasswords.ui.onboarding.onboardingStartDestination37import com.peachpasswords.ui.theme.ACCENT_COLORS38import com.peachpasswords.ui.theme.DEFAULT_ACCENT39import com.peachpasswords.ui.theme.PeachTheme40import com.peachpasswords.updates.EXTRA_OPEN_UPDATE41import com.peachpasswords.updates.UpdateRepository42import com.peachpasswords.vault.VaultLockTimer43import com.peachpasswords.vault.VaultRepository444546@AndroidEntryPoint47class MainActivity : FragmentActivity() {48 @Inject lateinit var vault: VaultRepository49 @Inject lateinit var favicons: Favicons50 @Inject lateinit var lockTimer: VaultLockTimer51 @Inject lateinit var updates: UpdateRepository52 @Inject lateinit var autofillNav: AutofillNavRepository5354 55 override fun onUserInteraction() {56 super.onUserInteraction()57 lockTimer.poke()58 }5960 61 62 63 override fun onNewIntent(intent: Intent) {64 super.onNewIntent(intent)65 setIntent(intent)66 routeIntent(intent)67 if (BuildConfig.DEBUG) {68 QaFlags.disableSecureCaptureForQa = intent.getBooleanExtra(69 QaFlags.EXTRA_DISABLE_SECURE_CAPTURE,70 false,71 )72 }73 }7475 override fun onCreate(savedInstanceState: Bundle?) {76 installSplashScreen()77 super.onCreate(savedInstanceState)7879 80 routeIntent(intent)8182 83 84 window.setBackgroundDrawable(ColorDrawable(AndroidColor.parseColor("#0C0A06")))8586 enableEdgeToEdge()8788 89 90 91 if (!BuildConfig.DEBUG) {92 SecureWindowPolicy.setUserSecure(window, true)93 } else {94 QaFlags.disableSecureCaptureForQa = intent.getBooleanExtra(95 QaFlags.EXTRA_DISABLE_SECURE_CAPTURE,96 false,97 )98 }99100 101 val onboarded = getSharedPreferences(ONBOARD_PREFS, MODE_PRIVATE)102 .getBoolean(ONBOARD_KEY, false)103 val start = onboardingStartDestination(hasVault = vault.hasVault(), onboarded = onboarded)104105 setContent {106 val settings by vault.settings.collectAsState()107 108 109 110 111 SideEffect {112 WindowCompat.getInsetsController(window, window.decorView).apply {113 isAppearanceLightStatusBars = false114 isAppearanceLightNavigationBars = false115 }116 }117 val accent = ACCENT_COLORS[settings.accentColor] ?: DEFAULT_ACCENT118 SideEffect {119 if (!(BuildConfig.DEBUG && QaFlags.disableSecureCaptureForQa)) {120 SecureWindowPolicy.setUserSecure(window, !settings.allowScreenshots)121 }122 }123 CompositionLocalProvider(124 LocalFavicons provides favicons,125 LocalFaviconsEnabled provides settings.faviconsEnabled,126 ) {127 PeachTheme(128 accent = accent,129 reduceMotion = settings.reduceMotion,130 textSize = settings.textSize,131 ) {132 WideScreenScale(scalePreference = settings.uiScale) {133 PeachAppNavHost(startDestination = start, isUnlocked = vault.isUnlocked, uiGateLocked = vault.uiGateLocked)134 }135 }136 }137 }138 }139}140141142private val BASE_WIDTH = 411.dp143144145146147148149150151152153private fun MainActivity.routeIntent(intent: Intent) {154 if (intent.getBooleanExtra(EXTRA_OPEN_UPDATE, false)) updates.requestOpen()155 forwardNewEntryRequest(intent)156 routeAction(intent.action)?.let(PendingActionQueue::request)157}158159160161162163164private fun MainActivity.forwardNewEntryRequest(intent: Intent) {165 if (!intent.getBooleanExtra(AutofillIntents.EXTRA_OPEN_NEW_ENTRY, false)) return166 167 168 if (!AutofillIntents.consumeNewEntryToken(intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_TOKEN))) return169 autofillNav.requestNewEntry(170 url = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_URL),171 username = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_USERNAME),172 email = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_EMAIL),173 )174}175176177private const val MAX_SCALE = 1.35f178179180181182183184185186187188189@Composable190private fun WideScreenScale(191 scalePreference: String = "auto",192 content: @Composable () -> Unit,193) {194 BoxWithConstraints(Modifier.fillMaxSize()) {195 val base = LocalDensity.current196 val scale = wideScreenScaleFor(scalePreference, maxWidth, maxHeight)197 if (scale == 1f) {198 content()199 } else {200 CompositionLocalProvider(201 LocalDensity provides Density(base.density * scale, base.fontScale),202 content = content,203 )204 }205 }206}207208internal fun wideScreenScaleFor(preference: String, width: Dp, height: Dp): Float {209 val phoneScale = (width / BASE_WIDTH).coerceIn(1f, MAX_SCALE)210 return when (preference) {211 "compact" -> 1f212 "large" -> phoneScale213 else -> if (isTabletLike(width, height)) 1f else phoneScale214 }215}216217private fun isTabletLike(width: Dp, height: Dp): Boolean {218 val shortest = minOf(width.value, height.value)219 val longest = maxOf(width.value, height.value)220 return shortest >= 720f || (shortest >= 600f && longest / shortest >= 1.45f)221}