From 6195060f35c78249f4630865e09855f9e624bd32 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Thu, 5 Nov 2026 15:52:37 +0100 Subject: feat(app): add an about screen Version with build number, documentation and project links, Ko-fi, the privacy statement, the certificate this copy was really signed with, and the licenses. Debug builds also show the git commit, since a test build can share its version with a release. Co-Authored-By: Claude Opus 5.5 --- .../app/passwordstore/ui/settings/AboutScreen.kt | 208 +++++++++++++++++++++ .../app/passwordstore/ui/settings/SettingsGraph.kt | 7 + .../passwordstore/ui/settings/SettingsScreen.kt | 10 +- .../main/res/drawable-nodpi/ic_kofi_symbol.webp | Bin 0 -> 4100 bytes app/src/main/res/values-it/strings.xml | 15 ++ app/src/main/res/values/strings.xml | 15 ++ 6 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/app/passwordstore/ui/settings/AboutScreen.kt create mode 100644 app/src/main/res/drawable-nodpi/ic_kofi_symbol.webp (limited to 'app/src/main') diff --git a/app/src/main/java/app/passwordstore/ui/settings/AboutScreen.kt b/app/src/main/java/app/passwordstore/ui/settings/AboutScreen.kt new file mode 100644 index 00000000..fd47c119 --- /dev/null +++ b/app/src/main/java/app/passwordstore/ui/settings/AboutScreen.kt @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2026 Danilo M. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.ui.settings + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import androidx.fragment.app.FragmentActivity +import app.passwordstore.BuildConfig +import app.passwordstore.R +import app.passwordstore.ui.compose.theme.SpacingMedium +import app.passwordstore.ui.crypto.copyTextToClipboard +import app.passwordstore.util.extensions.getPackageInfoCompat +import app.passwordstore.util.extensions.snackbar +import java.security.MessageDigest + +private const val REPOSITORY = "https://github.com/danixland/HardenedPass" +private const val DOCUMENTATION = "https://danix.xyz/hardenedpass" +private const val KO_FI = "https://ko-fi.com/K6Y520C9I6" + +/** + * What ships in the APK and its license, by hand: update it when a dependency comes or goes. A + * generating plugin would be one more build dependency for a list this short. + */ +private val LICENSES = + listOf( + "AndroidX, Jetpack Compose" to "Apache-2.0", + "Apache Commons Codec, Curtains, Guava, JavaEWAH, JSpecify, uri-kmp" to "Apache-2.0", + "Bouncy Castle" to "Bouncy Castle Licence (MIT)", + "Dagger, Hilt" to "Apache-2.0", + "EFF Diceware word list" to "CC-BY", + "JGit" to "EDL-1.0 (BSD-3-Clause)", + "kotlin-result" to "ISC", + "Kotlin, kotlinx" to "Apache-2.0", + "LeakCanary plumber, logcat" to "Apache-2.0", + "Manrope" to "OFL-1.1", + "PGPainless" to "Apache-2.0", + "Public Suffix List" to "MPL-2.0", + "SLF4J" to "MIT", + "sshj" to "Apache-2.0", + "sublime-fuzzy" to "MIT", + "ZXing, zxing-android-embedded" to "Apache-2.0", + ) + +/** `0.3.0 (300)`, followed by the [commit] on the debug builds that have one. */ +internal fun versionLine(name: String, code: Int, commit: String): String = + if (commit.isEmpty()) "$name ($code)" else "$name ($code) · $commit" + +/** The changelog at the tag of [versionName], without a suffix like `-debug`. */ +internal fun changelogUrl(versionName: String): String = + "$REPOSITORY/blob/v${versionName.substringBefore('-')}/CHANGELOG.md" + +/** SHA-256 of [certificate] as `apksigner` and `keytool` users compare it, colon separated. */ +internal fun certificateFingerprint(certificate: ByteArray): String = + MessageDigest.getInstance("SHA-256").digest(certificate).joinToString(":") { "%02X".format(it) } + +/** The certificate this copy was really signed with, so it can be compared with the published. */ +private fun Context.signingFingerprint(): String? = + packageManager + .getPackageInfoCompat(packageName, PackageManager.GET_SIGNING_CERTIFICATES) + .signingInfo + ?.apkContentsSigners + ?.firstOrNull() + ?.let { certificateFingerprint(it.toByteArray()) } + +/** The about screen. Links leave for the browser: the app itself makes no connection for them. */ +@Composable +fun AboutDestination( + activity: FragmentActivity, + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + val fingerprint = remember { activity.signingFingerprint() } + AboutScreen( + version = + versionLine(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, BuildConfig.GIT_COMMIT), + changelog = changelogUrl(BuildConfig.VERSION_NAME), + fingerprint = fingerprint, + onCopy = { activity.copyTextToClipboard(it) }, + onOpen = { url -> + try { + activity.startActivity(Intent(Intent.ACTION_VIEW, url.toUri())) + } catch (_: ActivityNotFoundException) { + activity.snackbar(message = activity.getString(R.string.about_no_browser)) + } + }, + onBack = onBack, + modifier = modifier, + ) +} + +@Composable +private fun AboutScreen( + version: String, + changelog: String, + fingerprint: String?, + onCopy: (String) -> Unit, + onOpen: (String) -> Unit, + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + SettingsScaffold(stringResource(R.string.about), onBack, modifier) { + Column( + Modifier.fillMaxWidth().padding(SpacingMedium), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(SpacingMedium), + ) { + Box(Modifier.size(96.dp).clip(CircleShape)) { + Image(painterResource(R.drawable.ic_launcher_background), contentDescription = null) + Image(painterResource(R.drawable.ic_launcher_foreground), contentDescription = null) + } + Text(stringResource(R.string.app_name), style = MaterialTheme.typography.headlineSmall) + Text(stringResource(R.string.about_summary), textAlign = TextAlign.Center) + } + AboutRow(stringResource(R.string.about_version), version) { onCopy(version) } + AboutRow(stringResource(R.string.about_docs)) { onOpen(DOCUMENTATION) } + AboutRow(stringResource(R.string.about_changelog)) { onOpen(changelog) } + AboutRow(stringResource(R.string.about_source)) { onOpen(REPOSITORY) } + AboutRow(stringResource(R.string.about_issues)) { onOpen("$REPOSITORY/issues") } + // White in both themes: the logo's dark outline disappears on a dark background, and Ko-fi + // asks that the logo itself stays as it is. + Button( + onClick = { onOpen(KO_FI) }, + modifier = Modifier.fillMaxWidth().padding(SpacingMedium), + colors = + ButtonDefaults.buttonColors(containerColor = Color.White, contentColor = Color.Black), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Image( + painterResource(R.drawable.ic_kofi_symbol), + contentDescription = null, + modifier = Modifier.height(24.dp), + ) + Box(Modifier.width(SpacingMedium)) + Text(stringResource(R.string.about_support)) + } + ListItem( + headlineContent = { Text(stringResource(R.string.about_privacy_title)) }, + supportingContent = { Text(stringResource(R.string.about_privacy)) }, + colors = rowColors(), + ) + if (fingerprint != null) + ListItem( + headlineContent = { Text(stringResource(R.string.about_certificate)) }, + supportingContent = { Text(fingerprint, fontFamily = FontFamily.Monospace) }, + modifier = Modifier.clickable(role = Role.Button) { onCopy(fingerprint) }, + colors = rowColors(), + ) + AboutRow(stringResource(R.string.about_license), stringResource(R.string.about_license_text)) { + onOpen("$REPOSITORY/blob/master/LICENSE.txt") + } + ListItem( + headlineContent = { Text(stringResource(R.string.about_libraries)) }, + colors = rowColors(), + ) + LICENSES.forEach { (name, license) -> + ListItem( + headlineContent = { Text(name) }, + supportingContent = { Text(license) }, + colors = rowColors(), + ) + } + } +} + +@Composable +private fun AboutRow(title: String, summary: String? = null, onClick: () -> Unit) { + ListItem( + headlineContent = { Text(title) }, + supportingContent = summary?.let { { Text(it) } }, + modifier = Modifier.clickable(role = Role.Button, onClick = onClick), + colors = rowColors(), + ) +} diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsGraph.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsGraph.kt index ae4e277b..44ae06d8 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/SettingsGraph.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsGraph.kt @@ -30,6 +30,9 @@ import kotlinx.serialization.Serializable /** The store's commit log. */ @Serializable data object GitLogRoute +/** Version, links, certificate and licenses. */ +@Serializable data object AboutRoute + /** HTTP(S) proxy settings. */ @Serializable data object ProxyRoute @@ -44,9 +47,13 @@ fun NavGraphBuilder.settingsGraph(activity: MainActivity, nav: NavHostController composable { entry -> SettingsScreen( onSection = { if (onTop(entry)) nav.navigate(SettingsSectionRoute(it)) }, + onAbout = { if (onTop(entry)) nav.navigate(AboutRoute) }, onBack = { close(entry) }, ) } + composable { entry -> + AboutDestination(activity = activity, onBack = { close(entry) }) + } composable { entry -> SettingsSectionDestination( activity = activity, diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt index d13d1cf5..14895017 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Info import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -68,7 +69,7 @@ enum class SettingsAction { /** Top bar with [title] and back, then [content] in one grouped surface, scrolling. */ @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun SettingsScaffold( +internal fun SettingsScaffold( title: String, onBack: () -> Unit, modifier: Modifier = Modifier, @@ -97,6 +98,7 @@ private fun SettingsScaffold( @Composable fun SettingsScreen( onSection: (SettingsSection) -> Unit, + onAbout: () -> Unit, onBack: () -> Unit, modifier: Modifier = Modifier, ) { @@ -109,6 +111,12 @@ fun SettingsScreen( colors = rowColors(), ) } + ListItem( + headlineContent = { Text(stringResource(R.string.about)) }, + modifier = Modifier.clickable(role = Role.Button, onClick = onAbout), + leadingContent = { Icon(Icons.Filled.Info, contentDescription = null) }, + colors = rowColors(), + ) } } diff --git a/app/src/main/res/drawable-nodpi/ic_kofi_symbol.webp b/app/src/main/res/drawable-nodpi/ic_kofi_symbol.webp new file mode 100644 index 00000000..f31847d8 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/ic_kofi_symbol.webp differ diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index e15b903f..f6de40bd 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -379,4 +379,19 @@ I commit e le modifiche locali che non sono su questo remote branch vengono scartati. %1$s: completato Eliminare l\'archivio locale? Le voci di cui non è stato fatto il push sul remote vanno perse. + Informazioni + Un client Android per pass, il gestore di password standard di Unix. Un fork mantenuto di Android Password Store. + Versione + Documentazione + Novità + Codice sorgente + Segnala un problema + Sostieni su Ko-fi + Privacy + Nessuna telemetria, nessuna analisi, nessun servizio Google. HardenedPass si collega solo al tuo server git, tramite il tuo proxy se ne imposti uno. + Certificato di firma (SHA-256) + Licenza + GNU General Public License v3.0 only. Copyright degli autori di Android Password Store e dei contributori di HardenedPass. + Licenze open source + Nessun browser trovato per aprire questo link diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 755fa9bc..38ef2cdc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -413,4 +413,19 @@ Local commits and changes that are not on this remote branch are discarded. %1$s: done Delete the local store? Entries not pushed to the remote are lost. + About + An Android client for pass, the standard Unix password manager. A maintained fork of Android Password Store. + Version + Documentation + What\'s new + Source code + Report a problem + Support on Ko-fi + Privacy + No telemetry, no analytics, no Google services. HardenedPass connects only to your git server, through your proxy if you set one. + Signing certificate (SHA-256) + License + GNU General Public License v3.0 only. Copyright the Android Password Store authors and HardenedPass contributors. + Open source licenses + No browser found to open this link -- cgit v1.2.3