Implement Android Credential Manager and Autofill providers (#109)
This commit is contained in:
1 parent
316a875d63
commit
0c8366d34f
48 files changed
+4138
-78
No files matched your search
+172
@@ -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>
|
||||
Reference in new issue
Block a user