AutofillEntryPickerActivity.kt

9.0 KB · raw

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.Inject4849/**50 * Branded bottom sheet shown when the user taps "Search Peach…"/"Unlock Peach"51 * in the system autofill drop-down. Lists matching vault entries using the same52 * row vocabulary as the vault list; picking one returns a filled [android.service.autofill.Dataset]53 * to the framework via [AutofillManager.EXTRA_AUTHENTICATION_RESULT].54 */55@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}