package com.peachpasswords.autofill import android.content.Intent import android.os.Bundle import android.view.autofill.AutofillId import android.view.autofill.AutofillManager import androidx.activity.compose.setContent import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.fragment.app.FragmentActivity import com.peachpasswords.MainActivity import com.peachpasswords.shared.extractDomain import com.peachpasswords.ui.components.FaviconInitials import com.peachpasswords.ui.theme.ACCENT_COLORS import com.peachpasswords.ui.theme.DEFAULT_ACCENT import com.peachpasswords.ui.theme.LocalPeachAccent import com.peachpasswords.ui.theme.peachColors import com.peachpasswords.ui.theme.PeachTheme import com.peachpasswords.ui.theme.PeachType import com.peachpasswords.vault.VaultEntry import com.peachpasswords.vault.VaultRepository import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject /** * Branded bottom sheet shown when the user taps "Search Peach…"/"Unlock Peach" * in the system autofill drop-down. Lists matching vault entries using the same * row vocabulary as the vault list; picking one returns a filled [android.service.autofill.Dataset] * to the framework via [AutofillManager.EXTRA_AUTHENTICATION_RESULT]. */ @AndroidEntryPoint class AutofillEntryPickerActivity : FragmentActivity() { @Inject lateinit var vault: VaultRepository @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val origin = intent.getStringExtra(AutofillIntents.EXTRA_ORIGIN) val isWebOrigin = intent.getBooleanExtra(AutofillIntents.EXTRA_IS_WEB_ORIGIN, false) val parsed = ParsedStructure( usernameId = intentAutofillId(AutofillIntents.EXTRA_IDENTITY_ID), passwordId = intentAutofillId(AutofillIntents.EXTRA_PASSWORD_ID), otpId = intentAutofillId(AutofillIntents.EXTRA_OTP_ID), webDomain = origin.takeIf { isWebOrigin }, packageName = origin.takeUnless { isWebOrigin }, ) val unlocked = vault.isUnlocked.value val entries = if (unlocked) { autofillPickerEntries(vault.getEntries(), parsed, MAX_ROWS) } else { emptyList() } val accent = vault.settings.value.accentColor.let { ACCENT_COLORS[it] } ?: DEFAULT_ACCENT setContent { PeachTheme(accent = accent) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ModalBottomSheet( onDismissRequest = { cancel() }, sheetState = sheetState, containerColor = peachColors.S1, dragHandle = { AccentDragHandle() }, ) { PickerBody( origin = parsed.origin, unlocked = unlocked, entries = entries, onPick = { returnDataset(parsed, it) }, onOpenApp = { openApp() }, ) } } } } private fun returnDataset(parsed: ParsedStructure, entry: VaultEntry) { val dataset = AutofillResponseBuilder.datasetFor(this, parsed, entry, inlineSpec = null) if (dataset == null) { cancel(); return } val reply = Intent().putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, dataset) setResult(RESULT_OK, reply) finish() } private fun openApp() { startActivity(Intent(this, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) cancel() } private fun cancel() { setResult(RESULT_CANCELED) finish() } @Suppress("DEPRECATION") private fun intentAutofillId(key: String): AutofillId? = if (android.os.Build.VERSION.SDK_INT >= 33) { intent.getParcelableExtra(key, AutofillId::class.java) } else { intent.getParcelableExtra(key) } private companion object { const val MAX_ROWS = 12 } } internal fun autofillPickerEntries( entries: List, parsed: ParsedStructure, limit: Int, ): List = when { parsed.webDomain != null -> AutofillResponseBuilder.matchEntries(entries, parsed.webDomain).all.take(limit) parsed.packageName != null -> entries.filter { it.url == parsed.packageName }.take(limit) else -> AutofillResponseBuilder.matchEntries(entries, null).all.take(limit) } @Composable private fun AccentDragHandle() { val accent = LocalPeachAccent.current Row( modifier = Modifier.fillMaxWidth().padding(top = 12.dp, bottom = 6.dp), horizontalArrangement = Arrangement.Center, ) { Box( modifier = Modifier .size(width = 32.dp, height = 4.dp) .background(accent.copy(alpha = 0.7f), RoundedCornerShape(2.dp)), ) } } @Composable private fun PickerBody( origin: String?, unlocked: Boolean, entries: List, onPick: (VaultEntry) -> Unit, onOpenApp: () -> Unit, ) { val accent = LocalPeachAccent.current Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp).padding(bottom = 28.dp)) { Text( text = "peach", color = accent, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 15.sp, ) Text( text = if (origin.isNullOrBlank()) "your vault" else "Suggested for $origin", color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 9.sp, modifier = Modifier.padding(top = 2.dp, bottom = 14.dp), ) if (!unlocked) { Text( text = "Peach is locked. Open the app to unlock, then return to this field.", color = peachColors.T1, fontFamily = PeachType.Body, fontSize = 14.sp, modifier = Modifier.padding(bottom = 16.dp), ) Text( text = "Open Peach", color = accent, fontFamily = PeachType.Mono, fontSize = 13.sp, modifier = Modifier .fillMaxWidth() .clickable(onClick = onOpenApp) .padding(vertical = 12.dp), ) } else if (entries.isEmpty()) { Text( text = "No saved entries match this site.", color = peachColors.T1, fontFamily = PeachType.Body, fontSize = 14.sp, ) } else { LazyColumn { items(entries, key = { it.id }) { entry -> PickerRow(entry, onClick = { onPick(entry) }) } } } } } @Composable private fun PickerRow(entry: VaultEntry, onClick: () -> Unit) { Row( modifier = Modifier .fillMaxWidth() .clickable(onClick = onClick) .padding(vertical = 9.dp), verticalAlignment = Alignment.CenterVertically, ) { FaviconInitials(source = entry.url ?: entry.title, size = 34.dp) Column(modifier = Modifier.padding(start = 12.dp)) { Text( text = entry.title, color = peachColors.T0, fontFamily = PeachType.Body, fontSize = 15.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, ) val sub = entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: "" if (sub.isNotBlank()) { Text( text = sub, color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 11.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, ) } } } }