Harden Android accessibility, localization and startup (#105)
Android ARM verification / rust (push) Failing after 46m41s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Successful in 22s

Implement independent hardening and regression coverage. Keep #105 open for final feature and environment coverage after #106–#110; document the completion boundary and resume order.
This commit is contained in:
hugo committed 2026-10-11 15:56:33 +02:00
1 parent 43f660ace3
commit 6669b77f4e
55 files changed
+24445 -296

No files matched your search

@@ -63,7 +63,7 @@ fun EntryEditorScreen(repository: EditorRepository, authentication: Authenticati
) {
item {
Text(
state.title,
state.title.ifEmpty { stringResource(R.string.editor_initial_title) },
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.semantics {
heading()
@@ -165,11 +165,39 @@ fun EntryEditorScreen(repository: EditorRepository, authentication: Authenticati
})
)
field.diagnostic?.let { Text(it) }
val revealLabel = stringResource(
R.string.entry_field_action,
stringResource(if (shown) R.string.editor_hide else R.string.editor_reveal),
field.label
)
val removeLabel =
stringResource(
R.string.entry_field_action,
stringResource(R.string.editor_remove),
field.label
)
val upLabel =
stringResource(
R.string.entry_field_action,
stringResource(R.string.editor_up),
field.label
)
val downLabel =
stringResource(
R.string.entry_field_action,
stringResource(R.string.editor_down),
field.label
)
FlowRow {
TextButton(onClick = {
revealed =
if (shown) revealed - field.id else revealed + field.id
}) {
TextButton(
modifier = Modifier.semantics {
contentDescription = revealLabel
},
onClick = {
revealed =
if (shown) revealed - field.id else revealed + field.id
}
) {
Text(
stringResource(
if (shown) R.string.editor_hide else R.string.editor_reveal
@@ -177,9 +205,16 @@ fun EntryEditorScreen(repository: EditorRepository, authentication: Authenticati
)
}
if (field.removable) {
TextButton(enabled = !state.busy, onClick = {
repository.remove(field.id)
}, modifier = Modifier.testTag("editor-remove-${field.id}")) {
TextButton(
enabled = !state.busy,
onClick = {
repository.remove(field.id)
},
modifier = Modifier.testTag("editor-remove-${field.id}").semantics {
contentDescription =
removeLabel
}
) {
Text(stringResource(R.string.editor_remove))
}
}
@@ -196,20 +231,30 @@ fun EntryEditorScreen(repository: EditorRepository, authentication: Authenticati
).toUInt()
)
},
modifier = Modifier.testTag("editor-up-${field.id}")
modifier = Modifier.testTag("editor-up-${field.id}").semantics {
contentDescription =
upLabel
}
) {
Text(stringResource(R.string.editor_up))
}
val canMoveDown = !state.busy && index < state.fields.lastIndex
TextButton(enabled = canMoveDown, onClick = {
repository.reorder(
field.id,
(
index +
1
).toUInt()
)
}, modifier = Modifier.testTag("editor-down-${field.id}")) {
TextButton(
enabled = canMoveDown,
onClick = {
repository.reorder(
field.id,
(
index +
1
).toUInt()
)
},
modifier = Modifier.testTag("editor-down-${field.id}").semantics {
contentDescription =
downLabel
}
) {
Text(stringResource(R.string.editor_down))
}
}
@@ -156,11 +156,13 @@ fun LazyListScope.entryViewer(state: PasswordState, repository: PasswordReposito
stringResource(
if (field.revealed) R.string.entry_hide else R.string.entry_reveal
)
val revealLabel =
stringResource(R.string.entry_field_action, reveal, field.label)
TextButton(
enabled = !state.loading,
modifier = Modifier.semantics {
contentDescription =
"$reveal ${field.label}"
revealLabel
},
onClick = {
if (field.revealed) {
@@ -174,20 +176,23 @@ fun LazyListScope.entryViewer(state: PasswordState, repository: PasswordReposito
) { Text(reveal) }
}
val copy = stringResource(R.string.entry_copy)
val copyLabel = stringResource(R.string.entry_field_action, copy, field.label)
TextButton(
enabled = !state.loading,
modifier = Modifier.semantics {
contentDescription = "$copy ${field.label}"
contentDescription = copyLabel
},
onClick = { repository.copy(field.id) }
) { Text(copy) }
if (field.editable) {
val edit = stringResource(R.string.entry_edit)
val editLabel =
stringResource(R.string.entry_field_action, edit, field.label)
TextButton(
enabled = !state.loading,
modifier = Modifier.semantics {
contentDescription =
"$edit ${field.label}"
editLabel
},
onClick = { repository.edit(field.id) }
) { Text(edit) }
@@ -84,14 +84,14 @@ fun OnboardingScreen(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
state.title,
state.title.ifEmpty { stringResource(R.string.setup_initial_title) },
modifier = Modifier.semantics {
heading()
},
style = MaterialTheme.typography.headlineSmall
)
Text(
state.detail,
state.detail.ifEmpty { stringResource(R.string.setup_initial_detail) },
color = if (state.failure) {
MaterialTheme.colorScheme.error
} else {
@@ -142,7 +142,9 @@ fun PasswordScreen(
item {
Column(Modifier.padding(16.dp)) {
Text(
state.title,
state.title.ifEmpty {
stringResource(R.string.password_initial_title)
},
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.semantics { heading() }
)
@@ -22,6 +22,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.heading
@@ -88,7 +89,12 @@ fun SearchScreen(
CircularProgressIndicator()
Text(stringResource(R.string.search_loading))
} else if (state.error != null) {
Text(requireNotNull(state.error), color = MaterialTheme.colorScheme.error)
Text(
state.error.orEmpty().ifEmpty {
stringResource(R.string.search_recovery)
},
color = MaterialTheme.colorScheme.error
)
} else if (state.cancelled) {
Text(stringResource(R.string.search_cancelled))
} else if (state.results.isEmpty()) {
@@ -98,7 +104,13 @@ fun SearchScreen(
)
)
} else {
Text(stringResource(R.string.search_count, state.results.size))
Text(
pluralStringResource(
R.plurals.search_results,
state.results.size,
state.results.size
)
)
}
}
LazyColumn(Modifier.weight(1f).fillMaxSize()) {
@@ -28,6 +28,7 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
@@ -37,6 +38,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
@@ -52,6 +54,8 @@ import de.rfc1437.ironstorage.core.AuthenticationRepository
import de.rfc1437.ironstorage.core.OnboardingRepository
import de.rfc1437.ironstorage.core.ProviderStatus
import de.rfc1437.ironstorage.core.SettingsRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun SettingsScreen(viewModel: ShellViewModel, setup: SetupFormViewModel) {
@@ -190,9 +194,10 @@ fun SettingsScreen(
)
)
Text(
stringResource(
R.string.settings_timeout_value,
it.timeout.toString()
pluralStringResource(
R.plurals.settings_timeout_value,
it.timeout.toInt(),
it.timeout.toLong()
)
)
}
@@ -288,12 +293,12 @@ fun SettingsScreen(
if (state.transfer.open) KeyTransferDialog(repository, authentication)
if (licenses) {
val resources = LocalResources.current
val text =
remember(resources) {
resources.openRawResource(R.raw.license).bufferedReader().use {
it.readText()
}
val loading = stringResource(R.string.settings_license_loading)
val text by produceState(loading, resources) {
value = withContext(Dispatchers.IO) {
resources.openRawResource(R.raw.license).bufferedReader().use { it.readText() }
}
}
AlertDialog(
onDismissRequest = {
licenses = false
@@ -491,8 +496,8 @@ private fun KeyTransferDialog(
val description =
stringResource(
R.string.settings_qr_frame,
qr.sequence.toString(),
qr.total.toString()
qr.sequence.toInt(),
qr.total.toInt()
)
Text(description)
Canvas(
@@ -552,10 +557,11 @@ private fun KeyTransferDialog(
""
}) { Text(stringResource(R.string.settings_qr_add)) }
Text(
stringResource(
R.string.settings_qr_progress,
state.transfer.received.toString(),
state.transfer.total.toString()
pluralStringResource(
R.plurals.settings_qr_progress,
state.transfer.total.toInt(),
state.transfer.received.toInt(),
state.transfer.total.toInt()
)
)
state.transfer.ready?.let {
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
@@ -81,6 +82,7 @@ fun ShellScreen(viewModel: ShellViewModel, setupForm: SetupFormViewModel) {
val rail = maxWidth >= 600.dp
val largeFont = LocalDensity.current.fontScale >= 1.6f
Scaffold(
modifier = Modifier.imePadding(),
bottomBar = {
if (!rail) {
NavigationBar(Modifier.testTag("shell-bottom-navigation")) {
@@ -110,7 +110,6 @@
<string name="search_loading">Searching names…</string>
<string name="search_prompt">Enter an entry name, folder, or path.</string>
<string name="search_empty">No matching entries.</string>
<string name="search_count">Results: %1$d</string>
<string name="search_open">Open entry</string>
<string name="search_cancelled">Search cancelled. Refresh to try again.</string>
<string name="shell_appearance">Appearance</string>
@@ -168,7 +167,10 @@
<string name="settings_edit_git">Edit Git author</string>
<string name="settings_biometry_available">Strong biometric authentication is available</string>
<string name="settings_biometry_unavailable">Strong biometric authentication is unavailable; use a GPG passphrase</string>
<string name="settings_timeout_value">Inactivity timeout: %1$s seconds</string>
<plurals name="settings_timeout_value">
<item quantity="one">Inactivity timeout: %1$d second</item>
<item quantity="other">Inactivity timeout: %1$d seconds</item>
</plurals>
<string name="settings_edit_timeout">Edit inactivity timeout</string>
<string name="settings_lock_now">Lock now</string>
<string name="settings_rebuild">Rebuild TOTP cache</string>
@@ -197,15 +199,29 @@
<string name="settings_private_warning">Private QR codes contain encrypted secret key material. Export only after checking the intended recipient. A private export requires its GPG passphrase.</string>
<string name="settings_private_export">Export private key</string>
<string name="settings_export_confirm">Confirm and export selected key</string>
<string name="settings_qr_frame">QR frame %1$s of %2$s</string>
<string name="settings_qr_frame">QR frame %1$d of %2$d</string>
<string name="settings_previous">Previous frame</string>
<string name="settings_next">Next frame</string>
<string name="settings_qr_images">Choose QR images</string>
<string name="settings_qr_images_detail">Select all frames of one transfer together. Opening another app clears the current transfer.</string>
<string name="settings_qr_payload">QR payload or armored GPG key</string>
<string name="settings_qr_add">Add transfer payload</string>
<string name="settings_qr_progress">Received %1$s of %2$s frames</string>
<plurals name="settings_qr_progress">
<item quantity="one">Received %1$d of %2$d frame</item>
<item quantity="other">Received %1$d of %2$d frames</item>
</plurals>
<string name="settings_make_default">Make imported private key the default</string>
<string name="settings_replace_key">Confirm replacement of an existing key</string>
<string name="settings_import_confirm">Confirm and import reconstructed key</string>
<string name="setup_initial_title">Set up your password store</string>
<string name="setup_initial_detail">Create a local store or clone an existing HTTPS repository.</string>
<string name="editor_initial_title">Entry editor</string>
<string name="password_initial_title">Passwords</string>
<string name="search_recovery">Search is unavailable. Unlock the device or recover the store, then retry.</string>
<string name="entry_field_action">%1$s %2$s</string>
<plurals name="search_results">
<item quantity="one">%1$d result</item>
<item quantity="other">%1$d results</item>
</plurals>
<string name="settings_license_loading">Loading license…</string>
</resources>