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.ACTION_OPEN_UPDATE42import com.peachpasswords.updates.EXTRA_OPEN_UPDATE43import com.peachpasswords.updates.EXTRA_OPEN_UPDATE_TOKEN44import com.peachpasswords.updates.UpdateRepository45import com.peachpasswords.updates.consumeUpdateOpenToken46import com.peachpasswords.vault.VaultLockTimer47import com.peachpasswords.vault.VaultRepository48import com.peachpasswords.vault.faviconsAllowed495051@AndroidEntryPoint52class MainActivity : FragmentActivity() {53 @Inject lateinit var vault: VaultRepository54 @Inject lateinit var favicons: Favicons55 @Inject lateinit var lockTimer: VaultLockTimer56 @Inject lateinit var updates: UpdateRepository57 @Inject lateinit var autofillNav: AutofillNavRepository5859 60 override fun onUserInteraction() {61 super.onUserInteraction()62 lockTimer.poke()63 }6465 66 67 68 override fun onStop() {69 (application as PeachApplication).stopWasConfigurationChange = isChangingConfigurations()70 super.onStop()71 }7273 74 75 76 override fun onNewIntent(intent: Intent) {77 super.onNewIntent(intent)78 setIntent(intent)79 routeIntent(intent)80 if (BuildConfig.DEBUG) {81 QaFlags.disableSecureCaptureForQa = intent.getBooleanExtra(82 QaFlags.EXTRA_DISABLE_SECURE_CAPTURE,83 false,84 )85 }86 }8788 override fun onCreate(savedInstanceState: Bundle?) {89 installSplashScreen()90 super.onCreate(savedInstanceState)9192 93 routeIntent(intent)9495 96 97 window.setBackgroundDrawable(ColorDrawable(AndroidColor.parseColor("#0C0A06")))9899 enableEdgeToEdge()100101 102 103 104 if (!BuildConfig.DEBUG) {105 SecureWindowPolicy.setUserSecure(window, true)106 } else {107 QaFlags.disableSecureCaptureForQa = intent.getBooleanExtra(108 QaFlags.EXTRA_DISABLE_SECURE_CAPTURE,109 false,110 )111 }112113 114 val onboarded = getSharedPreferences(ONBOARD_PREFS, MODE_PRIVATE)115 .getBoolean(ONBOARD_KEY, false)116 val start = onboardingStartDestination(hasVault = vault.hasVault(), onboarded = onboarded)117118 setContent {119 val settings by vault.settings.collectAsState()120 121 122 123 124 SideEffect {125 WindowCompat.getInsetsController(window, window.decorView).apply {126 isAppearanceLightStatusBars = false127 isAppearanceLightNavigationBars = false128 }129 }130 val accent = ACCENT_COLORS[settings.accentColor] ?: DEFAULT_ACCENT131 SideEffect {132 if (!(BuildConfig.DEBUG && QaFlags.disableSecureCaptureForQa)) {133 SecureWindowPolicy.setUserSecure(window, !settings.allowScreenshots)134 }135 }136 CompositionLocalProvider(137 LocalFavicons provides favicons,138 LocalFaviconsEnabled provides faviconsAllowed(settings),139 ) {140 PeachTheme(141 accent = accent,142 reduceMotion = settings.reduceMotion,143 textSize = settings.textSize,144 ) {145 WideScreenScale(scalePreference = settings.uiScale) {146 PeachAppNavHost(startDestination = start, isUnlocked = vault.isUnlocked, uiGateLocked = vault.uiGateLocked)147 }148 }149 }150 }151 }152}153154155private val BASE_WIDTH = 411.dp156157158159160161162163164165166private fun MainActivity.routeIntent(intent: Intent) {167 if (intent.getBooleanExtra(EXTRA_OPEN_UPDATE, false) &&168 intent.action == ACTION_OPEN_UPDATE &&169 consumeUpdateOpenToken(this, intent.getStringExtra(EXTRA_OPEN_UPDATE_TOKEN))) {170 updates.requestOpen()171 }172 forwardNewEntryRequest(intent)173 routeAction(intent.action)?.let(PendingActionQueue::request)174}175176177178179180181private fun MainActivity.forwardNewEntryRequest(intent: Intent) {182 if (!intent.getBooleanExtra(AutofillIntents.EXTRA_OPEN_NEW_ENTRY, false)) return183 184 185 if (!AutofillIntents.consumeNewEntryToken(intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_TOKEN))) return186 autofillNav.requestNewEntry(187 url = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_URL),188 username = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_USERNAME),189 email = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_EMAIL),190 )191}192193194private const val MAX_SCALE = 1.35f195196197198199200201202203204205206@Composable207private fun WideScreenScale(208 scalePreference: String = "auto",209 content: @Composable () -> Unit,210) {211 BoxWithConstraints(Modifier.fillMaxSize()) {212 val base = LocalDensity.current213 val scale = wideScreenScaleFor(scalePreference, maxWidth, maxHeight)214 215 216 217 218 219 220 221 222 val density = remember(base, scale) { Density(base.density * scale, base.fontScale) }223 CompositionLocalProvider(LocalDensity provides density, content = content)224 }225}226227internal fun wideScreenScaleFor(preference: String, width: Dp, height: Dp): Float {228 val phoneScale = (width / BASE_WIDTH).coerceIn(1f, MAX_SCALE)229 return when (preference) {230 "compact" -> 1f231 "large" -> phoneScale232 else -> if (isTabletLike(width, height)) 1f else phoneScale233 }234}235236private fun isTabletLike(width: Dp, height: Dp): Boolean {237 val shortest = minOf(width.value, height.value)238 val longest = maxOf(width.value, height.value)239 return shortest >= 720f || (shortest >= 600f && longest / shortest >= 1.45f)240}