AutofillEntryPickerActivity.kt

8.4 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 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}