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.VaultRepository44import com.peachpasswords.vault.faviconsAllowed454647@AndroidEntryPoint48class MainActivity : FragmentActivity() {49 @Inject lateinit var vault: VaultRepository50 @Inject lateinit var favicons: Favicons51 @Inject lateinit var lockTimer: VaultLockTimer52 @Inject lateinit var updates: UpdateRepository53 @Inject lateinit var autofillNav: AutofillNavRepository5455 56 override fun onUserInteraction() {57 super.onUserInteraction()58 lockTimer.poke()59 }6061 62 63 64 override fun onStop() {65 (application as PeachApplication).stopWasConfigurationChange = isChangingConfigurations()66 super.onStop()67 }6869 70 71 72 override fun onNewIntent(intent: Intent) {73 super.onNewIntent(intent)74 setIntent(intent)75 routeIntent(intent)76 if (BuildConfig.DEBUG) {77 QaFlags.disableSecureCaptureForQa = intent.getBooleanExtra(78 QaFlags.EXTRA_DISABLE_SECURE_CAPTURE,79 false,80 )81 }82 }8384 override fun onCreate(savedInstanceState: Bundle?) {85 installSplashScreen()86 super.onCreate(savedInstanceState)8788 89 routeIntent(intent)9091 92 93 window.setBackgroundDrawable(ColorDrawable(AndroidColor.parseColor("#0C0A06")))9495 enableEdgeToEdge()9697 98 99 100 if (!BuildConfig.DEBUG) {101 SecureWindowPolicy.setUserSecure(window, true)102 } else {103 QaFlags.disableSecureCaptureForQa = intent.getBooleanExtra(104 QaFlags.EXTRA_DISABLE_SECURE_CAPTURE,105 false,106 )107 }108109 110 val onboarded = getSharedPreferences(ONBOARD_PREFS, MODE_PRIVATE)111 .getBoolean(ONBOARD_KEY, false)112 val start = onboardingStartDestination(hasVault = vault.hasVault(), onboarded = onboarded)113114 setContent {115 val settings by vault.settings.collectAsState()116 117 118 119 120 SideEffect {121 WindowCompat.getInsetsController(window, window.decorView).apply {122 isAppearanceLightStatusBars = false123 isAppearanceLightNavigationBars = false124 }125 }126 val accent = ACCENT_COLORS[settings.accentColor] ?: DEFAULT_ACCENT127 SideEffect {128 if (!(BuildConfig.DEBUG && QaFlags.disableSecureCaptureForQa)) {129 SecureWindowPolicy.setUserSecure(window, !settings.allowScreenshots)130 }131 }132 CompositionLocalProvider(133 LocalFavicons provides favicons,134 LocalFaviconsEnabled provides faviconsAllowed(settings),135 ) {136 PeachTheme(137 accent = accent,138 reduceMotion = settings.reduceMotion,139 textSize = settings.textSize,140 ) {141 WideScreenScale(scalePreference = settings.uiScale) {142 PeachAppNavHost(startDestination = start, isUnlocked = vault.isUnlocked, uiGateLocked = vault.uiGateLocked)143 }144 }145 }146 }147 }148}149150151private val BASE_WIDTH = 411.dp152153154155156157158159160161162private fun MainActivity.routeIntent(intent: Intent) {163 if (intent.getBooleanExtra(EXTRA_OPEN_UPDATE, false)) updates.requestOpen()164 forwardNewEntryRequest(intent)165 routeAction(intent.action)?.let(PendingActionQueue::request)166}167168169170171172173private fun MainActivity.forwardNewEntryRequest(intent: Intent) {174 if (!intent.getBooleanExtra(AutofillIntents.EXTRA_OPEN_NEW_ENTRY, false)) return175 176 177 if (!AutofillIntents.consumeNewEntryToken(intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_TOKEN))) return178 autofillNav.requestNewEntry(179 url = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_URL),180 username = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_USERNAME),181 email = intent.getStringExtra(AutofillIntents.EXTRA_NEW_ENTRY_EMAIL),182 )183}184185186private const val MAX_SCALE = 1.35f187188189190191192193194195196197198@Composable199private fun WideScreenScale(200 scalePreference: String = "auto",201 content: @Composable () -> Unit,202) {203 BoxWithConstraints(Modifier.fillMaxSize()) {204 val base = LocalDensity.current205 val scale = wideScreenScaleFor(scalePreference, maxWidth, maxHeight)206 if (scale == 1f) {207 content()208 } else {209 CompositionLocalProvider(210 LocalDensity provides Density(base.density * scale, base.fontScale),211 content = content,212 )213 }214 }215}216217internal fun wideScreenScaleFor(preference: String, width: Dp, height: Dp): Float {218 val phoneScale = (width / BASE_WIDTH).coerceIn(1f, MAX_SCALE)219 return when (preference) {220 "compact" -> 1f221 "large" -> phoneScale222 else -> if (isTabletLike(width, height)) 1f else phoneScale223 }224}225226private fun isTabletLike(width: Dp, height: Dp): Boolean {227 val shortest = minOf(width.value, height.value)228 val longest = maxOf(width.value, height.value)229 return shortest >= 720f || (shortest >= 600f && longest / shortest >= 1.45f)230}