1package com.peachpasswords.autofill23import android.content.Intent4import android.os.Bundle5import android.view.autofill.AutofillId6import android.view.autofill.AutofillManager7import androidx.activity.compose.setContent8import androidx.compose.foundation.background9import androidx.compose.foundation.clickable10import androidx.compose.foundation.layout.Arrangement11import androidx.compose.foundation.layout.Box12import androidx.compose.foundation.layout.Column13import androidx.compose.foundation.layout.Row14import androidx.compose.foundation.layout.fillMaxWidth15import androidx.compose.foundation.layout.padding16import androidx.compose.foundation.layout.size17import androidx.compose.foundation.lazy.LazyColumn18import androidx.compose.foundation.lazy.items19import androidx.compose.foundation.shape.RoundedCornerShape20import androidx.compose.material3.ExperimentalMaterial3Api21import androidx.compose.material3.ModalBottomSheet22import androidx.compose.material3.Text23import androidx.compose.material3.rememberModalBottomSheetState24import androidx.compose.runtime.Composable25import androidx.compose.runtime.getValue26import androidx.compose.runtime.remember27import androidx.compose.ui.Alignment28import androidx.compose.ui.Modifier29import androidx.compose.ui.graphics.Color30import androidx.compose.ui.text.font.FontStyle31import androidx.compose.ui.text.style.TextOverflow32import androidx.compose.ui.unit.dp33import androidx.compose.ui.unit.sp34import androidx.fragment.app.FragmentActivity35import com.peachpasswords.MainActivity36import com.peachpasswords.shared.extractDomain37import com.peachpasswords.ui.components.FaviconInitials38import com.peachpasswords.ui.theme.ACCENT_COLORS39import com.peachpasswords.ui.theme.DEFAULT_ACCENT40import com.peachpasswords.ui.theme.LocalPeachAccent41import com.peachpasswords.ui.theme.peachColors42import com.peachpasswords.ui.theme.PeachTheme43import com.peachpasswords.ui.theme.PeachType44import com.peachpasswords.vault.VaultEntry45import com.peachpasswords.vault.VaultRepository46import dagger.hilt.android.AndroidEntryPoint47import javax.inject.Inject4849505152535455@AndroidEntryPoint56class AutofillEntryPickerActivity : FragmentActivity() {5758 @Inject lateinit var vault: VaultRepository5960 @OptIn(ExperimentalMaterial3Api::class)61 override fun onCreate(savedInstanceState: Bundle?) {62 super.onCreate(savedInstanceState)6364 val origin = intent.getStringExtra(AutofillIntents.EXTRA_ORIGIN)65 val isWebOrigin = intent.getBooleanExtra(AutofillIntents.EXTRA_IS_WEB_ORIGIN, false)66 val parsed = ParsedStructure(67 usernameId = intentAutofillId(AutofillIntents.EXTRA_IDENTITY_ID),68 passwordId = intentAutofillId(AutofillIntents.EXTRA_PASSWORD_ID),69 otpId = intentAutofillId(AutofillIntents.EXTRA_OTP_ID),70 webDomain = origin.takeIf { isWebOrigin },71 packageName = origin.takeUnless { isWebOrigin },72 )73 val unlocked = vault.isUnlocked.value74 val entries = if (unlocked) {75 autofillPickerEntries(vault.getEntries(), parsed, MAX_ROWS)76 } else {77 emptyList()78 }79 val accent = vault.settings.value.accentColor.let { ACCENT_COLORS[it] } ?: DEFAULT_ACCENT8081 setContent {82 PeachTheme(accent = accent) {83 val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)84 ModalBottomSheet(85 onDismissRequest = { cancel() },86 sheetState = sheetState,87 containerColor = peachColors.S1,88 dragHandle = { AccentDragHandle() },89 ) {90 PickerBody(91 origin = parsed.origin,92 unlocked = unlocked,93 entries = entries,94 onPick = { returnDataset(parsed, it) },95 onOpenApp = { openApp() },96 )97 }98 }99 }100 }101102 private fun returnDataset(parsed: ParsedStructure, entry: VaultEntry) {103 val dataset = AutofillResponseBuilder.datasetFor(this, parsed, entry, inlineSpec = null)104 if (dataset == null) { cancel(); return }105 val reply = Intent().putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, dataset)106 setResult(RESULT_OK, reply)107 finish()108 }109110 private fun openApp() {111 startActivity(Intent(this, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))112 cancel()113 }114115 private fun cancel() {116 setResult(RESULT_CANCELED)117 finish()118 }119120 @Suppress("DEPRECATION")121 private fun intentAutofillId(key: String): AutofillId? =122 if (android.os.Build.VERSION.SDK_INT >= 33) {123 intent.getParcelableExtra(key, AutofillId::class.java)124 } else {125 intent.getParcelableExtra(key)126 }127128 private companion object {129 const val MAX_ROWS = 12130 }131}132133internal fun autofillPickerEntries(134 entries: List<VaultEntry>,135 parsed: ParsedStructure,136 limit: Int,137): List<VaultEntry> = when {138 parsed.webDomain != null -> AutofillResponseBuilder.matchEntries(entries, parsed.webDomain).all.take(limit)139 parsed.packageName != null -> entries.filter { it.url == parsed.packageName }.take(limit)140 else -> AutofillResponseBuilder.matchEntries(entries, null).all.take(limit)141}142143@Composable144private fun AccentDragHandle() {145 val accent = LocalPeachAccent.current146 Row(147 modifier = Modifier.fillMaxWidth().padding(top = 12.dp, bottom = 6.dp),148 horizontalArrangement = Arrangement.Center,149 ) {150 Box(151 modifier = Modifier152 .size(width = 32.dp, height = 4.dp)153 .background(accent.copy(alpha = 0.7f), RoundedCornerShape(2.dp)),154 )155 }156}157158@Composable159private fun PickerBody(160 origin: String?,161 unlocked: Boolean,162 entries: List<VaultEntry>,163 onPick: (VaultEntry) -> Unit,164 onOpenApp: () -> Unit,165) {166 val accent = LocalPeachAccent.current167 Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp).padding(bottom = 28.dp)) {168 Text(169 text = "peach",170 color = accent,171 fontFamily = PeachType.Display,172 fontStyle = FontStyle.Italic,173 fontSize = 15.sp,174 )175 Text(176 text = if (origin.isNullOrBlank()) "your vault" else "Suggested for $origin",177 color = peachColors.T2,178 fontFamily = PeachType.Mono,179 fontSize = 9.sp,180 modifier = Modifier.padding(top = 2.dp, bottom = 14.dp),181 )182183 if (!unlocked) {184 Text(185 text = "Peach is locked. Open the app to unlock, then return to this field.",186 color = peachColors.T1,187 fontFamily = PeachType.Body,188 fontSize = 14.sp,189 modifier = Modifier.padding(bottom = 16.dp),190 )191 Text(192 text = "Open Peach",193 color = accent,194 fontFamily = PeachType.Mono,195 fontSize = 13.sp,196 modifier = Modifier197 .fillMaxWidth()198 .clickable(onClick = onOpenApp)199 .padding(vertical = 12.dp),200 )201 } else if (entries.isEmpty()) {202 Text(203 text = "No saved entries match this site.",204 color = peachColors.T1,205 fontFamily = PeachType.Body,206 fontSize = 14.sp,207 )208 } else {209 LazyColumn {210 items(entries, key = { it.id }) { entry ->211 PickerRow(entry, onClick = { onPick(entry) })212 }213 }214 }215 }216}217218@Composable219private fun PickerRow(entry: VaultEntry, onClick: () -> Unit) {220 Row(221 modifier = Modifier222 .fillMaxWidth()223 .clickable(onClick = onClick)224 .padding(vertical = 9.dp),225 verticalAlignment = Alignment.CenterVertically,226 ) {227 FaviconInitials(source = entry.url ?: entry.title, size = 34.dp)228 Column(modifier = Modifier.padding(start = 12.dp)) {229 Text(230 text = entry.title,231 color = peachColors.T0,232 fontFamily = PeachType.Body,233 fontSize = 15.sp,234 maxLines = 1,235 overflow = TextOverflow.Ellipsis,236 )237 val sub = entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: ""238 if (sub.isNotBlank()) {239 Text(240 text = sub,241 color = peachColors.T2,242 fontFamily = PeachType.Mono,243 fontSize = 11.sp,244 maxLines = 1,245 overflow = TextOverflow.Ellipsis,246 )247 }248 }249 }250}