Implement Android Credential Manager and Autofill providers (#109)
Android ARM verification / rust (push) Failing after 49m12s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Failing after 21s

This commit is contained in:
hugo committed 2026-10-12 19:18:10 +02:00
1 parent 316a875d63
commit 0c8366d34f
48 files changed
+4138 -78

No files matched your search

@@ -0,0 +1,172 @@
package de.rfc1437.ironstorage.features
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import de.rfc1437.ironstorage.core.ProviderRequest
@Composable
fun CredentialSelectionScreen(
request: ProviderRequest?,
saving: Boolean,
selected: String?,
query: String,
destination: String,
passphrase: String,
consent: Boolean,
overwrite: Boolean,
busy: Boolean,
failure: String?,
onSelect: (String?) -> Unit,
onQuery: (String) -> Unit,
onDestination: (String) -> Unit,
onPassphrase: (String) -> Unit,
onConsent: (Boolean) -> Unit,
onOverwrite: (Boolean) -> Unit,
onConfirm: () -> Unit,
onCancel: () -> Unit
) {
LazyColumn(
Modifier.fillMaxSize().padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
Text(
stringResource(
if (saving) R.string.provider_save_title else R.string.provider_fill_title
),
style = MaterialTheme.typography.headlineSmall
)
Text(request?.title ?: stringResource(R.string.provider_checking))
}
if (saving) {
item {
OutlinedTextField(
destination,
onDestination,
label = { Text(stringResource(R.string.provider_destination)) },
enabled = !busy,
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
ProviderConsent(
overwrite,
!busy,
stringResource(R.string.provider_overwrite),
onOverwrite
)
}
} else if (selected == null) {
item {
OutlinedTextField(
query,
onQuery,
label = { Text(stringResource(R.string.provider_search)) },
enabled = !busy,
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
}
items(request?.candidates ?: emptyList(), key = { it.path }) { candidate ->
TextButton(onClick = {
onSelect(candidate.path)
}, enabled = !busy, modifier = Modifier.fillMaxWidth()) {
Text(candidate.path)
}
}
} else {
item {
Text(stringResource(R.string.provider_selected, selected))
TextButton(onClick = { onSelect(null) }, enabled = !busy) {
Text(stringResource(R.string.provider_choose_another))
}
}
}
if (saving || selected != null) {
item {
OutlinedTextField(
passphrase,
onPassphrase,
label = { Text(stringResource(R.string.provider_passphrase)) },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
autoCorrectEnabled = false
),
enabled = !busy,
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
val recipient = request?.title ?: stringResource(R.string.provider_this_application)
val confirmation = if (saving) {
stringResource(
R.string.provider_confirm_save
)
} else {
stringResource(R.string.provider_confirm_fill, recipient)
}
ProviderConsent(consent, !busy, confirmation, onConsent)
}
}
item {
failure?.let { Text(it, color = MaterialTheme.colorScheme.error) }
Button(
onClick = onConfirm,
enabled = !busy && consent && request != null &&
(if (saving) destination.isNotBlank() else selected != null)
) {
Text(
stringResource(
if (busy) {
R.string.provider_working
} else if (saving) {
R.string.provider_save
} else {
R.string.provider_fill
}
)
)
}
TextButton(onClick = onCancel) { Text(stringResource(R.string.auth_cancel)) }
}
}
}
@Composable
private fun ProviderConsent(
checked: Boolean,
enabled: Boolean,
label: String,
onChange: (Boolean) -> Unit
) {
Row(
Modifier.fillMaxWidth().toggleable(
value = checked,
enabled = enabled,
role = Role.Checkbox,
onValueChange = onChange
)
) {
Checkbox(checked, null, enabled = enabled)
Text(label, Modifier.padding(top = 12.dp))
}
}
@@ -229,6 +229,13 @@ fun SettingsScreen(
}) { Text(stringResource(R.string.settings_rebuild)) }
}
"providers" -> {
Text(stringResource(R.string.settings_chrome_guidance))
TextButton(onClick = {
uri.openUri(
"https://support.google.com/chrome/answer/142893?co=GENIE.Platform%3DAndroid&hl=en"
)
}) { Text(stringResource(R.string.settings_chrome_help)) }
page?.let {
ProviderSetting(
stringResource(R.string.settings_autofill),
@@ -156,7 +156,7 @@
<string name="settings_git">Git commit author name and email</string>
<string name="settings_auth">Authentication, inactivity timeout, biometrics and appearance</string>
<string name="settings_cache">TOTP discovery cache</string>
<string name="settings_providers">Autofill and Credential Manager</string>
<string name="settings_providers">Autofill and Credential Manager providers</string>
<string name="settings_recovery">Recovery and offline use</string>
<string name="settings_about">Version, licenses, privacy and support</string>
<string name="settings_no_token">No application token stored</string>
@@ -266,4 +266,21 @@
<item quantity="one">%1$s password entry</item>
<item quantity="other">%1$s password entries</item>
</plurals>
<string name="settings_chrome_guidance">After selecting IronStorage in Android settings, open Chrome Settings → Autofill and passwords → Autofill settings. Select Autofill using another service and restart Chrome.</string>
<string name="settings_chrome_help">Chrome autofill help</string>
<string name="provider_fill_title">Fill from IronStorage</string>
<string name="provider_save_title">Save in IronStorage</string>
<string name="provider_checking">Checking the requesting application…</string>
<string name="provider_destination">Destination entry path</string>
<string name="provider_overwrite">Allow updating this destination if it exists</string>
<string name="provider_search">Search entries</string>
<string name="provider_selected">Selected: %1$s</string>
<string name="provider_choose_another">Choose another entry</string>
<string name="provider_passphrase">GPG passphrase (leave empty for biometric unlock)</string>
<string name="provider_this_application">this application</string>
<string name="provider_confirm_save">Save the supplied credential at the chosen destination</string>
<string name="provider_confirm_fill">Allow %1$s to receive this entry\'s username and password</string>
<string name="provider_working">Working…</string>
<string name="provider_save">Confirm save</string>
<string name="provider_fill">Unlock and fill</string>
</resources>