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 parsed = ParsedStructure(65 usernameId = intentAutofillId(AutofillIntents.EXTRA_IDENTITY_ID),66 passwordId = intentAutofillId(AutofillIntents.EXTRA_PASSWORD_ID),67 otpId = intentAutofillId(AutofillIntents.EXTRA_OTP_ID),68 webDomain = intent.getStringExtra(AutofillIntents.EXTRA_ORIGIN),69 )70 val unlocked = vault.isUnlocked.value71 val entries = if (unlocked) {72 AutofillResponseBuilder.matchEntries(vault.getEntries(), parsed.origin).all.take(MAX_ROWS)73 } else {74 emptyList()75 }76 val accent = vault.settings.value.accentColor.let { ACCENT_COLORS[it] } ?: DEFAULT_ACCENT7778 setContent {79 PeachTheme(accent = accent) {80 val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)81 ModalBottomSheet(82 onDismissRequest = { cancel() },83 sheetState = sheetState,84 containerColor = peachColors.S1,85 dragHandle = { AccentDragHandle() },86 ) {87 PickerBody(88 origin = parsed.origin,89 unlocked = unlocked,90 entries = entries,91 onPick = { returnDataset(parsed, it) },92 onOpenApp = { openApp() },93 )94 }95 }96 }97 }9899 private fun returnDataset(parsed: ParsedStructure, entry: VaultEntry) {100 val dataset = AutofillResponseBuilder.datasetFor(this, parsed, entry, inlineSpec = null)101 if (dataset == null) { cancel(); return }102 val reply = Intent().putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, dataset)103 setResult(RESULT_OK, reply)104 finish()105 }106107 private fun openApp() {108 startActivity(Intent(this, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))109 cancel()110 }111112 private fun cancel() {113 setResult(RESULT_CANCELED)114 finish()115 }116117 @Suppress("DEPRECATION")118 private fun intentAutofillId(key: String): AutofillId? =119 if (android.os.Build.VERSION.SDK_INT >= 33) {120 intent.getParcelableExtra(key, AutofillId::class.java)121 } else {122 intent.getParcelableExtra(key)123 }124125 private companion object {126 const val MAX_ROWS = 12127 }128}129130@Composable131private fun AccentDragHandle() {132 val accent = LocalPeachAccent.current133 Row(134 modifier = Modifier.fillMaxWidth().padding(top = 12.dp, bottom = 6.dp),135 horizontalArrangement = Arrangement.Center,136 ) {137 Box(138 modifier = Modifier139 .size(width = 32.dp, height = 4.dp)140 .background(accent.copy(alpha = 0.7f), RoundedCornerShape(2.dp)),141 )142 }143}144145@Composable146private fun PickerBody(147 origin: String?,148 unlocked: Boolean,149 entries: List<VaultEntry>,150 onPick: (VaultEntry) -> Unit,151 onOpenApp: () -> Unit,152) {153 val accent = LocalPeachAccent.current154 Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp).padding(bottom = 28.dp)) {155 Text(156 text = "peach",157 color = accent,158 fontFamily = PeachType.Display,159 fontStyle = FontStyle.Italic,160 fontSize = 15.sp,161 )162 Text(163 text = if (origin.isNullOrBlank()) "your vault" else "Suggested for $origin",164 color = peachColors.T2,165 fontFamily = PeachType.Mono,166 fontSize = 9.sp,167 modifier = Modifier.padding(top = 2.dp, bottom = 14.dp),168 )169170 if (!unlocked) {171 Text(172 text = "Peach is locked. Open the app to unlock, then return to this field.",173 color = peachColors.T1,174 fontFamily = PeachType.Body,175 fontSize = 14.sp,176 modifier = Modifier.padding(bottom = 16.dp),177 )178 Text(179 text = "Open Peach",180 color = accent,181 fontFamily = PeachType.Mono,182 fontSize = 13.sp,183 modifier = Modifier184 .fillMaxWidth()185 .clickable(onClick = onOpenApp)186 .padding(vertical = 12.dp),187 )188 } else if (entries.isEmpty()) {189 Text(190 text = "No saved entries match this site.",191 color = peachColors.T1,192 fontFamily = PeachType.Body,193 fontSize = 14.sp,194 )195 } else {196 LazyColumn {197 items(entries, key = { it.id }) { entry ->198 PickerRow(entry, onClick = { onPick(entry) })199 }200 }201 }202 }203}204205@Composable206private fun PickerRow(entry: VaultEntry, onClick: () -> Unit) {207 Row(208 modifier = Modifier209 .fillMaxWidth()210 .clickable(onClick = onClick)211 .padding(vertical = 9.dp),212 verticalAlignment = Alignment.CenterVertically,213 ) {214 FaviconInitials(source = entry.url ?: entry.title, size = 34.dp)215 Column(modifier = Modifier.padding(start = 12.dp)) {216 Text(217 text = entry.title,218 color = peachColors.T0,219 fontFamily = PeachType.Body,220 fontSize = 15.sp,221 maxLines = 1,222 overflow = TextOverflow.Ellipsis,223 )224 val sub = entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: ""225 if (sub.isNotBlank()) {226 Text(227 text = sub,228 color = peachColors.T2,229 fontFamily = PeachType.Mono,230 fontSize = 11.sp,231 maxLines = 1,232 overflow = TextOverflow.Ellipsis,233 )234 }235 }236 }237}