aboutsummaryrefslogtreecommitdiffstats
path: root/app/src
diff options
context:
space:
mode:
Diffstat (limited to 'app/src')
-rw-r--r--app/src/debug/ic_launcher-playstore.pngbin20129 -> 0 bytes
-rw-r--r--app/src/debug/res/drawable/ic_launcher_background.xml2
-rw-r--r--app/src/debug/res/drawable/ic_launcher_foreground.xml63
-rw-r--r--app/src/debug/res/mipmap-anydpi/ic_launcher.xml10
-rw-r--r--app/src/debug/res/mipmap-hdpi/ic_launcher.pngbin4212 -> 0 bytes
-rw-r--r--app/src/debug/res/mipmap-mdpi/ic_launcher.pngbin2635 -> 0 bytes
-rw-r--r--app/src/debug/res/mipmap-xhdpi/ic_launcher.pngbin5946 -> 0 bytes
-rw-r--r--app/src/debug/res/mipmap-xxhdpi/ic_launcher.pngbin9368 -> 0 bytes
-rw-r--r--app/src/debug/res/mipmap-xxxhdpi/ic_launcher.pngbin13251 -> 0 bytes
-rw-r--r--app/src/main/AndroidManifest.xml8
-rw-r--r--app/src/main/ic_launcher-playstore.pngbin20843 -> 0 bytes
-rw-r--r--app/src/main/java/app/passwordstore/Application.kt59
-rw-r--r--app/src/main/java/app/passwordstore/injection/AutofillResponseBuilderModule.kt11
-rw-r--r--app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt11
-rw-r--r--app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt12
-rw-r--r--app/src/main/java/app/passwordstore/ui/autofill/AutofillPublisherChangedActivity.kt12
-rw-r--r--app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt6
-rw-r--r--app/src/main/java/app/passwordstore/ui/crypto/CryptoExtensions.kt23
-rw-r--r--app/src/main/java/app/passwordstore/ui/editor/EditorDestination.kt13
-rw-r--r--app/src/main/java/app/passwordstore/ui/editor/EditorViewModel.kt10
-rw-r--r--app/src/main/java/app/passwordstore/ui/git/base/BaseGitActivity.kt27
-rw-r--r--app/src/main/java/app/passwordstore/ui/list/ListDestination.kt2
-rw-r--r--app/src/main/java/app/passwordstore/ui/list/ListScreen.kt11
-rw-r--r--app/src/main/java/app/passwordstore/ui/list/StoreActions.kt72
-rw-r--r--app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt4
-rw-r--r--app/src/main/java/app/passwordstore/ui/main/MainActivity.kt10
-rw-r--r--app/src/main/java/app/passwordstore/ui/prompts/Prompts.kt293
-rw-r--r--app/src/main/java/app/passwordstore/ui/settings/AboutScreen.kt231
-rw-r--r--app/src/main/java/app/passwordstore/ui/settings/GitConfigScreen.kt4
-rw-r--r--app/src/main/java/app/passwordstore/ui/settings/ProxyViewModel.kt6
-rw-r--r--app/src/main/java/app/passwordstore/ui/settings/SettingsDestination.kt13
-rw-r--r--app/src/main/java/app/passwordstore/ui/settings/SettingsGraph.kt9
-rw-r--r--app/src/main/java/app/passwordstore/ui/settings/SettingsModel.kt15
-rw-r--r--app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt17
-rw-r--r--app/src/main/java/app/passwordstore/ui/settings/SettingsViewModel.kt30
-rw-r--r--app/src/main/java/app/passwordstore/ui/setup/GitServerScreen.kt4
-rw-r--r--app/src/main/java/app/passwordstore/ui/setup/PgpKeysScreen.kt4
-rw-r--r--app/src/main/java/app/passwordstore/ui/setup/SshKeyScreen.kt2
-rw-r--r--app/src/main/java/app/passwordstore/util/auth/BiometricAuthenticator.kt9
-rw-r--r--app/src/main/java/app/passwordstore/util/autofill/Api26AutofillResponseBuilder.kt173
-rw-r--r--app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt1
-rw-r--r--app/src/main/java/app/passwordstore/util/autofill/AutofillMatcher.kt2
-rw-r--r--app/src/main/java/app/passwordstore/util/autofill/AutofillResponseBuilder.kt11
-rw-r--r--app/src/main/java/app/passwordstore/util/autofill/AutofillViewUtils.kt11
-rw-r--r--app/src/main/java/app/passwordstore/util/crypto/KeystorePreferences.kt21
-rw-r--r--app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt23
-rw-r--r--app/src/main/java/app/passwordstore/util/git/GitCommandExecutor.kt9
-rw-r--r--app/src/main/java/app/passwordstore/util/git/operation/BreakOutOfDetached.kt22
-rw-r--r--app/src/main/java/app/passwordstore/util/git/operation/CloneOperation.kt4
-rw-r--r--app/src/main/java/app/passwordstore/util/git/operation/CredentialFinder.kt56
-rw-r--r--app/src/main/java/app/passwordstore/util/git/operation/GcOperation.kt4
-rw-r--r--app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt47
-rw-r--r--app/src/main/java/app/passwordstore/util/git/operation/PullOperation.kt4
-rw-r--r--app/src/main/java/app/passwordstore/util/git/operation/PushOperation.kt4
-rw-r--r--app/src/main/java/app/passwordstore/util/git/operation/ResetToRemoteOperation.kt4
-rw-r--r--app/src/main/java/app/passwordstore/util/git/operation/SyncOperation.kt4
-rw-r--r--app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt21
-rw-r--r--app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt32
-rw-r--r--app/src/main/java/app/passwordstore/util/log/LogcatLogger.kt9
-rw-r--r--app/src/main/java/app/passwordstore/util/services/ClipboardService.kt7
-rw-r--r--app/src/main/java/app/passwordstore/util/services/OreoAutofillService.kt17
-rw-r--r--app/src/main/java/app/passwordstore/util/settings/Constants.kt2
-rw-r--r--app/src/main/java/app/passwordstore/util/settings/GitSettings.kt6
-rw-r--r--app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt1
-rw-r--r--app/src/main/res/drawable-nodpi/ic_kofi_symbol.webpbin0 -> 4100 bytes
-rw-r--r--app/src/main/res/drawable/ic_action_new_folder.xml2
-rw-r--r--app/src/main/res/drawable/ic_action_new_password.xml2
-rw-r--r--app/src/main/res/drawable/ic_action_secure_24dp.xml2
-rw-r--r--app/src/main/res/drawable/ic_delete_24dp.xml2
-rw-r--r--app/src/main/res/drawable/ic_edit_24dp.xml2
-rw-r--r--app/src/main/res/drawable/ic_launcher_background.xml2
-rw-r--r--app/src/main/res/drawable/ic_launcher_foreground.xml61
-rw-r--r--app/src/main/res/drawable/ic_launcher_foreground_monochrome.xml20
-rw-r--r--app/src/main/res/drawable/ic_move_folder_24dp.xml2
-rw-r--r--app/src/main/res/drawable/ic_open_in_new_24dp.xml12
-rw-r--r--app/src/main/res/drawable/ic_push_pin_24dp.xml2
-rw-r--r--app/src/main/res/drawable/ic_round_import_export.xml2
-rw-r--r--app/src/main/res/drawable/ic_search_24dp.xml2
-rw-r--r--app/src/main/res/drawable/ic_share_24dp.xml2
-rw-r--r--app/src/main/res/layout/git_credential_layout.xml37
-rw-r--r--app/src/main/res/mipmap-hdpi/ic_launcher.pngbin4260 -> 0 bytes
-rw-r--r--app/src/main/res/mipmap-mdpi/ic_launcher.pngbin2670 -> 0 bytes
-rw-r--r--app/src/main/res/mipmap-xhdpi/ic_launcher.pngbin5999 -> 0 bytes
-rw-r--r--app/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin9402 -> 0 bytes
-rw-r--r--app/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin13343 -> 0 bytes
-rw-r--r--app/src/main/res/values-de/strings.xml2
-rw-r--r--app/src/main/res/values-fr/strings.xml2
-rw-r--r--app/src/main/res/values-gl/strings.xml2
-rw-r--r--app/src/main/res/values-it/strings.xml25
-rw-r--r--app/src/main/res/values-ko-rKR/strings.xml2
-rw-r--r--app/src/main/res/values-night/themes_material3.xml29
-rw-r--r--app/src/main/res/values-pl-rPL/strings.xml2
-rw-r--r--app/src/main/res/values-pt-rBR/strings.xml2
-rw-r--r--app/src/main/res/values-ru/strings.xml2
-rw-r--r--app/src/main/res/values-v29/arrays.xml17
-rw-r--r--app/src/main/res/values-v29/prefs.xml8
-rw-r--r--app/src/main/res/values-zh-rCN/strings.xml2
-rw-r--r--app/src/main/res/values/arrays.xml4
-rw-r--r--app/src/main/res/values/colors_material3.xml54
-rw-r--r--app/src/main/res/values/prefs.xml2
-rw-r--r--app/src/main/res/values/strings.xml25
-rw-r--r--app/src/main/res/values/themes_material3.xml42
-rw-r--r--app/src/test/java/app/passwordstore/ui/prompts/PromptsTest.kt102
-rw-r--r--app/src/test/java/app/passwordstore/ui/settings/AboutTest.kt39
-rw-r--r--app/src/test/java/app/passwordstore/util/settings/GitSettingsTest.kt17
-rw-r--r--app/src/test/java/app/passwordstore/util/viewmodel/StrictDomainRegexTest.kt2
106 files changed, 1132 insertions, 900 deletions
diff --git a/app/src/debug/ic_launcher-playstore.png b/app/src/debug/ic_launcher-playstore.png
deleted file mode 100644
index d93ce3cf..00000000
--- a/app/src/debug/ic_launcher-playstore.png
+++ /dev/null
Binary files differ
diff --git a/app/src/debug/res/drawable/ic_launcher_background.xml b/app/src/debug/res/drawable/ic_launcher_background.xml
index f64ed7e3..d6bd4668 100644
--- a/app/src/debug/res/drawable/ic_launcher_background.xml
+++ b/app/src/debug/res/drawable/ic_launcher_background.xml
@@ -9,6 +9,6 @@
android:viewportWidth="432"
android:viewportHeight="432">
<path
- android:fillColor="#5E0033"
+ android:fillColor="#4B2E83"
android:pathData="M0,0h432v432h-432z" />
</vector>
diff --git a/app/src/debug/res/drawable/ic_launcher_foreground.xml b/app/src/debug/res/drawable/ic_launcher_foreground.xml
deleted file mode 100644
index 2cdef53f..00000000
--- a/app/src/debug/res/drawable/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,63 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- ~ SPDX-License-Identifier: GPL-3.0-only
- -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:aapt="http://schemas.android.com/aapt"
- android:width="108dp"
- android:height="108dp"
- android:viewportWidth="432"
- android:viewportHeight="432">
- <path android:pathData="M128.18,195.79C126.46,181.89 137.12,169.6 150.91,169.6H281.09C294.91,169.6 305.59,181.95 303.8,195.88L292.58,283.72C291.09,295.32 281.37,304 269.86,304H161.8C150.26,304 140.51,295.27 139.07,283.63L128.18,195.79Z">
- <aapt:attr name="android:fillColor">
- <gradient
- android:endX="216"
- android:endY="304"
- android:startX="216"
- android:startY="169.6"
- android:type="linear">
- <item
- android:color="#FFFFA800"
- android:offset="0" />
- <item
- android:color="#FFFF6341"
- android:offset="1" />
- </gradient>
- </aapt:attr>
- </path>
- <path android:pathData="M245.33,169.6V160C245.33,144.09 232.2,131.2 216,131.2C199.8,131.2 186.67,144.09 186.67,160V169.6H167.11V160C167.11,133.49 189,112 216,112C243,112 264.89,133.49 264.89,160V169.6H245.33Z">
- <aapt:attr name="android:fillColor">
- <gradient
- android:centerX="216"
- android:centerY="169.6"
- android:gradientRadius="33.6"
- android:type="radial">
- <item
- android:color="#FFD1004B"
- android:offset="0.24" />
- <item
- android:color="#FFFFD749"
- android:offset="0.75" />
- </gradient>
- </aapt:attr>
- </path>
- <path
- android:fillColor="#FFD749"
- android:pathData="M245.33,169.6h19.56v38.4h-19.56z" />
- <path
- android:fillColor="#FFD749"
- android:pathData="M245.33,217.6h19.56v48h-19.56z" />
- <path
- android:fillColor="#FFD749"
- android:pathData="M239.11,205.19l13.83,13.58l-34.57,33.94l-13.83,-13.58z" />
- <path
- android:fillColor="#FFFFF0"
- android:pathData="M235.56,208a19.56,19.2 0,1 0,39.11 0a19.56,19.2 0,1 0,-39.11 0z" />
- <path
- android:fillColor="#FFFFF0"
- android:pathData="M235.56,275.2a19.56,19.2 0,1 0,39.11 0a19.56,19.2 0,1 0,-39.11 0z" />
- <path
- android:fillColor="#FFFFF0"
- android:pathData="M204.54,252.71m-13.83,-13.58a19.56,19.2 0,1 1,27.66 27.15a19.56,19.2 0,1 1,-27.66 -27.15" />
-</vector>
diff --git a/app/src/debug/res/mipmap-anydpi/ic_launcher.xml b/app/src/debug/res/mipmap-anydpi/ic_launcher.xml
deleted file mode 100644
index 22bd3428..00000000
--- a/app/src/debug/res/mipmap-anydpi/ic_launcher.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- ~ SPDX-License-Identifier: GPL-3.0-only
- -->
-
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
- <background android:drawable="@drawable/ic_launcher_background" />
- <foreground android:drawable="@drawable/ic_launcher_foreground" />
- <monochrome android:drawable="@drawable/ic_launcher_foreground_monochrome" />
-</adaptive-icon>
diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher.png b/app/src/debug/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index a3b28da5..00000000
--- a/app/src/debug/res/mipmap-hdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher.png b/app/src/debug/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index 0be8ba3c..00000000
--- a/app/src/debug/res/mipmap-mdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 02a4af49..00000000
--- a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 9d32c97c..00000000
--- a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index d7d4ef18..00000000
--- a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 15d6447e..0eabbab4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -30,14 +30,12 @@
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppThemeM3"
- tools:ignore="GoogleAppIndexingWarning"
- tools:targetApi="s">
+ tools:ignore="GoogleAppIndexingWarning">
<activity
android:name=".ui.main.LaunchActivity"
android:configChanges="orientation|screenSize"
- android:exported="true"
- android:theme="@style/NoActionBarThemeM3">
+ android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -49,7 +47,6 @@
android:name=".ui.main.MainActivity"
android:configChanges="orientation|screenSize"
android:exported="false"
- android:theme="@style/NoActionBarThemeM3"
android:windowSoftInputMode="adjustResize" />
<activity
@@ -101,7 +98,6 @@
android:name=".ui.autofill.AutofillSaveActivity"
android:configChanges="orientation|screenSize"
android:exported="false"
- android:theme="@style/NoActionBarThemeM3"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ui.autofill.AutofillPublisherChangedActivity"
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
deleted file mode 100644
index 8de23a46..00000000
--- a/app/src/main/ic_launcher-playstore.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/java/app/passwordstore/Application.kt b/app/src/main/java/app/passwordstore/Application.kt
index bda61265..dbeb4ffb 100644
--- a/app/src/main/java/app/passwordstore/Application.kt
+++ b/app/src/main/java/app/passwordstore/Application.kt
@@ -4,14 +4,13 @@
*/
package app.passwordstore
+import android.app.UiModeManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
-import android.os.Build
import android.os.StrictMode
-import androidx.appcompat.app.AppCompatDelegate.*
import app.passwordstore.data.crypto.PGPPassphraseCache
import app.passwordstore.injection.context.FilesDirPath
import app.passwordstore.injection.prefs.SettingsPreferences
@@ -24,7 +23,6 @@ import app.passwordstore.util.settings.GitSettings
import app.passwordstore.util.settings.PreferenceKeys
import app.passwordstore.util.settings.removeSecurityCryptoData
import app.passwordstore.util.settings.runMigrations
-import com.google.android.material.color.DynamicColors
import dagger.hilt.android.HiltAndroidApp
import java.util.concurrent.Executors
import javax.inject.Inject
@@ -65,7 +63,6 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
removeSecurityCryptoData(this, prefs)
runMigrations(filesDirPath, prefs, gitSettings)
proxyUtils.setDefaultProxy()
- DynamicColors.applyToActivitiesIfAvailable(this)
setupScreenOffHandler()
}
@@ -92,49 +89,35 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
}
private fun setVmPolicy() {
- val builder =
+ StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectActivityLeaks()
.detectFileUriExposure()
.detectLeakedClosableObjects()
.detectLeakedRegistrationObjects()
.detectLeakedSqlLiteObjects()
-
- builder.detectContentUriWithoutPermission()
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- builder.detectCredentialProtectedWhileLocked().detectImplicitDirectBoot()
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- builder.detectNonSdkApiUsage()
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- builder.detectIncorrectContextUse().detectUnsafeIntentLaunch()
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- builder.penaltyListener(Executors.newSingleThreadExecutor()) { violation ->
- logcat(VERBOSE) { violation.stackTraceToString() }
- }
- } else {
- builder.penaltyLog()
- }
-
- val policy = builder.build()
- StrictMode.setVmPolicy(policy)
+ .detectContentUriWithoutPermission()
+ .detectCredentialProtectedWhileLocked()
+ .detectImplicitDirectBoot()
+ .detectNonSdkApiUsage()
+ .detectIncorrectContextUse()
+ .detectUnsafeIntentLaunch()
+ .penaltyListener(Executors.newSingleThreadExecutor()) { violation ->
+ logcat(VERBOSE) { violation.stackTraceToString() }
+ }
+ .build()
+ )
}
private fun setNightMode() {
- setDefaultNightMode(
- when (prefs.getString(PreferenceKeys.APP_THEME) ?: getString(R.string.app_theme_def)) {
- "light" -> MODE_NIGHT_NO
- "dark" -> MODE_NIGHT_YES
- "follow_system" -> MODE_NIGHT_FOLLOW_SYSTEM
- else -> MODE_NIGHT_AUTO_BATTERY
- }
- )
+ getSystemService(UiModeManager::class.java)
+ .setApplicationNightMode(
+ when (prefs.getString(PreferenceKeys.APP_THEME) ?: getString(R.string.app_theme_def)) {
+ "light" -> UiModeManager.MODE_NIGHT_NO
+ "dark" -> UiModeManager.MODE_NIGHT_YES
+ else -> UiModeManager.MODE_NIGHT_AUTO
+ }
+ )
}
companion object {
diff --git a/app/src/main/java/app/passwordstore/injection/AutofillResponseBuilderModule.kt b/app/src/main/java/app/passwordstore/injection/AutofillResponseBuilderModule.kt
index 46e61397..1717e4cc 100644
--- a/app/src/main/java/app/passwordstore/injection/AutofillResponseBuilderModule.kt
+++ b/app/src/main/java/app/passwordstore/injection/AutofillResponseBuilderModule.kt
@@ -5,8 +5,6 @@
package app.passwordstore.injection
-import android.os.Build
-import app.passwordstore.util.autofill.Api26AutofillResponseBuilder
import app.passwordstore.util.autofill.Api30AutofillResponseBuilder
import app.passwordstore.util.autofill.AutofillResponseBuilder
import dagger.Module
@@ -21,11 +19,6 @@ object AutofillResponseBuilderModule {
@Provides
@Reusable
- fun provideAutofillResponseBuilder(): AutofillResponseBuilder.Factory {
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- Api30AutofillResponseBuilder.Factory
- } else {
- Api26AutofillResponseBuilder.Factory
- }
- }
+ fun provideAutofillResponseBuilder(): AutofillResponseBuilder.Factory =
+ Api30AutofillResponseBuilder.Factory
}
diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt
index 7ba4df74..3b62de4c 100644
--- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt
@@ -9,14 +9,12 @@ import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.content.SharedPreferences
-import android.os.Build
import android.os.Bundle
import android.view.WindowManager
import android.view.autofill.AutofillManager
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
-import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -25,6 +23,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.core.content.edit
+import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import app.passwordstore.Application.Companion.screenWasOff
import app.passwordstore.R
@@ -71,7 +70,7 @@ import logcat.logcat
* FLAG_SECURE window shows only the "no keys" and passphrase dialogs.
*/
@AndroidEntryPoint
-class AutofillDecryptActivity : AppCompatActivity() {
+class AutofillDecryptActivity : FragmentActivity() {
@SettingsPreferences @Inject lateinit var settings: SharedPreferences
@Inject lateinit var repository: CryptoRepository
@@ -368,11 +367,7 @@ class AutofillDecryptActivity : AppCompatActivity() {
context,
decryptFileRequestCode++,
intent,
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
- } else {
- PendingIntent.FLAG_CANCEL_CURRENT
- },
+ PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE,
)
.intentSender
}
diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt
index c607b81f..474005b5 100644
--- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt
+++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt
@@ -8,19 +8,18 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentSender
-import android.os.Build
import android.os.Bundle
import android.view.autofill.AutofillManager
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.activity.viewModels
-import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
+import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.passwordstore.R
import app.passwordstore.data.repo.PasswordRepository
@@ -44,7 +43,7 @@ import logcat.LogPriority.ERROR
import logcat.logcat
@AndroidEntryPoint
-class AutofillFilterView : AppCompatActivity() {
+class AutofillFilterView : FragmentActivity() {
@Inject lateinit var dispatcherProvider: DispatcherProvider
@@ -74,11 +73,7 @@ class AutofillFilterView : AppCompatActivity() {
context,
matchAndDecryptFileRequestCode++,
intent,
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
- } else {
- PendingIntent.FLAG_CANCEL_CURRENT
- },
+ PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE,
)
.intentSender
}
@@ -133,7 +128,6 @@ class AutofillFilterView : AppCompatActivity() {
return
}
}
- supportActionBar?.hide()
setResult(RESULT_CANCELED)
val isWeb = formOrigin is FormOrigin.Web
diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillPublisherChangedActivity.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillPublisherChangedActivity.kt
index 803896a5..74637ca1 100644
--- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillPublisherChangedActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillPublisherChangedActivity.kt
@@ -9,14 +9,13 @@ import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.content.pm.PackageManager
-import android.os.Build
import android.os.Bundle
import android.service.autofill.FillResponse
import android.text.format.DateUtils
import android.view.autofill.AutofillManager
import androidx.activity.compose.setContent
-import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.IntentCompat
+import androidx.fragment.app.FragmentActivity
import app.passwordstore.R
import app.passwordstore.ui.compose.theme.APSTheme
import app.passwordstore.util.autofill.AutofillMatcher
@@ -31,7 +30,7 @@ import com.github.michaelbull.result.runCatching
import logcat.LogPriority.ERROR
import logcat.logcat
-class AutofillPublisherChangedActivity : AppCompatActivity() {
+class AutofillPublisherChangedActivity : FragmentActivity() {
companion object {
@@ -54,11 +53,7 @@ class AutofillPublisherChangedActivity : AppCompatActivity() {
context,
publisherChangedRequestCode++,
intent,
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
- } else {
- PendingIntent.FLAG_CANCEL_CURRENT
- },
+ PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE,
)
.intentSender
}
@@ -79,7 +74,6 @@ class AutofillPublisherChangedActivity : AppCompatActivity() {
finish()
return
}
- supportActionBar?.hide()
val (packageInfo, appInfo, hash) =
runCatching {
Triple(
diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt
index eb533411..2ed0adef 100644
--- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt
@@ -12,14 +12,15 @@ import android.os.Bundle
import android.view.WindowManager
import android.view.autofill.AutofillManager
import androidx.activity.compose.setContent
-import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
+import androidx.fragment.app.FragmentActivity
import app.passwordstore.data.passfile.PasswordEntry
import app.passwordstore.ui.compose.theme.APSTheme
import app.passwordstore.ui.editor.EditorDestination
import app.passwordstore.ui.editor.EditorForm
import app.passwordstore.ui.editor.SaveResult
import app.passwordstore.ui.editor.passContent
+import app.passwordstore.ui.prompts.PromptHost
import app.passwordstore.util.autofill.AutofillMatcher
import app.passwordstore.util.autofill.AutofillPreferences
import app.passwordstore.util.autofill.AutofillResponseBuilder
@@ -38,7 +39,7 @@ import logcat.logcat
* password, fills it in.
*/
@AndroidEntryPoint
-class AutofillSaveActivity : AppCompatActivity() {
+class AutofillSaveActivity : FragmentActivity() {
companion object {
@@ -139,6 +140,7 @@ class AutofillSaveActivity : AppCompatActivity() {
onSaved = ::finishSaved,
generate = generate,
)
+ PromptHost(this@AutofillSaveActivity)
}
}
}
diff --git a/app/src/main/java/app/passwordstore/ui/crypto/CryptoExtensions.kt b/app/src/main/java/app/passwordstore/ui/crypto/CryptoExtensions.kt
index 67343a2e..626ec130 100644
--- a/app/src/main/java/app/passwordstore/ui/crypto/CryptoExtensions.kt
+++ b/app/src/main/java/app/passwordstore/ui/crypto/CryptoExtensions.kt
@@ -7,9 +7,7 @@ package app.passwordstore.ui.crypto
import android.content.ClipData
import android.content.Intent
-import android.os.Build
import android.os.PersistableBundle
-import androidx.annotation.StringRes
import androidx.fragment.app.FragmentActivity
import app.passwordstore.R
import app.passwordstore.crypto.PGPIdentifier
@@ -17,7 +15,6 @@ import app.passwordstore.data.repo.PasswordRepository
import app.passwordstore.util.extensions.clipboard
import app.passwordstore.util.extensions.getString
import app.passwordstore.util.extensions.sharedPrefs
-import app.passwordstore.util.extensions.snackbar
import app.passwordstore.util.services.ClipboardService
import app.passwordstore.util.settings.Constants
import app.passwordstore.util.settings.PreferenceKeys
@@ -51,23 +48,15 @@ fun getLongName(fullPath: String, repositoryPath: String, basename: String): Str
}
}
-/**
- * Copies provided [text] to the clipboard, flagged sensitive. Below Android 11 it also shows a
- * snackbar, which can be disabled by passing [showSnackbar] as false.
- */
-fun FragmentActivity.copyTextToClipboard(
- text: String?,
- showSnackbar: Boolean = true,
- @StringRes snackbarTextRes: Int = R.string.clipboard_copied_text,
-) {
+/** Copies provided [text] to the clipboard; [sensitive] text is hidden from its preview. */
+fun FragmentActivity.copyTextToClipboard(text: String?, sensitive: Boolean = true) {
val clipboard = clipboard ?: return
val clip = ClipData.newPlainText("pgp_handler_result_pm", text)
- clip.description.extras =
- PersistableBundle().apply { putBoolean("android.content.extra.IS_SENSITIVE", true) }
- clipboard.setPrimaryClip(clip)
- if (showSnackbar && Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
- snackbar(message = resources.getString(snackbarTextRes))
+ if (sensitive) {
+ clip.description.extras =
+ PersistableBundle().apply { putBoolean("android.content.extra.IS_SENSITIVE", true) }
}
+ clipboard.setPrimaryClip(clip)
}
/**
diff --git a/app/src/main/java/app/passwordstore/ui/editor/EditorDestination.kt b/app/src/main/java/app/passwordstore/ui/editor/EditorDestination.kt
index b5a961d3..617d804a 100644
--- a/app/src/main/java/app/passwordstore/ui/editor/EditorDestination.kt
+++ b/app/src/main/java/app/passwordstore/ui/editor/EditorDestination.kt
@@ -13,6 +13,7 @@ import androidx.activity.result.contract.ActivityResultContracts.GetContent
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -34,14 +35,15 @@ import app.passwordstore.R
import app.passwordstore.data.repo.PasswordRepository
import app.passwordstore.ui.crypto.copyPasswordToClipboard
import app.passwordstore.ui.crypto.getPGPIdentifiers
+import app.passwordstore.ui.git.base.BaseGitActivity
import app.passwordstore.ui.list.FolderPicker
+import app.passwordstore.ui.list.commitAndPush
import app.passwordstore.util.extensions.commitChange
import app.passwordstore.util.extensions.getString
import app.passwordstore.util.extensions.isInsideRepository
import app.passwordstore.util.extensions.sharedPrefs
import app.passwordstore.util.extensions.snackbar
import app.passwordstore.util.settings.PreferenceKeys
-import com.google.android.material.snackbar.Snackbar
import com.google.zxing.integration.android.IntentIntegrator
import java.io.File
import kotlinx.coroutines.launch
@@ -142,18 +144,21 @@ fun EditorDestination(
R.string.password_creation_file_delete_fail_message,
it.nameWithoutExtension,
),
- length = Snackbar.LENGTH_LONG,
+ length = SnackbarDuration.Long,
)
}
}
// ponytail: commit failures are not shown, as for the list's move and delete. A commit cut
// short by recreation runs again; one that already went through finds nothing to commit.
- activity.commitChange(
+ val message =
activity.getString(
if (original != null) R.string.git_commit_edit_text else R.string.git_commit_add_text,
saved.file.relativeTo(root).path.removeSuffix(".gpg"),
)
- )
+ // ponytail: AutofillSaveActivity closes right after saving, which would cancel a push, so its
+ // saves stay local until the next push or sync.
+ if (activity is BaseGitActivity) activity.commitAndPush(message)
+ else activity.commitChange(message)
keyboard?.hide()
focusManager.clearFocus()
viewModel.left()
diff --git a/app/src/main/java/app/passwordstore/ui/editor/EditorViewModel.kt b/app/src/main/java/app/passwordstore/ui/editor/EditorViewModel.kt
index 6d2fbaec..fe5ee772 100644
--- a/app/src/main/java/app/passwordstore/ui/editor/EditorViewModel.kt
+++ b/app/src/main/java/app/passwordstore/ui/editor/EditorViewModel.kt
@@ -10,8 +10,6 @@ import android.content.SharedPreferences
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.net.Uri
-import android.os.Build
-import android.provider.MediaStore
import androidx.annotation.StringRes
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -168,12 +166,8 @@ constructor(
return withContext(dispatcherProvider.io()) {
runCatching {
val bitmap =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- ImageDecoder.decodeBitmap(ImageDecoder.createSource(resolver, uri))
- .copy(Bitmap.Config.ARGB_8888, false)
- } else {
- @Suppress("DEPRECATION") MediaStore.Images.Media.getBitmap(resolver, uri)
- }
+ ImageDecoder.decodeBitmap(ImageDecoder.createSource(resolver, uri))
+ .copy(Bitmap.Config.ARGB_8888, false)
val pixels = IntArray(bitmap.width * bitmap.height)
bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
val source = RGBLuminanceSource(bitmap.width, bitmap.height, pixels)
diff --git a/app/src/main/java/app/passwordstore/ui/git/base/BaseGitActivity.kt b/app/src/main/java/app/passwordstore/ui/git/base/BaseGitActivity.kt
index 32872761..f7181c4e 100644
--- a/app/src/main/java/app/passwordstore/ui/git/base/BaseGitActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/git/base/BaseGitActivity.kt
@@ -5,10 +5,11 @@
package app.passwordstore.ui.git.base
import android.content.SharedPreferences
-import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
+import androidx.fragment.app.FragmentActivity
import app.passwordstore.R
import app.passwordstore.injection.prefs.GitPreferences
+import app.passwordstore.ui.prompts.prompts
import app.passwordstore.util.coroutines.DispatcherProvider
import app.passwordstore.util.extensions.sharedPrefs
import app.passwordstore.util.git.ErrorMessages
@@ -25,10 +26,8 @@ import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.andThen
import com.github.michaelbull.result.mapError
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
-import kotlinx.coroutines.withContext
import logcat.asLog
import logcat.logcat
import net.schmizz.sshj.common.DisconnectReason
@@ -37,11 +36,11 @@ import net.schmizz.sshj.transport.TransportException
import net.schmizz.sshj.userauth.UserAuthException
/**
- * Abstract [AppCompatActivity] that holds some information that is commonly shared across
+ * Abstract [FragmentActivity] that holds some information that is commonly shared across
* git-related tasks and makes sense to be held here.
*/
@AndroidEntryPoint
-abstract class BaseGitActivity : AppCompatActivity() {
+abstract class BaseGitActivity : FragmentActivity() {
/** Enum of possible Git operations than can be run through [launchGitOperation]. */
enum class GitOp {
@@ -100,23 +99,17 @@ abstract class BaseGitActivity : AppCompatActivity() {
finish()
}
- suspend fun promptOnErrorHandler(err: Throwable, onPromptDone: () -> Unit = {}) {
+ suspend fun promptOnErrorHandler(err: Throwable) {
val error = rootCauseException(err)
if (!isExplicitlyUserInitiatedError(error)) {
gitPrefs.edit { remove(PreferenceKeys.HTTPS_PASSWORD) }
sharedPrefs.edit { remove(PreferenceKeys.SSH_OPENKEYSTORE_KEYID) }
logcat { error.asLog() }
- withContext(dispatcherProvider.main()) {
- MaterialAlertDialogBuilder(this@BaseGitActivity).run {
- setTitle(resources.getString(R.string.jgit_error_dialog_title))
- setMessage(ErrorMessages[error])
- setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> }
- setOnDismissListener { onPromptDone() }
- show()
- }
- }
- } else {
- onPromptDone()
+ prompts.ask(
+ title = getString(R.string.jgit_error_dialog_title),
+ message = ErrorMessages[error],
+ confirm = getString(R.string.dialog_ok),
+ )
}
}
diff --git a/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt b/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt
index 54a7e66c..97092716 100644
--- a/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt
+++ b/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt
@@ -73,6 +73,7 @@ fun ListDestination(
onNewEntry: () -> Unit,
onClone: () -> Unit,
onSettings: () -> Unit,
+ onAbout: () -> Unit,
onNoStore: () -> Unit,
modifier: Modifier = Modifier,
viewModel: SearchableRepositoryViewModel = hiltViewModel(),
@@ -159,6 +160,7 @@ fun ListDestination(
},
onGitOp = { op -> ifInitialized { runGit(op) } },
onSettings = onSettings,
+ onAbout = onAbout,
onNew = { dialog = ListDialog.Create },
onMove = { dialog = ListDialog.Move },
onDelete = {
diff --git a/app/src/main/java/app/passwordstore/ui/list/ListScreen.kt b/app/src/main/java/app/passwordstore/ui/list/ListScreen.kt
index 1b359d5b..593c744b 100644
--- a/app/src/main/java/app/passwordstore/ui/list/ListScreen.kt
+++ b/app/src/main/java/app/passwordstore/ui/list/ListScreen.kt
@@ -91,6 +91,7 @@ fun ListScreen(
onSync: () -> Unit,
onGitOp: (GitOp) -> Unit,
onSettings: () -> Unit,
+ onAbout: () -> Unit,
onNew: () -> Unit,
onMove: () -> Unit,
onDelete: () -> Unit,
@@ -133,7 +134,7 @@ fun ListScreen(
SelectionBar(rows, selected, onClearSelection, onMove, onDelete, onRename, onPin)
} else {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
- SearchField(query, gitOps, onQueryChange, onGitOp, onSettings, searchFocus)
+ SearchField(query, gitOps, onQueryChange, onGitOp, onSettings, onAbout, searchFocus)
BreadcrumbBar(
crumbsFor(path),
stringResource(R.string.list_store_root),
@@ -238,6 +239,7 @@ private fun SearchField(
onQueryChange: (String) -> Unit,
onGitOp: (GitOp) -> Unit,
onSettings: () -> Unit,
+ onAbout: () -> Unit,
focus: FocusRequester,
) {
val focusManager = LocalFocusManager.current
@@ -282,6 +284,13 @@ private fun SearchField(
onSettings()
},
)
+ DropdownMenuItem(
+ text = { Text(stringResource(R.string.about)) },
+ onClick = {
+ menu = false
+ onAbout()
+ },
+ )
}
}
}
diff --git a/app/src/main/java/app/passwordstore/ui/list/StoreActions.kt b/app/src/main/java/app/passwordstore/ui/list/StoreActions.kt
index 3bdede73..f300a814 100644
--- a/app/src/main/java/app/passwordstore/ui/list/StoreActions.kt
+++ b/app/src/main/java/app/passwordstore/ui/list/StoreActions.kt
@@ -6,11 +6,15 @@
package app.passwordstore.ui.list
import android.content.Context
+import androidx.compose.material3.SnackbarDuration
import androidx.core.content.edit
+import androidx.lifecycle.lifecycleScope
import app.passwordstore.R
import app.passwordstore.data.repo.PasswordRepository
import app.passwordstore.ui.crypto.getLongName
import app.passwordstore.ui.git.base.BaseGitActivity
+import app.passwordstore.ui.prompts.Choice
+import app.passwordstore.ui.prompts.prompts
import app.passwordstore.util.autofill.AutofillMatcher
import app.passwordstore.util.extensions.base64
import app.passwordstore.util.extensions.commitChange
@@ -18,15 +22,17 @@ import app.passwordstore.util.extensions.getString
import app.passwordstore.util.extensions.isInsideRepository
import app.passwordstore.util.extensions.listFilesRecursively
import app.passwordstore.util.extensions.sharedPrefs
+import app.passwordstore.util.extensions.snackbar
import app.passwordstore.util.settings.PasswordSortOrder
import app.passwordstore.util.settings.PreferenceKeys
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.github.michaelbull.result.onFailure
+import com.github.michaelbull.result.onSuccess
import java.io.File
import java.nio.file.Files
-import kotlin.coroutines.resume
-import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import logcat.LogPriority.ERROR
+import logcat.asLog
import logcat.logcat
/** Shared preferences file of the "recently used" sort order, keyed by base64 of the path. */
@@ -65,6 +71,27 @@ internal fun File.deleteWithoutFollowingLinks(): Boolean {
return delete()
}
+/**
+ * Commits the store and, when the user asked for it, pushes without holding up the caller. A failed
+ * push only says so: the commit stays local, and [BaseGitActivity.promptOnErrorHandler] would
+ * delete the saved credentials over what may just be a missing network.
+ *
+ * ponytail: a push cut off by recreation is not resumed; the next one sends everything. An SSH key
+ * behind biometrics prompts on each change.
+ */
+suspend fun BaseGitActivity.commitAndPush(message: String) {
+ commitChange(message).onSuccess {
+ if (gitSettings.pushAfterCommit && gitSettings.url != null) {
+ lifecycleScope.launch {
+ launchGitOperation(BaseGitActivity.GitOp.PUSH).onFailure {
+ logcat(ERROR) { it.asLog() }
+ snackbar(getString(R.string.git_push_after_commit_failed), SnackbarDuration.Long)
+ }
+ }
+ }
+ }
+}
+
/** Deletes [files] (entries or whole folders), drops their autofill matches and commits. */
suspend fun BaseGitActivity.deleteItems(files: List<File>) {
val root = PasswordRepository.getRepositoryDirectory()
@@ -73,7 +100,7 @@ suspend fun BaseGitActivity.deleteItems(files: List<File>) {
files.forEach { it.deleteWithoutFollowingLinks() }
AutofillMatcher.updateMatches(applicationContext, delete = deleted)
}
- commitChange(
+ commitAndPush(
getString(
R.string.git_commit_remove_text,
files.joinToString(", ") { it.toRelativeString(root) },
@@ -117,14 +144,14 @@ suspend fun BaseGitActivity.moveItems(files: List<File>, target: File) {
val relative = "${target.absolutePath}/".replace(repo, "").replace("/+".toRegex(), "/")
getString(R.string.git_commit_move_multiple_text, relative)
}
- commitChange(message)
+ commitAndPush(message)
}
/** Renames the folder [from] to [to], keeps its recently-used time, then commits. */
suspend fun BaseGitActivity.renameFolder(from: File, to: File) {
if (!moveFile(from, to)) return
moveHistory(from, to)
- commitChange(getString(R.string.git_commit_move_text, from.name, to.name))
+ commitAndPush(getString(R.string.git_commit_move_text, from.name, to.name))
}
/** Carries the "recently used" time of [from] over to [to]. */
@@ -152,28 +179,21 @@ private suspend fun BaseGitActivity.moveFile(source: File, destination: File): B
}
}
if (!moved) {
- logcat(ERROR) { "Something went wrong while moving $source to $destination." }
- // ponytail: View dialog like BaseGitActivity's git prompts; both move to Compose together.
- MaterialAlertDialogBuilder(this)
- .setTitle(R.string.password_move_error_title)
- .setMessage(getString(R.string.password_move_error_message, source, destination))
- .setPositiveButton(android.R.string.ok, null)
- .show()
+ logcat(ERROR) { "Something went wrong while moving a file." }
+ prompts.ask(
+ title = getString(R.string.password_move_error_title),
+ message = getString(R.string.password_move_error_message, source, destination),
+ confirm = getString(R.string.dialog_ok),
+ )
}
return moved
}
/** Shows [message] under [title] and suspends until answered; true when confirmed. */
-// ponytail: View dialog, see moveFile.
-private suspend fun Context.confirm(title: String, message: String): Boolean =
- suspendCancellableCoroutine { cont ->
- val dialog =
- MaterialAlertDialogBuilder(this)
- .setTitle(title)
- .setMessage(message)
- .setPositiveButton(R.string.dialog_ok) { _, _ -> cont.resume(true) }
- .setNegativeButton(R.string.dialog_cancel) { _, _ -> cont.resume(false) }
- .setOnCancelListener { cont.resume(false) }
- .show()
- cont.invokeOnCancellation { dialog.dismiss() }
- }
+private suspend fun BaseGitActivity.confirm(title: String, message: String): Boolean =
+ prompts.ask(
+ title = title,
+ message = message,
+ confirm = getString(R.string.dialog_ok),
+ cancel = getString(R.string.dialog_cancel),
+ ) == Choice.Confirm
diff --git a/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt b/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt
index c0af5742..54aaf9de 100644
--- a/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt
@@ -9,8 +9,8 @@ import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
-import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
+import androidx.fragment.app.FragmentActivity
import app.passwordstore.data.repo.PasswordRepository
import app.passwordstore.util.auth.BiometricAuthenticator
import app.passwordstore.util.auth.BiometricAuthenticator.Result
@@ -19,7 +19,7 @@ import app.passwordstore.util.settings.PreferenceKeys
import java.io.File
@SuppressLint("CustomSplashScreen")
-class LaunchActivity : AppCompatActivity() {
+class LaunchActivity : FragmentActivity() {
private var prompted = false
diff --git a/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt b/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt
index 462bdb18..5037e646 100644
--- a/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt
@@ -29,6 +29,8 @@ import app.passwordstore.ui.entry.EntryViewModel
import app.passwordstore.ui.git.base.BaseGitActivity
import app.passwordstore.ui.list.ListDestination
import app.passwordstore.ui.list.folderMoves
+import app.passwordstore.ui.prompts.PromptHost
+import app.passwordstore.ui.settings.AboutRoute
import app.passwordstore.ui.settings.SettingsRoute
import app.passwordstore.ui.settings.settingsGraph
import app.passwordstore.ui.setup.GitServerRoute
@@ -70,8 +72,8 @@ import kotlinx.serialization.Serializable
/**
* Single-activity host for the Compose UI. Not exported: it is only started by [LaunchActivity],
* after its biometric gate and path check, and by the app itself through [setupIntent]. Extends
- * [BaseGitActivity] (an AppCompatActivity, so also the FragmentActivity BiometricPrompt needs)
- * because the git operations require one; it hosts no fragments.
+ * [BaseGitActivity] (a FragmentActivity, which BiometricPrompt needs) because the git operations
+ * require one; it hosts no fragments.
*/
@AndroidEntryPoint
class MainActivity : BaseGitActivity() {
@@ -129,6 +131,7 @@ class MainActivity : BaseGitActivity() {
onSettings = {
if (nav.currentBackStackEntry?.id == entry.id) nav.navigate(SettingsRoute)
},
+ onAbout = { if (nav.currentBackStackEntry?.id == entry.id) nav.navigate(AboutRoute) },
onNoStore = {
// The store is gone (deleted from Settings): setup starts over.
if (nav.currentBackStackEntry?.id == entry.id)
@@ -199,6 +202,7 @@ class MainActivity : BaseGitActivity() {
setupGraph(this@MainActivity, nav)
settingsGraph(this@MainActivity, nav)
}
+ PromptHost(this@MainActivity)
}
}
}
@@ -263,7 +267,7 @@ class MainActivity : BaseGitActivity() {
*/
// ponytail: a second instance instead of in-graph navigation. Autofill's activities stay
// outside the graph for good (the autofill service starts them); the git operations' "No SSH
- // key" dialog could navigate in-graph once it becomes a Compose dialog.
+ // key" dialog could navigate in-graph if git operations could reach the nav controller.
fun setupIntent(context: Context, start: SetupStart): Intent =
Intent(context, MainActivity::class.java).putExtra(EXTRA_SETUP, start.name)
}
diff --git a/app/src/main/java/app/passwordstore/ui/prompts/Prompts.kt b/app/src/main/java/app/passwordstore/ui/prompts/Prompts.kt
new file mode 100644
index 00000000..1af3f01c
--- /dev/null
+++ b/app/src/main/java/app/passwordstore/ui/prompts/Prompts.kt
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package app.passwordstore.ui.prompts
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.selection.toggleable
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.window.DialogProperties
+import androidx.compose.ui.window.SecureFlagPolicy
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import app.passwordstore.R
+import app.passwordstore.ui.compose.R as composeR
+import kotlin.coroutines.resume
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/** The button that answered a [Question]. Dismissing the dialog answers [Cancel]. */
+enum class Choice {
+ Confirm,
+ Other,
+ Cancel,
+}
+
+/** What the user entered in a [CredentialRequest]. */
+class CredentialAnswer(val text: String, val remember: Boolean)
+
+/** A dialog waiting for its answer; [cancelled] is the answer to dismissing it. */
+sealed class Prompt<T>(private val cancelled: T, private val deliver: (T) -> Unit) {
+ internal fun answer(value: T) = deliver(value)
+
+ internal fun cancel() = deliver(cancelled)
+}
+
+class Question(
+ val title: String,
+ val message: String,
+ val confirm: String,
+ val cancel: String?,
+ val other: String?,
+ deliver: (Choice) -> Unit,
+) : Prompt<Choice>(Choice.Cancel, deliver)
+
+class CredentialRequest(
+ val message: String,
+ val hint: String,
+ val remember: String,
+ val error: String?,
+ deliver: (CredentialAnswer?) -> Unit,
+) : Prompt<CredentialAnswer?>(null, deliver)
+
+/** A snackbar; each one is a distinct object, so showing the same text again shows it again. */
+class Snack(val message: String, val length: SnackbarDuration)
+
+/**
+ * The dialogs and the snackbar of one activity, for code with no composable of its own (git
+ * operations and store actions), drawn by [PromptHost]. Reach it through [prompts].
+ */
+class Prompts : ViewModel() {
+
+ private val queue = mutableStateListOf<Prompt<*>>()
+
+ /** Set once the activity is gone for good: nothing draws a new dialog any more. */
+ private var closed = false
+
+ /** The dialog on screen. The others wait for it to be answered. */
+ val current: Prompt<*>?
+ get() = queue.firstOrNull()
+
+ var snack by mutableStateOf<Snack?>(null)
+ private set
+
+ /** Shows a dialog and suspends until it is answered. Cancelling the caller closes it. */
+ suspend fun ask(
+ title: String,
+ message: String,
+ confirm: String,
+ cancel: String? = null,
+ other: String? = null,
+ ): Choice = suspendCancellableCoroutine { cont ->
+ val question = Question(title, message, confirm, cancel, other) { cont.resume(it) }
+ queue += question
+ cont.invokeOnCancellation { queue.remove(question) }
+ }
+
+ /**
+ * Asks for a credential. Not suspending: the SSH thread waits on [deliver], and no cancellation
+ * reaches it, so this dialog outlives a recreated activity and is drawn again by the new one.
+ */
+ fun askCredential(
+ message: String,
+ hint: String,
+ remember: String,
+ error: String?,
+ deliver: (CredentialAnswer?) -> Unit,
+ ) {
+ if (closed) {
+ deliver(null)
+ return
+ }
+ queue += CredentialRequest(message, hint, remember, error, deliver)
+ }
+
+ /** Answers [prompt], once: a double tap must not resume its caller twice. */
+ fun <T> answer(prompt: Prompt<T>, value: T) {
+ if (queue.remove(prompt)) prompt.answer(value)
+ }
+
+ fun cancel(prompt: Prompt<*>) {
+ if (queue.remove(prompt)) prompt.cancel()
+ }
+
+ fun cancelAll() {
+ queue.toList().forEach(::cancel)
+ }
+
+ /** Shows [message] in place of any current snackbar, as the View snackbar did. */
+ fun snackbar(message: String, length: SnackbarDuration = SnackbarDuration.Short): Snack =
+ Snack(message, length).also { snack = it }
+
+ /** Hides [shown], unless another snackbar replaced it. */
+ fun dismiss(shown: Snack) {
+ if (snack === shown) snack = null
+ }
+
+ internal fun close() {
+ closed = true
+ cancelAll()
+ }
+
+ override fun onCleared() = close()
+}
+
+/** The [Prompts] of this activity; it survives recreation. */
+val FragmentActivity.prompts: Prompts
+ get() = ViewModelProvider(this)[Prompts::class.java]
+
+/** Draws the [Prompts] of [activity]. Place it after the activity's content, inside `APSTheme`. */
+@Composable
+fun PromptHost(activity: FragmentActivity, modifier: Modifier = Modifier) {
+ val prompts = remember(activity) { activity.prompts }
+ val snackbars = remember { SnackbarHostState() }
+ val snack = prompts.snack
+ // A new snack cancels this effect, which takes the old one off the screen.
+ LaunchedEffect(snack) {
+ if (snack != null) {
+ snackbars.showSnackbar(snack.message, duration = snack.length)
+ prompts.dismiss(snack)
+ }
+ }
+ Box(modifier.fillMaxSize().navigationBarsPadding().imePadding(), Alignment.BottomCenter) {
+ SnackbarHost(snackbars)
+ }
+ val prompt = prompts.current
+ // Keyed: a retry is a new request and starts with an empty field.
+ key(prompt) {
+ when (prompt) {
+ is Question -> QuestionDialog(prompt, prompts)
+ is CredentialRequest -> CredentialDialog(prompt, prompts)
+ null -> {}
+ }
+ }
+}
+
+@Composable
+private fun QuestionDialog(question: Question, prompts: Prompts) {
+ AlertDialog(
+ onDismissRequest = { prompts.cancel(question) },
+ title = { Text(question.title) },
+ text = { Text(question.message) },
+ confirmButton = {
+ TextButton(onClick = { prompts.answer(question, Choice.Confirm) }) { Text(question.confirm) }
+ },
+ dismissButton = {
+ // No Row: the dialog's button area wraps its children when they do not fit.
+ question.cancel?.let {
+ TextButton(onClick = { prompts.answer(question, Choice.Cancel) }) { Text(it) }
+ }
+ question.other?.let {
+ TextButton(onClick = { prompts.answer(question, Choice.Other) }) { Text(it) }
+ }
+ },
+ )
+}
+
+/** The credential lives only in `remember` and the dialog's window is FLAG_SECURE. */
+@Composable
+private fun CredentialDialog(request: CredentialRequest, prompts: Prompts) {
+ // remember, never rememberSaveable: the credential must not reach saved state.
+ var text by remember { mutableStateOf("") }
+ var keep by remember { mutableStateOf(true) }
+ var revealed by remember { mutableStateOf(false) }
+ var error by remember { mutableStateOf(request.error) }
+ val focus = remember { FocusRequester() }
+ fun submit() = prompts.answer(request, CredentialAnswer(text, keep))
+ AlertDialog(
+ onDismissRequest = { prompts.cancel(request) },
+ properties = DialogProperties(securePolicy = SecureFlagPolicy.SecureOn),
+ title = { Text(stringResource(R.string.passphrase_dialog_title)) },
+ text = {
+ Column {
+ Text(request.message)
+ OutlinedTextField(
+ value = text,
+ onValueChange = {
+ text = it
+ error = null
+ },
+ modifier = Modifier.focusRequester(focus),
+ label = { Text(request.hint) },
+ supportingText = error?.let { { Text(it) } },
+ isError = error != null,
+ trailingIcon = {
+ IconButton(onClick = { revealed = !revealed }) {
+ Icon(
+ painterResource(
+ if (revealed) composeR.drawable.baseline_visibility_off_24
+ else composeR.drawable.baseline_visibility_24
+ ),
+ contentDescription =
+ stringResource(
+ if (revealed) R.string.entry_hide_password else R.string.entry_show_password
+ ),
+ )
+ }
+ },
+ visualTransformation =
+ if (revealed) VisualTransformation.None else PasswordVisualTransformation(),
+ singleLine = true,
+ keyboardOptions =
+ KeyboardOptions(
+ autoCorrectEnabled = false,
+ keyboardType = KeyboardType.Password,
+ imeAction = ImeAction.Done,
+ ),
+ keyboardActions = KeyboardActions(onDone = { submit() }),
+ )
+ Row(
+ Modifier.toggleable(value = keep, role = Role.Checkbox, onValueChange = { keep = it }),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Checkbox(checked = keep, onCheckedChange = null)
+ Text(request.remember)
+ }
+ }
+ LaunchedEffect(Unit) { focus.requestFocus() }
+ },
+ confirmButton = { TextButton(onClick = ::submit) { Text(stringResource(R.string.dialog_ok)) } },
+ dismissButton = {
+ TextButton(onClick = { prompts.cancel(request) }) {
+ Text(stringResource(R.string.dialog_cancel))
+ }
+ },
+ )
+}
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..4a366390
--- /dev/null
+++ b/app/src/main/java/app/passwordstore/ui/settings/AboutScreen.kt
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ * 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.Icon
+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, sensitive = false) },
+ 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) }
+ LinkRow(stringResource(R.string.about_docs)) { onOpen(DOCUMENTATION) }
+ LinkRow(stringResource(R.string.about_changelog)) { onOpen(changelog) }
+ LinkRow(stringResource(R.string.about_source)) { onOpen(REPOSITORY) }
+ LinkRow(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,
+ trailing: (@Composable () -> Unit)? = null,
+ onClick: () -> Unit,
+) {
+ ListItem(
+ headlineContent = { Text(title) },
+ supportingContent = summary?.let { { Text(it) } },
+ trailingContent = trailing,
+ modifier = Modifier.clickable(role = Role.Button, onClick = onClick),
+ colors = rowColors(),
+ )
+}
+
+/** A row that leaves for the browser, and says so. */
+@Composable
+private fun LinkRow(title: String, summary: String? = null, onClick: () -> Unit) {
+ AboutRow(
+ title = title,
+ summary = summary,
+ trailing = {
+ Icon(
+ painterResource(R.drawable.ic_open_in_new_24dp),
+ contentDescription = stringResource(R.string.about_opens_browser),
+ )
+ },
+ onClick = onClick,
+ )
+}
diff --git a/app/src/main/java/app/passwordstore/ui/settings/GitConfigScreen.kt b/app/src/main/java/app/passwordstore/ui/settings/GitConfigScreen.kt
index d008eea3..ed07c389 100644
--- a/app/src/main/java/app/passwordstore/ui/settings/GitConfigScreen.kt
+++ b/app/src/main/java/app/passwordstore/ui/settings/GitConfigScreen.kt
@@ -28,6 +28,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
@@ -54,7 +55,6 @@ import app.passwordstore.ui.main.MainActivity
import app.passwordstore.ui.setup.SetupDialog
import app.passwordstore.util.extensions.snackbar
import com.github.michaelbull.result.fold
-import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
/**
@@ -223,7 +223,7 @@ fun GitConfigDestination(
}
activity.snackbar(
message = activity.getString(R.string.git_tool_done, activity.getString(label)),
- length = Snackbar.LENGTH_LONG,
+ length = SnackbarDuration.Long,
)
viewModel.done = null
}
diff --git a/app/src/main/java/app/passwordstore/ui/settings/ProxyViewModel.kt b/app/src/main/java/app/passwordstore/ui/settings/ProxyViewModel.kt
index fb2bfe93..10f82178 100644
--- a/app/src/main/java/app/passwordstore/ui/settings/ProxyViewModel.kt
+++ b/app/src/main/java/app/passwordstore/ui/settings/ProxyViewModel.kt
@@ -6,7 +6,6 @@
package app.passwordstore.ui.settings
import android.net.InetAddresses
-import android.os.Build
import android.util.Patterns
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -19,10 +18,7 @@ import javax.inject.Inject
/** A numeric address or a plain host name: no scheme, port or path, which the port field holds. */
fun isProxyHost(text: String): Boolean {
- val numeric =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) InetAddresses.isNumericAddress(text)
- else @Suppress("DEPRECATION") Patterns.IP_ADDRESS.matcher(text).matches()
- return numeric ||
+ return InetAddresses.isNumericAddress(text) ||
(!text.contains(':') && !text.contains('/') && Patterns.DOMAIN_NAME.matcher(text).matches())
}
diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsDestination.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsDestination.kt
index c3809b36..05dc6b65 100644
--- a/app/src/main/java/app/passwordstore/ui/settings/SettingsDestination.kt
+++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsDestination.kt
@@ -38,6 +38,7 @@ import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleResumeEffect
+import androidx.lifecycle.lifecycleScope
import app.passwordstore.BuildConfig
import app.passwordstore.R
import app.passwordstore.ui.compose.theme.SpacingLarge
@@ -52,6 +53,7 @@ import app.passwordstore.util.services.PasswordExportService
import app.passwordstore.util.settings.PreferenceKeys
import com.github.androidpasswordstore.autofillparser.BrowserAutofillSupportLevel
import com.github.androidpasswordstore.autofillparser.getInstalledBrowsersWithAutofillSupportLevel
+import kotlinx.coroutines.launch
private enum class SettingsDialog {
Autofill,
@@ -125,6 +127,16 @@ fun SettingsSectionDestination(
)
}
+ val exportLog =
+ rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
+ if (uri != null)
+ activity.lifecycleScope.launch {
+ val message =
+ if (viewModel.exportLog(uri)) R.string.log_exported else R.string.log_export_failed
+ activity.snackbar(message = activity.getString(message))
+ }
+ }
+
// One screen lock prompt at a time: a double tap must not open two. A recreation resets it
// (remember), and the old activity's prompt is gone with it.
var prompting by remember { mutableStateOf(false) }
@@ -206,6 +218,7 @@ fun SettingsSectionDestination(
SettingsAction.ClearSecret -> dialog = SettingsDialog.ClearSecret
SettingsAction.DeleteRepository -> dialog = SettingsDialog.DeleteRepository
SettingsAction.Export -> export.launch(null)
+ SettingsAction.ExportLog -> exportLog.launch("hardenedpass-log.txt")
else -> onOpen(action)
}
},
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..e45166e0 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
@@ -47,6 +50,9 @@ fun NavGraphBuilder.settingsGraph(activity: MainActivity, nav: NavHostController
onBack = { close(entry) },
)
}
+ composable<AboutRoute> { entry ->
+ AboutDestination(activity = activity, onBack = { close(entry) })
+ }
composable<SettingsSectionRoute> { entry ->
SettingsSectionDestination(
activity = activity,
@@ -64,7 +70,8 @@ fun NavGraphBuilder.settingsGraph(activity: MainActivity, nav: NavHostController
// Handled by the destination itself.
SettingsAction.ClearSecret,
SettingsAction.DeleteRepository,
- SettingsAction.Export -> Unit
+ SettingsAction.Export,
+ SettingsAction.ExportLog -> Unit
}
},
onDeleted = {
diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsModel.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsModel.kt
index eca6770c..54250f74 100644
--- a/app/src/main/java/app/passwordstore/ui/settings/SettingsModel.kt
+++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsModel.kt
@@ -9,6 +9,7 @@ import app.passwordstore.util.extensions.hash
import app.passwordstore.util.extensions.time
import app.passwordstore.util.git.GitCommit
import java.io.IOException
+import java.io.OutputStream
import logcat.LogPriority.ERROR
import logcat.asLog
import logcat.logcat
@@ -77,3 +78,17 @@ fun gitLog(repo: Repository?): List<GitCommit> {
emptyList()
}
}
+
+/**
+ * Writes [header], then what logcat holds for this app: Android shows an app only its own lines,
+ * and those of its earlier runs and crashes until the buffer rotates.
+ */
+fun writeAppLog(header: String, out: OutputStream) {
+ out.write(header.toByteArray())
+ val logcat =
+ ProcessBuilder("logcat", "-d", "-b", "main,crash", "-v", "threadtime")
+ .redirectErrorStream(true)
+ .start()
+ logcat.inputStream.use { it.copyTo(out) }
+ logcat.waitFor()
+}
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 7d45ad74..2e7840ad 100644
--- a/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt
+++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt
@@ -35,6 +35,7 @@ import app.passwordstore.BuildConfig
import app.passwordstore.R
import app.passwordstore.ui.compose.GroupedSurface
import app.passwordstore.util.features.Feature
+import app.passwordstore.util.settings.Constants
import app.passwordstore.util.settings.PreferenceKeys
import kotlinx.collections.immutable.persistentListOf
import kotlinx.serialization.Serializable
@@ -62,12 +63,13 @@ enum class SettingsAction {
ClearSecret,
DeleteRepository,
Export,
+ ExportLog,
}
/** 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,
@@ -229,7 +231,7 @@ fun SettingsSectionScreen(
summary =
stringResource(
R.string.pref_clipboard_timeout_summary,
- state.clipboardTimeout.ifEmpty { "45" },
+ state.clipboardTimeout.ifEmpty { Constants.DEFAULT_DECRYPTION_TIMEOUT.toString() },
),
keyboardType = KeyboardType.Number,
)
@@ -257,6 +259,12 @@ fun SettingsSectionScreen(
else R.string.pref_rebase_on_pull_summary
),
)
+ PreferenceSwitch(
+ title = stringResource(R.string.pref_push_after_commit_title),
+ checked = state.pushAfterCommit,
+ onCheckedChange = { onBoolean(PreferenceKeys.PUSH_AFTER_COMMIT, it) },
+ summary = stringResource(R.string.pref_push_after_commit_summary),
+ )
if (state.isGitRepo) {
PreferenceAction(
title = stringResource(R.string.pref_edit_git_server_settings),
@@ -317,6 +325,11 @@ fun SettingsSectionScreen(
onCheckedChange = { onBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, it) },
summary = stringResource(R.string.pref_debug_logging_summary),
)
+ PreferenceAction(
+ title = stringResource(R.string.pref_export_log_title),
+ summary = stringResource(R.string.pref_export_log_summary),
+ onClick = { onAction(SettingsAction.ExportLog) },
+ )
}
SettingsSection.Pgp -> {
PreferenceAction(
diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsViewModel.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsViewModel.kt
index ef604f1f..90aecb2a 100644
--- a/app/src/main/java/app/passwordstore/ui/settings/SettingsViewModel.kt
+++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsViewModel.kt
@@ -8,6 +8,8 @@ package app.passwordstore.ui.settings
import android.app.Application
import android.content.SharedPreferences
import android.content.pm.ShortcutManager
+import android.net.Uri
+import android.os.Build
import androidx.annotation.StringRes
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -16,6 +18,7 @@ import androidx.core.content.edit
import androidx.core.content.getSystemService
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
+import app.passwordstore.BuildConfig
import app.passwordstore.R
import app.passwordstore.data.crypto.PGPPassphraseCache
import app.passwordstore.data.repo.PasswordRepository
@@ -31,12 +34,16 @@ import app.passwordstore.util.settings.GitSettings
import app.passwordstore.util.settings.PasswordSortOrder
import app.passwordstore.util.settings.PreferenceKeys
import dagger.hilt.android.lifecycle.HiltViewModel
+import java.io.IOException
import java.nio.file.Files
import java.nio.file.LinkOption
import javax.inject.Inject
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import logcat.LogPriority.ERROR
+import logcat.asLog
+import logcat.logcat
/** What the settings sections show: stored values, and the state rows depend on. */
data class SettingsState(
@@ -55,6 +62,7 @@ data class SettingsState(
val showPassword: Boolean,
val copyOnDecrypt: Boolean,
val rebaseOnPull: Boolean,
+ val pushAfterCommit: Boolean,
val isGitRepo: Boolean,
val httpsRemote: Boolean,
val canShowSshKey: Boolean,
@@ -110,6 +118,27 @@ constructor(
state = read()
}
+ /** Saves the app's log to [uri]; false when that failed. */
+ suspend fun exportLog(uri: Uri): Boolean =
+ withContext(dispatcherProvider.io()) {
+ val header =
+ "HardenedPass ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}), " +
+ "Android ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}), ${Build.MODEL}\n\n"
+ try {
+ val out =
+ getApplication<Application>().contentResolver.openOutputStream(uri)
+ ?: throw IOException("No stream for the chosen file")
+ out.use { writeAppLog(header, it) }
+ true
+ } catch (e: IOException) {
+ logcat(ERROR) { e.asLog() }
+ false
+ } catch (e: SecurityException) {
+ logcat(ERROR) { e.asLog() }
+ false
+ }
+ }
+
fun putBoolean(key: String, value: Boolean) {
prefs.edit { putBoolean(key, value) }
}
@@ -205,6 +234,7 @@ constructor(
showPassword = prefs.getBoolean(PreferenceKeys.SHOW_PASSWORD, true),
copyOnDecrypt = prefs.getBoolean(PreferenceKeys.COPY_ON_DECRYPT, false),
rebaseOnPull = gitSettings.rebaseOnPull,
+ pushAfterCommit = gitSettings.pushAfterCommit,
isGitRepo = PasswordRepository.isGitRepo(),
httpsRemote = gitSettings.url?.startsWith("https") == true,
canShowSshKey = SshKey.canShowSshPublicKey,
diff --git a/app/src/main/java/app/passwordstore/ui/setup/GitServerScreen.kt b/app/src/main/java/app/passwordstore/ui/setup/GitServerScreen.kt
index fd440996..4b24375b 100644
--- a/app/src/main/java/app/passwordstore/ui/setup/GitServerScreen.kt
+++ b/app/src/main/java/app/passwordstore/ui/setup/GitServerScreen.kt
@@ -32,6 +32,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
+import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
@@ -60,7 +61,6 @@ import app.passwordstore.util.settings.AuthMode
import app.passwordstore.util.settings.GitSettings.UpdateConnectionSettingsResult
import app.passwordstore.util.settings.Protocol
import com.github.michaelbull.result.fold
-import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
/**
@@ -210,7 +210,7 @@ fun GitServerDestination(
BackHandler(enabled = viewModel.progress != null) {}
fun say(message: String) {
- activity.snackbar(message = message, length = Snackbar.LENGTH_LONG)
+ activity.snackbar(message = message, length = SnackbarDuration.Long)
}
// ponytail: a theme or locale change cancels the clone with the activity's scope, like every
diff --git a/app/src/main/java/app/passwordstore/ui/setup/PgpKeysScreen.kt b/app/src/main/java/app/passwordstore/ui/setup/PgpKeysScreen.kt
index 11fd979c..6004ec90 100644
--- a/app/src/main/java/app/passwordstore/ui/setup/PgpKeysScreen.kt
+++ b/app/src/main/java/app/passwordstore/ui/setup/PgpKeysScreen.kt
@@ -31,6 +31,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
@@ -59,7 +60,6 @@ import app.passwordstore.ui.main.MainActivity
import app.passwordstore.util.extensions.commitChange
import app.passwordstore.util.extensions.snackbar
import com.github.michaelbull.result.onFailure
-import com.google.android.material.snackbar.Snackbar
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
@@ -269,7 +269,7 @@ fun PgpKeysDestination(
if (error != null) {
activity.snackbar(
message = activity.getString(R.string.store_create_failed, error),
- length = Snackbar.LENGTH_LONG,
+ length = SnackbarDuration.Long,
)
} else {
// A failed commit is reported like any git error; the store exists either way, and
diff --git a/app/src/main/java/app/passwordstore/ui/setup/SshKeyScreen.kt b/app/src/main/java/app/passwordstore/ui/setup/SshKeyScreen.kt
index f93fc9be..390f1160 100644
--- a/app/src/main/java/app/passwordstore/ui/setup/SshKeyScreen.kt
+++ b/app/src/main/java/app/passwordstore/ui/setup/SshKeyScreen.kt
@@ -195,7 +195,7 @@ fun SshKeyDestination(
busy = viewModel.busy,
onImport = { if (SshKey.exists) replace = true else picker.launch(arrayOf("*/*")) },
onGenerate = onGenerate,
- onCopy = { publicKey?.let { activity.copyTextToClipboard(it) } },
+ onCopy = { publicKey?.let { activity.copyTextToClipboard(it, sensitive = false) } },
onShare = { publicKey?.let { activity.startActivity(shareKey(it)) } },
onBack = { if (!viewModel.busy) onClose() },
)
diff --git a/app/src/main/java/app/passwordstore/util/auth/BiometricAuthenticator.kt b/app/src/main/java/app/passwordstore/util/auth/BiometricAuthenticator.kt
index 98f2229a..8f912879 100644
--- a/app/src/main/java/app/passwordstore/util/auth/BiometricAuthenticator.kt
+++ b/app/src/main/java/app/passwordstore/util/auth/BiometricAuthenticator.kt
@@ -5,7 +5,6 @@
package app.passwordstore.util.auth
import android.app.KeyguardManager
-import android.os.Build
import androidx.annotation.StringRes
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators
@@ -23,14 +22,10 @@ object BiometricAuthenticator {
/**
* Keystore keys that require user authentication are unlocked only by the device credential or a
* Class 3 (strong) biometric, so a weak biometric would pass the prompt and then fail to sign or
- * decrypt with `User not authenticated`. Android 9 and 10 do not support combining
- * [Authenticators.BIOMETRIC_STRONG] with [Authenticators.DEVICE_CREDENTIAL], so they keep the
- * weak class.
+ * decrypt with `User not authenticated`.
*/
private val VALID_AUTHENTICATORS =
- Authenticators.DEVICE_CREDENTIAL or
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) Authenticators.BIOMETRIC_STRONG
- else Authenticators.BIOMETRIC_WEAK
+ Authenticators.DEVICE_CREDENTIAL or Authenticators.BIOMETRIC_STRONG
/**
* Sealed class to wrap [BiometricPrompt]'s [Int]-based return codes into more easily-interpreted
diff --git a/app/src/main/java/app/passwordstore/util/autofill/Api26AutofillResponseBuilder.kt b/app/src/main/java/app/passwordstore/util/autofill/Api26AutofillResponseBuilder.kt
deleted file mode 100644
index f38011a7..00000000
--- a/app/src/main/java/app/passwordstore/util/autofill/Api26AutofillResponseBuilder.kt
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package app.passwordstore.util.autofill
-
-import android.content.Context
-import android.content.IntentSender
-import android.os.Build
-import android.service.autofill.Dataset
-import android.service.autofill.FillCallback
-import android.service.autofill.FillRequest
-import android.service.autofill.FillResponse
-import android.service.autofill.SaveInfo
-import app.passwordstore.ui.autofill.AutofillDecryptActivity
-import app.passwordstore.ui.autofill.AutofillFilterView
-import app.passwordstore.ui.autofill.AutofillPublisherChangedActivity
-import app.passwordstore.ui.autofill.AutofillSaveActivity
-import com.github.androidpasswordstore.autofillparser.AutofillAction
-import com.github.androidpasswordstore.autofillparser.FillableForm
-import com.github.androidpasswordstore.autofillparser.fillWith
-import com.github.michaelbull.result.fold
-import java.io.File
-import logcat.LogPriority.ERROR
-import logcat.asLog
-import logcat.logcat
-
-class Api26AutofillResponseBuilder private constructor(form: FillableForm) :
- AutofillResponseBuilder {
-
- object Factory : AutofillResponseBuilder.Factory {
- override fun create(form: FillableForm): AutofillResponseBuilder =
- Api26AutofillResponseBuilder(form)
- }
-
- private val formOrigin = form.formOrigin
- private val scenario = form.scenario
- private val ignoredIds = form.ignoredIds
- private val saveFlags = form.saveFlags
- private val clientState = form.toClientState()
-
- // We do not offer save when the only relevant field is a username field or there is no field.
- private val scenarioSupportsSave = scenario.hasPasswordFieldsToSave
- private val canBeSaved = saveFlags != null && scenarioSupportsSave
-
- @Suppress("DEPRECATION")
- private fun makeIntentDataset(
- context: Context,
- action: AutofillAction,
- intentSender: IntentSender,
- metadata: DatasetMetadata,
- ): Dataset {
- return Dataset.Builder(makeRemoteView(context, metadata)).run {
- fillWith(scenario, action, credentials = null)
- setAuthentication(intentSender)
- build()
- }
- }
-
- private fun makeMatchDataset(context: Context, file: File): Dataset? {
- if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null
- val metadata = makeFillMatchMetadata(context, file)
- val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
- return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata)
- }
-
- private fun makeSearchDataset(context: Context): Dataset? {
- if (!scenario.hasFieldsToFillOn(AutofillAction.Search)) return null
- val metadata = makeSearchAndFillMetadata(context)
- val intentSender = AutofillFilterView.makeMatchAndDecryptFileIntentSender(context, formOrigin)
- return makeIntentDataset(context, AutofillAction.Search, intentSender, metadata)
- }
-
- private fun makeGenerateDataset(context: Context): Dataset? {
- if (!scenario.hasFieldsToFillOn(AutofillAction.Generate)) return null
- val metadata = makeGenerateAndFillMetadata(context)
- val intentSender = AutofillSaveActivity.makeSaveIntentSender(context, null, formOrigin)
- return makeIntentDataset(context, AutofillAction.Generate, intentSender, metadata)
- }
-
- private fun makePublisherChangedDataset(
- context: Context,
- publisherChangedException: AutofillPublisherChangedException,
- ): Dataset {
- val metadata = makeWarningMetadata(context)
- // If the user decides to trust the new publisher, they can choose reset the list of
- // matches. In this case we need to immediately show a new `FillResponse` as if the app were
- // autofilled for the first time. This `FillResponse` needs to be returned as a result from
- // `AutofillPublisherChangedActivity`, which is why we create and pass it on here.
- val fillResponseAfterReset = makeFillResponse(context, emptyList())
- val intentSender =
- AutofillPublisherChangedActivity.makePublisherChangedIntentSender(
- context,
- publisherChangedException,
- fillResponseAfterReset,
- )
- return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata)
- }
-
- private fun makePublisherChangedResponse(
- context: Context,
- publisherChangedException: AutofillPublisherChangedException,
- ): FillResponse {
- return FillResponse.Builder().run {
- addDataset(makePublisherChangedDataset(context, publisherChangedException))
- setIgnoredIds(*ignoredIds.toTypedArray())
- build()
- }
- }
-
- // TODO: Support multi-step authentication flows in apps via FLAG_DELAY_SAVE
- // See:
- // https://developer.android.com/reference/android/service/autofill/SaveInfo#FLAG_DELAY_SAVE
- private fun makeSaveInfo(): SaveInfo? {
- if (!canBeSaved) return null
- check(saveFlags != null) { "saveFlags must not be null" }
- val idsToSave = scenario.fieldsToSave.toTypedArray()
- if (idsToSave.isEmpty()) return null
- var saveDataTypes = SaveInfo.SAVE_DATA_TYPE_PASSWORD
- if (scenario.hasUsername) {
- saveDataTypes = saveDataTypes or SaveInfo.SAVE_DATA_TYPE_USERNAME
- }
- return SaveInfo.Builder(saveDataTypes, idsToSave).run {
- setFlags(saveFlags)
- build()
- }
- }
-
- private fun makeFillResponse(context: Context, matchedFiles: List<File>): FillResponse? {
- var datasetCount = 0
- return FillResponse.Builder().run {
- for (file in matchedFiles) {
- makeMatchDataset(context, file)?.let {
- datasetCount++
- addDataset(it)
- }
- }
- makeGenerateDataset(context)?.let {
- datasetCount++
- addDataset(it)
- }
- makeSearchDataset(context)?.let {
- datasetCount++
- addDataset(it)
- }
- if (datasetCount == 0) return null
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- setHeader(
- makeRemoteView(
- context,
- makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true)),
- )
- )
- }
- makeSaveInfo()?.let { setSaveInfo(it) }
- setClientState(clientState)
- setIgnoredIds(*ignoredIds.toTypedArray())
- build()
- }
- }
-
- /** Creates and returns a suitable [FillResponse] to the Autofill framework. */
- override fun fillCredentials(context: Context, fillRequest: FillRequest, callback: FillCallback) {
- AutofillMatcher.getMatchesFor(context, formOrigin)
- .fold(
- success = { matchedFiles -> callback.onSuccess(makeFillResponse(context, matchedFiles)) },
- failure = { e ->
- logcat(ERROR) { e.asLog() }
- callback.onSuccess(makePublisherChangedResponse(context, e))
- },
- )
- }
-}
diff --git a/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt b/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt
index 55679b21..bf41327f 100644
--- a/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt
+++ b/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt
@@ -31,7 +31,6 @@ import logcat.asLog
import logcat.logcat
/** Implements [AutofillResponseBuilder]'s methods for API 30 and above */
-@RequiresApi(Build.VERSION_CODES.R)
class Api30AutofillResponseBuilder private constructor(form: FillableForm) :
AutofillResponseBuilder {
diff --git a/app/src/main/java/app/passwordstore/util/autofill/AutofillMatcher.kt b/app/src/main/java/app/passwordstore/util/autofill/AutofillMatcher.kt
index dbaa5675..b4b3d017 100644
--- a/app/src/main/java/app/passwordstore/util/autofill/AutofillMatcher.kt
+++ b/app/src/main/java/app/passwordstore/util/autofill/AutofillMatcher.kt
@@ -194,7 +194,7 @@ class AutofillMatcher {
.minus(oldNewPathMap.values)
.map { match ->
val newPath = oldNewPathMap[match] ?: return@map match
- logcat { "Updating match for $key: $match --> $newPath" }
+ logcat { "Updating match for $key" }
newPath
}
.toSet()
diff --git a/app/src/main/java/app/passwordstore/util/autofill/AutofillResponseBuilder.kt b/app/src/main/java/app/passwordstore/util/autofill/AutofillResponseBuilder.kt
index bfa47d74..a98aefc5 100644
--- a/app/src/main/java/app/passwordstore/util/autofill/AutofillResponseBuilder.kt
+++ b/app/src/main/java/app/passwordstore/util/autofill/AutofillResponseBuilder.kt
@@ -6,7 +6,6 @@
package app.passwordstore.util.autofill
import android.content.Context
-import android.os.Build
import android.os.Bundle
import android.service.autofill.Dataset
import android.service.autofill.FillCallback
@@ -34,19 +33,11 @@ interface AutofillResponseBuilder {
action: AutofillAction,
): Dataset {
val scenario = AutofillScenario.fromClientState(clientState)
- // Before Android P, Datasets used for fill-in had to come with a RemoteViews, even
- // though they are rarely shown.
// FIXME: We should clone the original dataset here and add the credentials to be filled
// in. Otherwise, the entry in the cached list of datasets will be overwritten by the
// fill-in dataset without any visual representation. This causes it to be missing from
// the Autofill suggestions shown after the user clears the filled out form fields.
- val builder =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- Dataset.Builder()
- } else {
- @Suppress("DEPRECATION") Dataset.Builder(makeRemoteView(context, makeEmptyMetadata()))
- }
- return builder.run {
+ return Dataset.Builder().run {
if (scenario != null) fillWith(scenario, action, credentials)
else logcat(LogPriority.ERROR) { "Failed to recover scenario from client state" }
build()
diff --git a/app/src/main/java/app/passwordstore/util/autofill/AutofillViewUtils.kt b/app/src/main/java/app/passwordstore/util/autofill/AutofillViewUtils.kt
index 59fd5cca..321c111a 100644
--- a/app/src/main/java/app/passwordstore/util/autofill/AutofillViewUtils.kt
+++ b/app/src/main/java/app/passwordstore/util/autofill/AutofillViewUtils.kt
@@ -10,7 +10,6 @@ import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.graphics.drawable.Icon
-import android.os.Build
import android.service.autofill.InlinePresentation
import android.view.View
import android.widget.RemoteViews
@@ -47,8 +46,6 @@ fun makeInlinePresentation(
imeSpec: InlinePresentationSpec,
metadata: DatasetMetadata,
): InlinePresentation? {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return null
-
if (UiVersions.INLINE_UI_VERSION_1 !in UiVersions.getVersions(imeSpec.style)) return null
val launchIntent =
@@ -56,11 +53,7 @@ fun makeInlinePresentation(
context,
0,
Intent(context, LaunchActivity::class.java),
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- PendingIntent.FLAG_MUTABLE
- } else {
- 0
- },
+ PendingIntent.FLAG_MUTABLE,
)
val slice =
InlineSuggestionUi.newContentBuilder(launchIntent).run {
@@ -101,8 +94,6 @@ fun makeGenerateAndFillMetadata(context: Context) =
R.drawable.ic_autofill_new_password,
)
-fun makeEmptyMetadata() = DatasetMetadata("PLACEHOLDER", "PLACEHOLDER", R.mipmap.ic_launcher)
-
fun makeWarningMetadata(context: Context) =
DatasetMetadata(
context.getString(R.string.oreo_autofill_warning_publisher_dataset_title),
diff --git a/app/src/main/java/app/passwordstore/util/crypto/KeystorePreferences.kt b/app/src/main/java/app/passwordstore/util/crypto/KeystorePreferences.kt
index f4d43e95..e23ec896 100644
--- a/app/src/main/java/app/passwordstore/util/crypto/KeystorePreferences.kt
+++ b/app/src/main/java/app/passwordstore/util/crypto/KeystorePreferences.kt
@@ -7,7 +7,6 @@ package app.passwordstore.util.crypto
import android.content.Context
import android.content.SharedPreferences
-import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.security.keystore.StrongBoxUnavailableException
@@ -66,18 +65,14 @@ fun keystoreAesKey(
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.apply(configure)
- .apply {
- if (useStrongBox && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- setIsStrongBoxBacked(true)
- }
- }
+ .setIsStrongBoxBacked(useStrongBox)
.build()
return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore").run {
init(spec)
generateKey()
}
}
- if (!strongBox || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return generate(false)
+ if (!strongBox) return generate(false)
return try {
generate(true)
} catch (_: StrongBoxUnavailableException) {
@@ -91,14 +86,10 @@ fun keystoreAesKey(
*/
fun KeyGenParameterSpec.Builder.requireUserAuthentication(seconds: Int) {
setUserAuthenticationRequired(true)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- setUserAuthenticationParameters(
- seconds,
- KeyProperties.AUTH_DEVICE_CREDENTIAL or KeyProperties.AUTH_BIOMETRIC_STRONG,
- )
- } else {
- @Suppress("DEPRECATION") setUserAuthenticationValidityDurationSeconds(seconds)
- }
+ setUserAuthenticationParameters(
+ seconds,
+ KeyProperties.AUTH_DEVICE_CREDENTIAL or KeyProperties.AUTH_BIOMETRIC_STRONG,
+ )
}
/**
diff --git a/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt b/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt
index 6528334d..e24f96ca 100644
--- a/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt
+++ b/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt
@@ -16,21 +16,21 @@ import android.content.pm.PackageManager.ApplicationInfoFlags
import android.content.pm.PackageManager.PackageInfoFlags
import android.os.Build
import android.util.Base64
-import android.view.View
import android.view.autofill.AutofillManager
+import androidx.compose.material3.SnackbarDuration
import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity
import app.passwordstore.BuildConfig
-import app.passwordstore.R
import app.passwordstore.data.repo.PasswordRepository
+import app.passwordstore.ui.prompts.Snack
+import app.passwordstore.ui.prompts.prompts
import app.passwordstore.util.crypto.keystorePreferences
import app.passwordstore.util.git.operation.GitOperation
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
-import com.google.android.material.snackbar.Snackbar
import logcat.logcat
-/** Get an instance of [AutofillManager]. Only available on Android Oreo and above */
+/** Get an instance of [AutofillManager] */
val Context.autofillManager: AutofillManager?
get() = getSystemService()
@@ -70,24 +70,19 @@ suspend fun FragmentActivity.commitChange(message: String): Result<Unit, Throwab
git.commit().setAll(true).setMessage(message),
)
- override fun preExecute(): Boolean {
- logcat { "Committing with message: '$message'" }
+ override suspend fun preExecute(): Boolean {
+ logcat { "Committing" }
return true
}
}
.execute()
}
-/** Show a [Snackbar] in a [FragmentActivity]. */
+/** Shows a snackbar through this activity's [prompts]; it must host a `PromptHost`. */
fun FragmentActivity.snackbar(
- view: View = findViewById(android.R.id.content),
message: String,
- length: Int = Snackbar.LENGTH_SHORT,
-): Snackbar {
- val snackbar = Snackbar.make(view, message, length)
- snackbar.show()
- return snackbar
-}
+ length: SnackbarDuration = SnackbarDuration.Short,
+): Snack = prompts.snackbar(message, length)
/** Simplifies the common `getString(key, null) ?: defaultValue` case slightly */
fun SharedPreferences.getString(key: String): String? = getString(key, null)
diff --git a/app/src/main/java/app/passwordstore/util/git/GitCommandExecutor.kt b/app/src/main/java/app/passwordstore/util/git/GitCommandExecutor.kt
index feb2cb2d..6d497bce 100644
--- a/app/src/main/java/app/passwordstore/util/git/GitCommandExecutor.kt
+++ b/app/src/main/java/app/passwordstore/util/git/GitCommandExecutor.kt
@@ -6,8 +6,10 @@
package app.passwordstore.util.git
import android.widget.Toast
+import androidx.compose.material3.SnackbarDuration
import androidx.fragment.app.FragmentActivity
import app.passwordstore.R
+import app.passwordstore.ui.prompts.prompts
import app.passwordstore.util.coroutines.DispatcherProvider
import app.passwordstore.util.extensions.snackbar
import app.passwordstore.util.extensions.unsafeLazy
@@ -17,7 +19,6 @@ import app.passwordstore.util.git.operation.GitOperation
import app.passwordstore.util.settings.GitSettings
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.runCatching
-import com.google.android.material.snackbar.Snackbar
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
@@ -45,10 +46,10 @@ class GitCommandExecutor(
suspend fun execute(): Result<Unit, Throwable> {
val gitSettings = hiltEntryPoint.gitSettings()
val dispatcherProvider = hiltEntryPoint.dispatcherProvider()
- val snackbar =
+ val running =
activity.snackbar(
message = activity.resources.getString(R.string.git_operation_running),
- length = Snackbar.LENGTH_INDEFINITE,
+ length = SnackbarDuration.Indefinite,
)
// Count the number of uncommitted files
var nbChanges = 0
@@ -123,7 +124,7 @@ class GitCommandExecutor(
}
}
}
- .also { snackbar.dismiss() }
+ .also { activity.prompts.dismiss(running) }
}
@EntryPoint
diff --git a/app/src/main/java/app/passwordstore/util/git/operation/BreakOutOfDetached.kt b/app/src/main/java/app/passwordstore/util/git/operation/BreakOutOfDetached.kt
index 4b4913d0..a08f2a35 100644
--- a/app/src/main/java/app/passwordstore/util/git/operation/BreakOutOfDetached.kt
+++ b/app/src/main/java/app/passwordstore/util/git/operation/BreakOutOfDetached.kt
@@ -4,17 +4,17 @@
*/
package app.passwordstore.util.git.operation
-import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.FragmentActivity
import app.passwordstore.R
import app.passwordstore.data.repo.PasswordRepository
+import app.passwordstore.ui.prompts.prompts
import app.passwordstore.util.extensions.unsafeLazy
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.eclipse.jgit.api.GitCommand
import org.eclipse.jgit.api.RebaseCommand
import org.eclipse.jgit.api.ResetCommand
import org.eclipse.jgit.lib.RepositoryState
-class BreakOutOfDetached(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
+class BreakOutOfDetached(callingActivity: FragmentActivity) : GitOperation(callingActivity) {
private val merging = repository.repositoryState == RepositoryState.MERGING
private val localBranch = PasswordRepository.getCurrentBranch()
@@ -50,16 +50,14 @@ class BreakOutOfDetached(callingActivity: AppCompatActivity) : GitOperation(call
}
}
- override fun preExecute() =
+ override suspend fun preExecute() =
if (!git.repository.repositoryState.isRebasing && !merging) {
- MaterialAlertDialogBuilder(callingActivity)
- .setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title))
- .setMessage(
- callingActivity.resources.getString(R.string.git_break_out_of_detached_unneeded)
- )
- // Only closes the dialog: the calling activity is the app's main screen.
- .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok), null)
- .show()
+ val resources = callingActivity.resources
+ callingActivity.prompts.ask(
+ title = resources.getString(R.string.git_abort_and_push_title),
+ message = resources.getString(R.string.git_break_out_of_detached_unneeded),
+ confirm = resources.getString(R.string.dialog_ok),
+ )
false
} else {
true
diff --git a/app/src/main/java/app/passwordstore/util/git/operation/CloneOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/CloneOperation.kt
index 0386f674..0d74835f 100644
--- a/app/src/main/java/app/passwordstore/util/git/operation/CloneOperation.kt
+++ b/app/src/main/java/app/passwordstore/util/git/operation/CloneOperation.kt
@@ -4,7 +4,7 @@
*/
package app.passwordstore.util.git.operation
-import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.FragmentActivity
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.GitCommand
@@ -14,7 +14,7 @@ import org.eclipse.jgit.api.GitCommand
* @param uri URL to clone the repository from
* @param callingActivity the calling activity
*/
-class CloneOperation(callingActivity: AppCompatActivity, uri: String) :
+class CloneOperation(callingActivity: FragmentActivity, uri: String) :
GitOperation(callingActivity) {
override val commands: Array<GitCommand<out Any>> =
diff --git a/app/src/main/java/app/passwordstore/util/git/operation/CredentialFinder.kt b/app/src/main/java/app/passwordstore/util/git/operation/CredentialFinder.kt
index e0ecb170..a0d6d16d 100644
--- a/app/src/main/java/app/passwordstore/util/git/operation/CredentialFinder.kt
+++ b/app/src/main/java/app/passwordstore/util/git/operation/CredentialFinder.kt
@@ -5,24 +5,17 @@
package app.passwordstore.util.git.operation
-import android.annotation.SuppressLint
import android.content.SharedPreferences
-import android.view.LayoutInflater
-import android.view.WindowManager
import androidx.annotation.StringRes
import androidx.core.content.edit
-import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.FragmentActivity
import app.passwordstore.R
import app.passwordstore.injection.prefs.GitPreferences
+import app.passwordstore.ui.prompts.Prompts
import app.passwordstore.util.coroutines.DispatcherProvider
import app.passwordstore.util.git.sshj.InteractivePasswordFinder
import app.passwordstore.util.settings.AuthMode
import app.passwordstore.util.settings.PreferenceKeys
-import com.google.android.material.checkbox.MaterialCheckBox
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.textfield.TextInputEditText
-import com.google.android.material.textfield.TextInputLayout
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
@@ -32,6 +25,7 @@ import kotlin.coroutines.resume
class CredentialFinder(
private val callingActivity: FragmentActivity,
+ private val prompts: Prompts,
private val authMode: AuthMode,
dispatcherProvider: DispatcherProvider,
) : InteractivePasswordFinder(dispatcherProvider) {
@@ -71,42 +65,18 @@ class CredentialFinder(
if (isRetry) gitOperationPrefs.edit { remove(credentialPref) }
val storedCredential = gitOperationPrefs.getString(credentialPref, null)
if (storedCredential == null) {
- val layoutInflater = LayoutInflater.from(callingActivity)
-
- @SuppressLint("InflateParams")
- val dialogView = layoutInflater.inflate(R.layout.git_credential_layout, null)
- val credentialLayout =
- dialogView.findViewById<TextInputLayout>(R.id.git_auth_passphrase_layout)
- val editCredential = dialogView.findViewById<TextInputEditText>(R.id.git_auth_credential)
- editCredential.setHint(hintRes)
- val rememberCredential =
- dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_credential)
- rememberCredential.setText(rememberRes)
- if (isRetry) {
- credentialLayout.error = callingActivity.resources.getString(errorRes)
- // Reset error when user starts entering a password
- editCredential.doOnTextChanged { _, _, _, _ -> credentialLayout.error = null }
- }
- MaterialAlertDialogBuilder(callingActivity)
- .run {
- setTitle(R.string.passphrase_dialog_title)
- setMessage(messageRes)
- setView(dialogView)
- setPositiveButton(R.string.dialog_ok) { _, _ ->
- val credential = editCredential.text.toString()
- if (rememberCredential.isChecked) {
- gitOperationPrefs.edit { putString(credentialPref, credential) }
- }
- cont.resume(credential)
- }
- setNegativeButton(R.string.dialog_cancel) { _, _ -> cont.resume(null) }
- setOnCancelListener { cont.resume(null) }
- create()
- }
- .run {
- window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
- show()
+ val resources = callingActivity.resources
+ prompts.askCredential(
+ message = resources.getString(messageRes),
+ hint = resources.getString(hintRes),
+ remember = resources.getString(rememberRes),
+ error = if (isRetry) resources.getString(errorRes) else null,
+ ) { answer ->
+ if (answer?.remember == true) {
+ gitOperationPrefs.edit { putString(credentialPref, answer.text) }
}
+ cont.resume(answer?.text)
+ }
} else {
cont.resume(storedCredential)
}
diff --git a/app/src/main/java/app/passwordstore/util/git/operation/GcOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/GcOperation.kt
index c1487fbf..64467f3e 100644
--- a/app/src/main/java/app/passwordstore/util/git/operation/GcOperation.kt
+++ b/app/src/main/java/app/passwordstore/util/git/operation/GcOperation.kt
@@ -5,7 +5,7 @@
package app.passwordstore.util.git.operation
-import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.FragmentActivity
import org.eclipse.jgit.api.GitCommand
import org.eclipse.jgit.internal.storage.file.FileRepository
import org.eclipse.jgit.internal.storage.file.GC
@@ -14,7 +14,7 @@ import org.eclipse.jgit.internal.storage.file.GC
* Run an aggressive garbage collection job on the repository, expiring every loose object to
* achieve the best compression.
*/
-class GcOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
+class GcOperation(callingActivity: FragmentActivity) : GitOperation(callingActivity) {
override val requiresAuth: Boolean = false
override val commands: Array<GitCommand<out Any>> =
diff --git a/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt
index 25d99d6d..f4ed7cd1 100644
--- a/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt
+++ b/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt
@@ -5,11 +5,12 @@
package app.passwordstore.util.git.operation
import android.widget.Toast
-import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentActivity
import app.passwordstore.R
import app.passwordstore.data.repo.PasswordRepository
import app.passwordstore.ui.main.MainActivity
+import app.passwordstore.ui.prompts.Choice
+import app.passwordstore.ui.prompts.prompts
import app.passwordstore.ui.setup.SetupStart
import app.passwordstore.util.auth.BiometricAuthenticator
import app.passwordstore.util.auth.BiometricAuthenticator.Result.CanceledByUser
@@ -24,7 +25,6 @@ import app.passwordstore.util.settings.AuthMode
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
@@ -63,8 +63,6 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
EntryPointAccessors.fromApplication<GitOperationEntryPoint>(callingActivity)
protected val repository = PasswordRepository.repository!!
protected val git = Git(repository)
- private val authActivity
- get() = callingActivity as AppCompatActivity
private class HttpsCredentialsProvider(private val passwordFinder: PasswordFinder) :
CredentialsProvider() {
@@ -131,22 +129,22 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
return operationResult
}
- private fun onMissingSshKeyFile() {
- MaterialAlertDialogBuilder(callingActivity)
- .setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text))
- .setTitle(callingActivity.resources.getString(R.string.ssh_preferences_dialog_title))
- .setPositiveButton(callingActivity.resources.getString(R.string.button_label_import)) { _, _
- ->
- getSshKey(false)
- }
- .setNegativeButton(
- callingActivity.resources.getString(R.string.ssh_preferences_dialog_generate)
- ) { _, _ ->
- getSshKey(true)
- }
- // Cancel only closes the dialog: the calling activity is the app's own screen now.
- .setNeutralButton(callingActivity.resources.getString(R.string.dialog_cancel), null)
- .show()
+ private suspend fun onMissingSshKeyFile() {
+ val resources = callingActivity.resources
+ val choice =
+ callingActivity.prompts.ask(
+ title = resources.getString(R.string.ssh_preferences_dialog_title),
+ message = resources.getString(R.string.ssh_preferences_dialog_text),
+ confirm = resources.getString(R.string.button_label_import),
+ cancel = resources.getString(R.string.dialog_cancel),
+ other = resources.getString(R.string.ssh_preferences_dialog_generate),
+ )
+ when (choice) {
+ Choice.Confirm -> getSshKey(false)
+ Choice.Other -> getSshKey(true)
+ // Only closes the dialog: the calling activity is the app's own screen now.
+ Choice.Cancel -> {}
+ }
}
suspend fun executeAfterAuthentication(authMode: AuthMode): Result<Unit, Throwable> {
@@ -169,7 +167,7 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
}
when (result) {
is Success -> {
- registerAuthProviders(SshAuthMethod.SshKey(authActivity))
+ registerAuthProviders(SshAuthMethod.SshKey(callingActivity))
}
is CanceledByUser -> {
return Err(SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER))
@@ -189,7 +187,7 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
}
}
} else {
- registerAuthProviders(SshAuthMethod.SshKey(authActivity))
+ registerAuthProviders(SshAuthMethod.SshKey(callingActivity))
}
} else {
onMissingSshKeyFile()
@@ -202,11 +200,12 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
HttpsCredentialsProvider(
CredentialFinder(
callingActivity,
+ callingActivity.prompts,
AuthMode.Password,
hiltEntryPoint.dispatcherProvider(),
)
)
- registerAuthProviders(SshAuthMethod.Password(authActivity), httpsCredentialProvider)
+ registerAuthProviders(SshAuthMethod.Password(callingActivity), httpsCredentialProvider)
}
AuthMode.None -> {}
}
@@ -214,7 +213,7 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
}
/** Called before execution of the Git operation. Return false to cancel. */
- open fun preExecute() = true
+ open suspend fun preExecute() = true
private suspend fun postExecute() {
withContext(hiltEntryPoint.dispatcherProvider().io()) { sshSessionFactory?.close() }
diff --git a/app/src/main/java/app/passwordstore/util/git/operation/PullOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/PullOperation.kt
index 1966c4ab..4738d7ee 100644
--- a/app/src/main/java/app/passwordstore/util/git/operation/PullOperation.kt
+++ b/app/src/main/java/app/passwordstore/util/git/operation/PullOperation.kt
@@ -4,10 +4,10 @@
*/
package app.passwordstore.util.git.operation
-import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.FragmentActivity
import org.eclipse.jgit.api.GitCommand
-class PullOperation(callingActivity: AppCompatActivity, rebase: Boolean) :
+class PullOperation(callingActivity: FragmentActivity, rebase: Boolean) :
GitOperation(callingActivity) {
/**
diff --git a/app/src/main/java/app/passwordstore/util/git/operation/PushOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/PushOperation.kt
index e013b401..a998cda5 100644
--- a/app/src/main/java/app/passwordstore/util/git/operation/PushOperation.kt
+++ b/app/src/main/java/app/passwordstore/util/git/operation/PushOperation.kt
@@ -4,10 +4,10 @@
*/
package app.passwordstore.util.git.operation
-import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.FragmentActivity
import org.eclipse.jgit.api.GitCommand
-class PushOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
+class PushOperation(callingActivity: FragmentActivity) : GitOperation(callingActivity) {
override val commands: Array<GitCommand<out Any>> =
arrayOf(git.push().setPushAll().setRemote("origin"))
diff --git a/app/src/main/java/app/passwordstore/util/git/operation/ResetToRemoteOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/ResetToRemoteOperation.kt
index 7ca1282b..904c62ee 100644
--- a/app/src/main/java/app/passwordstore/util/git/operation/ResetToRemoteOperation.kt
+++ b/app/src/main/java/app/passwordstore/util/git/operation/ResetToRemoteOperation.kt
@@ -4,12 +4,12 @@
*/
package app.passwordstore.util.git.operation
-import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.FragmentActivity
import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode.TRACK
import org.eclipse.jgit.api.GitCommand
import org.eclipse.jgit.api.ResetCommand
-class ResetToRemoteOperation(callingActivity: AppCompatActivity, remoteBranch: String) :
+class ResetToRemoteOperation(callingActivity: FragmentActivity, remoteBranch: String) :
GitOperation(callingActivity) {
override val commands: Array<GitCommand<out Any>> =
diff --git a/app/src/main/java/app/passwordstore/util/git/operation/SyncOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/SyncOperation.kt
index 25078e8a..c598b829 100644
--- a/app/src/main/java/app/passwordstore/util/git/operation/SyncOperation.kt
+++ b/app/src/main/java/app/passwordstore/util/git/operation/SyncOperation.kt
@@ -4,10 +4,10 @@
*/
package app.passwordstore.util.git.operation
-import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.FragmentActivity
import org.eclipse.jgit.api.GitCommand
-class SyncOperation(callingActivity: AppCompatActivity, rebase: Boolean) :
+class SyncOperation(callingActivity: FragmentActivity, rebase: Boolean) :
GitOperation(callingActivity) {
override val commands: Array<GitCommand<out Any>> =
diff --git a/app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt b/app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt
index 7c8f5ba5..e2a2c2dd 100644
--- a/app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt
+++ b/app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt
@@ -7,7 +7,6 @@ package app.passwordstore.util.git.sshj
import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri
-import android.os.Build
import android.provider.OpenableColumns
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyInfo
@@ -157,9 +156,7 @@ object SshKey {
context.sharedPrefs.edit { putString(PreferenceKeys.GIT_REMOTE_KEY_TYPE, value?.value) }
private val isStrongBoxSupported by unsafeLazy {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
- context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
- else false
+ context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
}
private enum class Type(val value: String) {
@@ -198,9 +195,7 @@ object SshKey {
setKeySize(256)
setAlgorithmParameterSpec(java.security.spec.ECGenParameterSpec("secp256r1"))
setDigests(KeyProperties.DIGEST_SHA256)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- setIsStrongBoxBacked(isStrongBoxSupported)
- }
+ setIsStrongBoxBacked(isStrongBoxSupported)
},
),
}
@@ -316,17 +311,7 @@ object SshKey {
val parameterSpec =
KeyGenParameterSpec.Builder(KEYSTORE_ALIAS, KeyProperties.PURPOSE_SIGN).run {
apply(algorithm.applyToSpec)
- if (requireAuthentication) {
- setUserAuthenticationRequired(true)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- setUserAuthenticationParameters(
- 30,
- KeyProperties.AUTH_DEVICE_CREDENTIAL or KeyProperties.AUTH_BIOMETRIC_STRONG,
- )
- } else {
- @Suppress("DEPRECATION") setUserAuthenticationValidityDurationSeconds(30)
- }
- }
+ if (requireAuthentication) requireUserAuthentication(30)
build()
}
val keyPair =
diff --git a/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt b/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt
index 51657b02..631e416f 100644
--- a/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt
+++ b/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt
@@ -5,7 +5,9 @@
package app.passwordstore.util.git.sshj
import android.util.Base64
-import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.FragmentActivity
+import app.passwordstore.ui.prompts.Prompts
+import app.passwordstore.ui.prompts.prompts
import app.passwordstore.util.coroutines.DispatcherProvider
import app.passwordstore.util.git.operation.CredentialFinder
import app.passwordstore.util.settings.AuthMode
@@ -42,10 +44,16 @@ import org.eclipse.jgit.transport.SshSessionFactory
import org.eclipse.jgit.transport.URIish
import org.eclipse.jgit.util.FS
-sealed class SshAuthMethod(val activity: AppCompatActivity) {
- class Password(activity: AppCompatActivity) : SshAuthMethod(activity)
+sealed class SshAuthMethod(val activity: FragmentActivity) {
+ /**
+ * The activity's [Prompts], taken while it is alive: the SSH thread reads it later, and asking an
+ * activity that was finished meanwhile for it would create one nothing draws.
+ */
+ val prompts = activity.prompts
+
+ class Password(activity: FragmentActivity) : SshAuthMethod(activity)
- class SshKey(activity: AppCompatActivity) : SshAuthMethod(activity)
+ class SshKey(activity: FragmentActivity) : SshAuthMethod(activity)
}
abstract class InteractivePasswordFinder(private val dispatcherProvider: DispatcherProvider) :
@@ -156,7 +164,14 @@ private class SshjSession(
ssh.connect(uri.host, uri.port.takeUnless { it == -1 } ?: 22)
if (!ssh.isConnected) throw IOException()
val passwordAuth =
- AuthPassword(CredentialFinder(authMethod.activity, AuthMode.Password, dispatcherProvider))
+ AuthPassword(
+ CredentialFinder(
+ authMethod.activity,
+ authMethod.prompts,
+ AuthMode.Password,
+ dispatcherProvider,
+ )
+ )
when (authMethod) {
is SshAuthMethod.Password -> {
ssh.auth(username, passwordAuth)
@@ -166,7 +181,12 @@ private class SshjSession(
AuthPublickey(
SshKey.provide(
ssh,
- CredentialFinder(authMethod.activity, AuthMode.SshKey, dispatcherProvider),
+ CredentialFinder(
+ authMethod.activity,
+ authMethod.prompts,
+ AuthMode.SshKey,
+ dispatcherProvider,
+ ),
)
)
ssh.auth(username, pubkeyAuth, passwordAuth)
diff --git a/app/src/main/java/app/passwordstore/util/log/LogcatLogger.kt b/app/src/main/java/app/passwordstore/util/log/LogcatLogger.kt
index c62e60a8..5ac2e0db 100644
--- a/app/src/main/java/app/passwordstore/util/log/LogcatLogger.kt
+++ b/app/src/main/java/app/passwordstore/util/log/LogcatLogger.kt
@@ -12,7 +12,8 @@ import logcat.logcat
/** An implementation of [AbstractLogger] that forwards logging calls to [logcat]. */
class LogcatLogger(name: String) : AbstractLogger(name) {
- override fun isTraceEnabled() = true
+ // sshj's trace level dumps every SSH packet in the clear, passwords included. Never log it.
+ override fun isTraceEnabled() = false
override fun isDebugEnabled() = true
@@ -27,11 +28,11 @@ class LogcatLogger(name: String) : AbstractLogger(name) {
@Suppress("RegExpRedundantEscape")
private fun String.fix() = replace("""(?!<\\)\{\}""".toRegex(), "%s")
- override fun t(message: String, t: Throwable?, vararg args: Any?) {
- logcat(name, LogPriority.VERBOSE) { message.fix().format(*args) + (t?.asLog() ?: "") }
- }
+ override fun t(message: String, t: Throwable?, vararg args: Any?) = Unit
override fun d(message: String, t: Throwable?, vararg args: Any?) {
+ // JGit's FileSnapshot writes thousands of lines per sync, pushing the rest out of the buffer.
+ if (name.endsWith(".FileSnapshot")) return
logcat(name) { message.fix().format(*args) + (t?.asLog() ?: "") }
}
diff --git a/app/src/main/java/app/passwordstore/util/services/ClipboardService.kt b/app/src/main/java/app/passwordstore/util/services/ClipboardService.kt
index b4bb0438..3347da78 100644
--- a/app/src/main/java/app/passwordstore/util/services/ClipboardService.kt
+++ b/app/src/main/java/app/passwordstore/util/services/ClipboardService.kt
@@ -12,7 +12,6 @@ import android.app.PendingIntent
import android.app.Service
import android.content.ClipData
import android.content.Intent
-import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.content.getSystemService
@@ -121,11 +120,7 @@ class ClipboardService : Service() {
this,
0,
clearIntent,
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
- } else {
- PendingIntent.FLAG_UPDATE_CURRENT
- },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,
)
val notification = createNotification(pendingIntent, clearTimeMs)
diff --git a/app/src/main/java/app/passwordstore/util/services/OreoAutofillService.kt b/app/src/main/java/app/passwordstore/util/services/OreoAutofillService.kt
index beeb4be7..a8b525d5 100644
--- a/app/src/main/java/app/passwordstore/util/services/OreoAutofillService.kt
+++ b/app/src/main/java/app/passwordstore/util/services/OreoAutofillService.kt
@@ -5,7 +5,6 @@
package app.passwordstore.util.services
import android.content.Context
-import android.os.Build
import android.os.CancellationSignal
import android.service.autofill.AutofillService
import android.service.autofill.FillCallback
@@ -74,16 +73,12 @@ class OreoAutofillService : AutofillService() {
return
}
if (structure.activityComponent.packageName in DENYLISTED_PACKAGES) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- callback.onSuccess(
- FillResponse.Builder().run {
- disableAutofill(DISABLE_AUTOFILL_DURATION_MS)
- build()
- }
- )
- } else {
- callback.onSuccess(null)
- }
+ callback.onSuccess(
+ FillResponse.Builder().run {
+ disableAutofill(DISABLE_AUTOFILL_DURATION_MS)
+ build()
+ }
+ )
return
}
val formToFill =
diff --git a/app/src/main/java/app/passwordstore/util/settings/Constants.kt b/app/src/main/java/app/passwordstore/util/settings/Constants.kt
index a0c4b038..2d5760e4 100644
--- a/app/src/main/java/app/passwordstore/util/settings/Constants.kt
+++ b/app/src/main/java/app/passwordstore/util/settings/Constants.kt
@@ -6,5 +6,5 @@
package app.passwordstore.util.settings
object Constants {
- const val DEFAULT_DECRYPTION_TIMEOUT = 60
+ const val DEFAULT_DECRYPTION_TIMEOUT = 45
}
diff --git a/app/src/main/java/app/passwordstore/util/settings/GitSettings.kt b/app/src/main/java/app/passwordstore/util/settings/GitSettings.kt
index 004028c4..0052f79c 100644
--- a/app/src/main/java/app/passwordstore/util/settings/GitSettings.kt
+++ b/app/src/main/java/app/passwordstore/util/settings/GitSettings.kt
@@ -125,6 +125,12 @@ constructor(
settings.edit { putBoolean(PreferenceKeys.REBASE_ON_PULL, value) }
}
+ var pushAfterCommit
+ get() = settings.getBoolean(PreferenceKeys.PUSH_AFTER_COMMIT, false)
+ set(value) {
+ settings.edit { putBoolean(PreferenceKeys.PUSH_AFTER_COMMIT, value) }
+ }
+
sealed class UpdateConnectionSettingsResult {
class MissingUsername(val newProtocol: Protocol) : UpdateConnectionSettingsResult()
diff --git a/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt b/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt
index ecc7a51c..9fd2658d 100644
--- a/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt
+++ b/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt
@@ -86,6 +86,7 @@ object PreferenceKeys {
const val PROXY_PASSWORD = "proxy_password"
const val REBASE_ON_PULL = "rebase_on_pull"
+ const val PUSH_AFTER_COMMIT = "push_after_commit"
const val DICEWARE_SEPARATOR = "diceware_separator"
const val DICEWARE_LENGTH = "diceware_length"
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
--- /dev/null
+++ b/app/src/main/res/drawable-nodpi/ic_kofi_symbol.webp
Binary files differ
diff --git a/app/src/main/res/drawable/ic_action_new_folder.xml b/app/src/main/res/drawable/ic_action_new_folder.xml
index 6db26eaf..7bccd8a6 100644
--- a/app/src/main/res/drawable/ic_action_new_folder.xml
+++ b/app/src/main/res/drawable/ic_action_new_folder.xml
@@ -6,7 +6,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:tint="?attr/colorControlNormal"
+ android:tint="?android:attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
diff --git a/app/src/main/res/drawable/ic_action_new_password.xml b/app/src/main/res/drawable/ic_action_new_password.xml
index 1b3df410..f204ea59 100644
--- a/app/src/main/res/drawable/ic_action_new_password.xml
+++ b/app/src/main/res/drawable/ic_action_new_password.xml
@@ -6,7 +6,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:tint="?attr/colorControlNormal"
+ android:tint="?android:attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
diff --git a/app/src/main/res/drawable/ic_action_secure_24dp.xml b/app/src/main/res/drawable/ic_action_secure_24dp.xml
index 9eb00ee4..5a62a473 100644
--- a/app/src/main/res/drawable/ic_action_secure_24dp.xml
+++ b/app/src/main/res/drawable/ic_action_secure_24dp.xml
@@ -6,7 +6,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:tint="?attr/colorControlNormal"
+ android:tint="?android:attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
diff --git a/app/src/main/res/drawable/ic_delete_24dp.xml b/app/src/main/res/drawable/ic_delete_24dp.xml
index 27865f8d..151f808e 100644
--- a/app/src/main/res/drawable/ic_delete_24dp.xml
+++ b/app/src/main/res/drawable/ic_delete_24dp.xml
@@ -6,7 +6,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:tint="?attr/colorControlNormal"
+ android:tint="?android:attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
diff --git a/app/src/main/res/drawable/ic_edit_24dp.xml b/app/src/main/res/drawable/ic_edit_24dp.xml
index 8bcfb841..b5f43cfd 100644
--- a/app/src/main/res/drawable/ic_edit_24dp.xml
+++ b/app/src/main/res/drawable/ic_edit_24dp.xml
@@ -6,7 +6,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:tint="?attr/colorControlNormal"
+ android:tint="?android:attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
index e83fc4a3..cd894791 100644
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -9,6 +9,6 @@
android:viewportWidth="432"
android:viewportHeight="432">
<path
- android:fillColor="#00375E"
+ android:fillColor="#2A2F38"
android:pathData="M0,0h432v432h-432z" />
</vector>
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
index 550a5531..04ef7ff9 100644
--- a/app/src/main/res/drawable/ic_launcher_foreground.xml
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -1,63 +1,18 @@
<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
+ ~ Copyright (C) 2026 Danilo M. <danix@danix.xyz>
~ SPDX-License-Identifier: GPL-3.0-only
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
- android:viewportWidth="432"
- android:viewportHeight="432">
- <path android:pathData="M128.18,195.79C126.46,181.89 137.12,169.6 150.91,169.6H281.09C294.91,169.6 305.59,181.95 303.8,195.88L292.58,283.72C291.09,295.32 281.37,304 269.86,304H161.8C150.26,304 140.51,295.27 139.07,283.63L128.18,195.79Z">
- <aapt:attr name="android:fillColor">
- <gradient
- android:endX="216"
- android:endY="304"
- android:startX="216"
- android:startY="169.6"
- android:type="linear">
- <item
- android:color="#FF2AE9DD"
- android:offset="0" />
- <item
- android:color="#FF0BAFAF"
- android:offset="1" />
- </gradient>
- </aapt:attr>
- </path>
- <path android:pathData="M245.33,169.6V160C245.33,144.09 232.2,131.2 216,131.2C199.8,131.2 186.67,144.09 186.67,160V169.6H167.11V160C167.11,133.49 189,112 216,112C243,112 264.89,133.49 264.89,160V169.6H245.33Z">
- <aapt:attr name="android:fillColor">
- <gradient
- android:centerX="216"
- android:centerY="169.6"
- android:gradientRadius="33.6"
- android:type="radial">
- <item
- android:color="#FF0086D1"
- android:offset="0.24" />
- <item
- android:color="#FF77E7FF"
- android:offset="0.75" />
- </gradient>
- </aapt:attr>
- </path>
+ android:viewportWidth="108"
+ android:viewportHeight="108">
<path
- android:fillColor="#77E7FF"
- android:pathData="M245.33,169.6h19.56v38.4h-19.56z" />
+ android:fillColor="#FF7A45"
+ android:pathData="M39,42V36a15,15 0 0 1 30,0v6h-6V36a9,9 0 0 0 -18,0v6Z" />
<path
- android:fillColor="#77E7FF"
- android:pathData="M245.33,217.6h19.56v48h-19.56z" />
- <path
- android:fillColor="#77E7FF"
- android:pathData="M239.11,205.19l13.83,13.58l-34.57,33.94l-13.83,-13.58z" />
- <path
- android:fillColor="#D1FEFF"
- android:pathData="M235.56,208a19.56,19.2 0,1 0,39.11 0a19.56,19.2 0,1 0,-39.11 0z" />
- <path
- android:fillColor="#D1FEFF"
- android:pathData="M235.56,275.2a19.56,19.2 0,1 0,39.11 0a19.56,19.2 0,1 0,-39.11 0z" />
- <path
- android:fillColor="#D1FEFF"
- android:pathData="M204.54,252.71m-13.83,-13.58a19.56,19.2 0,1 1,27.66 27.15a19.56,19.2 0,1 1,-27.66 -27.15" />
+ android:fillColor="#B8C4CF"
+ android:fillType="evenOdd"
+ android:pathData="M32,40H76V58C76,71 66,80 54,86C42,80 32,71 32,58ZM61.800,54.233L61.800,67.767A4.6,4.6 0 1 1 58.200,67.767L58.200,58.102L50.568,62.463A4.6,4.6 0 1 1 48.782,59.337L57.932,54.109A4.6,4.6 0 1 1 61.800,54.233Z" />
</vector>
diff --git a/app/src/main/res/drawable/ic_launcher_foreground_monochrome.xml b/app/src/main/res/drawable/ic_launcher_foreground_monochrome.xml
index 74af9a81..ce263547 100644
--- a/app/src/main/res/drawable/ic_launcher_foreground_monochrome.xml
+++ b/app/src/main/res/drawable/ic_launcher_foreground_monochrome.xml
@@ -1,14 +1,18 @@
-<!--
- ~ Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2026 Danilo M. <danix@danix.xyz>
~ SPDX-License-Identifier: GPL-3.0-only
-->
-<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
- android:viewportWidth="108"
- android:viewportHeight="108"
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
- android:height="108dp">
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M39,42V36a15,15 0 0 1 30,0v6h-6V36a9,9 0 0 0 -18,0v6Z" />
<path
- android:pathData="M44.556 46.3997L44.556 44.5001C44.556 39.2533 48.7844 35 54.0005 35C59.2165 35 63.4449 39.2533 63.4449 44.5001L63.4444 46.4001L66.5738 46.3997C69.2445 46.3997 71.3066 48.8438 70.9624 51.6012L68.7933 68.9857C68.5068 71.2819 66.6283 73 64.4045 73L43.5297 73C41.2994 73 39.4177 71.272 39.1389 68.9681L37.0354 51.5836C36.7024 48.8318 38.7625 46.3997 41.4263 46.3997L44.556 46.3997ZM59.6671 44.5001C59.6671 41.3521 57.1301 38.8 54.0005 38.8C50.8708 38.8 48.3338 41.3521 48.3338 44.5001L48.3338 46.3997L59.6671 46.4001L59.6667 50.7076C58.5375 51.3647 57.7778 52.5928 57.7778 53.9993C57.7778 54.0436 57.7785 54.0877 57.78 54.1316L52.765 59.1762C51.5047 58.8377 50.1043 59.1658 49.1156 60.1603C47.6403 61.6443 47.6403 64.0504 49.1156 65.5344C50.5909 67.0184 52.9829 67.0184 54.4582 65.5344C55.447 64.5397 55.7731 63.1309 55.4364 61.8631L59.6667 57.6079L59.6667 64.0078C58.5375 64.6648 57.7778 65.8929 57.7778 67.2995C57.7778 69.3982 59.4691 71.0995 61.5556 71.0995C63.642 71.0995 65.3333 69.3982 65.3333 67.2995C65.3333 65.8929 64.5736 64.6648 63.4444 64.0078L63.4444 57.291C64.5736 56.6339 65.3333 55.4059 65.3333 53.9993C65.3333 52.5928 64.5736 51.3647 63.4444 50.7076L63.4444 46.4001L59.6671 46.4001L59.6671 44.5001Z"
- android:fillColor="#FFFFFF" />
+ android:fillColor="#FFFFFF"
+ android:fillType="evenOdd"
+ android:pathData="M32,40H76V58C76,71 66,80 54,86C42,80 32,71 32,58ZM61.800,54.233L61.800,67.767A4.6,4.6 0 1 1 58.200,67.767L58.200,58.102L50.568,62.463A4.6,4.6 0 1 1 48.782,59.337L57.932,54.109A4.6,4.6 0 1 1 61.800,54.233Z" />
</vector>
diff --git a/app/src/main/res/drawable/ic_move_folder_24dp.xml b/app/src/main/res/drawable/ic_move_folder_24dp.xml
index 3df87291..e12ddaee 100644
--- a/app/src/main/res/drawable/ic_move_folder_24dp.xml
+++ b/app/src/main/res/drawable/ic_move_folder_24dp.xml
@@ -6,7 +6,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:tint="?attr/colorControlNormal"
+ android:tint="?android:attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
diff --git a/app/src/main/res/drawable/ic_open_in_new_24dp.xml b/app/src/main/res/drawable/ic_open_in_new_24dp.xml
new file mode 100644
index 00000000..b82ebe62
--- /dev/null
+++ b/app/src/main/res/drawable/ic_open_in_new_24dp.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:tint="?android:attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M19,19H5V5h7V3H5c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41V10h2V3h-7z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_push_pin_24dp.xml b/app/src/main/res/drawable/ic_push_pin_24dp.xml
index c542d784..41f61e0a 100644
--- a/app/src/main/res/drawable/ic_push_pin_24dp.xml
+++ b/app/src/main/res/drawable/ic_push_pin_24dp.xml
@@ -6,7 +6,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:tint="?attr/colorControlNormal"
+ android:tint="?android:attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
diff --git a/app/src/main/res/drawable/ic_round_import_export.xml b/app/src/main/res/drawable/ic_round_import_export.xml
index 2f243b22..7ab2aa1c 100644
--- a/app/src/main/res/drawable/ic_round_import_export.xml
+++ b/app/src/main/res/drawable/ic_round_import_export.xml
@@ -6,7 +6,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:tint="?attr/colorControlNormal"
+ android:tint="?android:attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
diff --git a/app/src/main/res/drawable/ic_search_24dp.xml b/app/src/main/res/drawable/ic_search_24dp.xml
index bb454f5b..666883b6 100644
--- a/app/src/main/res/drawable/ic_search_24dp.xml
+++ b/app/src/main/res/drawable/ic_search_24dp.xml
@@ -6,7 +6,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:tint="?attr/colorControlNormal"
+ android:tint="?android:attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
diff --git a/app/src/main/res/drawable/ic_share_24dp.xml b/app/src/main/res/drawable/ic_share_24dp.xml
index 2295122c..83d28c8d 100644
--- a/app/src/main/res/drawable/ic_share_24dp.xml
+++ b/app/src/main/res/drawable/ic_share_24dp.xml
@@ -6,7 +6,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:tint="?attr/colorControlNormal"
+ android:tint="?android:attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
diff --git a/app/src/main/res/layout/git_credential_layout.xml b/app/src/main/res/layout/git_credential_layout.xml
deleted file mode 100644
index 8fc527e9..00000000
--- a/app/src/main/res/layout/git_credential_layout.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- ~ SPDX-License-Identifier: GPL-3.0-only
- -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:padding="16dp">
-
- <com.google.android.material.textfield.TextInputLayout
- android:id="@+id/git_auth_passphrase_layout"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:endIconMode="password_toggle"
- app:errorEnabled="true"
- app:hintEnabled="true">
-
- <com.google.android.material.textfield.TextInputEditText
- android:id="@+id/git_auth_credential"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:hint="@string/ssh_keygen_passphrase"
- android:inputType="textPassword" />
-
- <requestFocus />
- </com.google.android.material.textfield.TextInputLayout>
-
- <com.google.android.material.checkbox.MaterialCheckBox
- android:id="@+id/git_auth_remember_credential"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/git_operation_remember_passphrase"
- app:checkedState="checked" />
-</LinearLayout>
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index a1133420..00000000
--- a/app/src/main/res/mipmap-hdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index b9e28b18..00000000
--- a/app/src/main/res/mipmap-mdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 353fe9c4..00000000
--- a/app/src/main/res/mipmap-xhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 148db12b..00000000
--- a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 2c1e2b05..00000000
--- a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index a0a20dc7..7baf4113 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -37,7 +37,6 @@
<string name="git_commit_move_multiple_text">Verschiebe mehrere Passwörter nach %1$s.</string>
<string name="git_commit_gpg_id">Initialisiere PGP-IDs in %1$s.</string>
<!-- PGPHandler -->
- <string name="clipboard_copied_text">In die Zwischenablage kopiert</string>
<string name="file_toast_text">Bitte setzen Sie einen Dateinamen.</string>
<string name="empty_toast_text">Sie können kein leeres Passwort oder leere Extra-Angaben setzen.</string>
<!-- Git Async Task -->
@@ -232,7 +231,6 @@
<string name="pref_app_theme_title">App-Farbthema</string>
<string name="pref_app_theme_value_light">Hell</string>
<string name="pref_app_theme_value_dark">Dunkel</string>
- <string name="pref_app_theme_value_battery_saver">Durch Energiesparmodus gesetzt</string>
<string name="pref_app_theme_value_follow_system">Systemstandard</string>
<string name="connection_mode_ssh_key">SSH-Schlüssel</string>
<string name="connection_mode_basic_authentication">Passwort</string>
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index d4d4d424..57be2b30 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -36,7 +36,6 @@
<string name="git_commit_move_text">Renommer %1$sà %2$s. </string>
<string name="git_commit_move_multiple_text">Déplacement de mots de passe vers %1$s.</string>
<!-- PGPHandler -->
- <string name="clipboard_copied_text">Copié dans le presse-papiers</string>
<string name="file_toast_text">Veuillez fournir un nom de fichier</string>
<string name="empty_toast_text">Vous ne pouvez pas utiliser un mot de passe vide ou des données supplémentaires vide</string>
<!-- Git Async Task -->
@@ -220,7 +219,6 @@
<string name="pref_app_theme_title">Thème de l\'application</string>
<string name="pref_app_theme_value_light">Clair</string>
<string name="pref_app_theme_value_dark">Sombre</string>
- <string name="pref_app_theme_value_battery_saver">Défini par l\'économiseur de batterie</string>
<string name="pref_app_theme_value_follow_system">Valeur par défaut du système</string>
<string name="connection_mode_ssh_key">Clé SSH</string>
<string name="connection_mode_basic_authentication">Mot de passe</string>
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index f966230c..b7a59428 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -37,7 +37,6 @@
<string name="git_commit_move_multiple_text">Mover varios contrasinais a %1$s.</string>
<string name="git_commit_gpg_id">Iniciar cos IDs GPG en %1$s.</string>
<!-- PGPHandler -->
- <string name="clipboard_copied_text">Copiado ao portapapeis</string>
<string name="file_toast_text">Debes proporcionar un nome de ficheiro</string>
<string name="empty_toast_text">Non podes deixar baleiro o contrasinal ou o contido extra</string>
<!-- Git Async Task -->
@@ -234,7 +233,6 @@ a app desde unha fonte de confianza, como a Play Store, Amazon Appstore, F-Droid
<string name="pref_app_theme_title">Decorado da App</string>
<string name="pref_app_theme_value_light">Claro</string>
<string name="pref_app_theme_value_dark">Escuro</string>
- <string name="pref_app_theme_value_battery_saver">Establecido polo Aforrador de enerxía</string>
<string name="pref_app_theme_value_follow_system">Por omisión do sistema</string>
<string name="connection_mode_ssh_key">Chave SSH</string>
<string name="connection_mode_basic_authentication">Contrasinal</string>
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index e7c05909..fed8adec 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -36,7 +36,6 @@
<string name="git_commit_move_text">Rinomina %1$s in %2$s.</string>
<string name="git_commit_move_multiple_text">Sposta più password in %1$s.</string>
<!-- PGPHandler -->
- <string name="clipboard_copied_text">Copiato negli appunti</string>
<string name="file_toast_text">Inserisci un nome per il file</string>
<string name="empty_toast_text">Non puoi usare una password vuota o dei contenuti extra vuoti</string>
<!-- Git Async Task -->
@@ -215,7 +214,6 @@
<string name="pref_app_theme_title">Tema dell\'App</string>
<string name="pref_app_theme_value_light">Chiaro</string>
<string name="pref_app_theme_value_dark">Scuro</string>
- <string name="pref_app_theme_value_battery_saver">Impostato dal Risparmio Energetico</string>
<string name="pref_app_theme_value_follow_system">Predefinito del sistema</string>
<string name="connection_mode_ssh_key">Chiave SSH</string>
<string name="connection_mode_basic_authentication">Password</string>
@@ -290,6 +288,13 @@
<string name="pref_rebase_on_pull_title">Rebase al pull</string>
<string name="pref_rebase_on_pull_summary">Durante pull o sincronizzazione, crea un merge commit con le modifiche del remote</string>
<string name="pref_rebase_on_pull_summary_on">Durante pull o sincronizzazione, esegui il rebase dei commit che non sono sul remote</string>
+ <string name="pref_export_log_title">Esporta log</string>
+ <string name="pref_export_log_summary">Salva in un file i log di questa app. Possono mostrare nomi delle voci, siti, app e il tuo server git: leggi il file prima di condividerlo</string>
+ <string name="log_exported">Log salvato</string>
+ <string name="log_export_failed">Impossibile salvare il log</string>
+ <string name="pref_push_after_commit_title">Push dopo ogni modifica</string>
+ <string name="pref_push_after_commit_summary">Esegui il push sul repository remoto dopo aver salvato, spostato o eliminato nell\'app</string>
+ <string name="git_push_after_commit_failed">Salvato sul dispositivo, ma push non riuscito. Sincronizza per riprovare</string>
<string name="pref_disable_sync_on_pull_title">Disattiva la sincronizzazione trascinando</string>
<string name="pref_disable_sync_on_pull_summary">Trascinare verso il basso l\'elenco delle password non avvia una sincronizzazione Git</string>
<string name="pref_import_pgp_key_title">Importa chiave PGP</string>
@@ -381,4 +386,20 @@
<string name="git_reset_confirm_message">I commit e le modifiche locali che non sono su questo remote branch vengono scartati.</string>
<string name="git_tool_done">%1$s: completato</string>
<string name="store_delete_confirm">Eliminare l\'archivio locale? Le voci di cui non è stato fatto il push sul remote vanno perse.</string>
+ <string name="about">Informazioni</string>
+ <string name="about_summary">Un client Android per pass, il gestore di password standard di Unix. Un fork mantenuto di Android Password Store.</string>
+ <string name="about_version">Versione</string>
+ <string name="about_docs">Documentazione</string>
+ <string name="about_changelog">Novità</string>
+ <string name="about_source">Codice sorgente</string>
+ <string name="about_issues">Segnala un problema</string>
+ <string name="about_support">Sostieni su Ko-fi</string>
+ <string name="about_privacy_title">Privacy</string>
+ <string name="about_privacy">Nessuna telemetria, nessuna analisi, nessun servizio Google. HardenedPass si collega solo al tuo server git, tramite il tuo proxy se ne imposti uno.</string>
+ <string name="about_certificate">Certificato di firma (SHA-256)</string>
+ <string name="about_license">Licenza</string>
+ <string name="about_license_text">GNU General Public License v3.0 only. Copyright degli autori di Android Password Store e dei contributori di HardenedPass.</string>
+ <string name="about_libraries">Licenze open source</string>
+ <string name="about_opens_browser">Si apre nel browser</string>
+ <string name="about_no_browser">Nessun browser trovato per aprire questo link</string>
</resources>
diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml
index add3de87..f6535e05 100644
--- a/app/src/main/res/values-ko-rKR/strings.xml
+++ b/app/src/main/res/values-ko-rKR/strings.xml
@@ -35,7 +35,6 @@
<string name="git_commit_move_multiple_text">여러개의 비밀번호를 %1$s 로 옮겼습니다.</string>
<string name="git_commit_gpg_id">%1$s의 GPG ID를 초기화 합니다.</string>
<!-- PGPHandler -->
- <string name="clipboard_copied_text">클립보드로 복사됨</string>
<string name="file_toast_text">파일 이름이 필요합니다</string>
<string name="empty_toast_text">비어있는 비밀번호나 추가 정보를 사용할 수 없습니다</string>
<!-- Git Async Task -->
@@ -228,7 +227,6 @@
<string name="pref_app_theme_title">앱 테마</string>
<string name="pref_app_theme_value_light">밝은 테마</string>
<string name="pref_app_theme_value_dark">어두운 테마</string>
- <string name="pref_app_theme_value_battery_saver">배터리 절약모드 설정</string>
<string name="pref_app_theme_value_follow_system">시스템 기본값</string>
<string name="connection_mode_ssh_key">SSH 키</string>
<string name="connection_mode_basic_authentication">비밀번호</string>
diff --git a/app/src/main/res/values-night/themes_material3.xml b/app/src/main/res/values-night/themes_material3.xml
index 548afa5f..22b62088 100644
--- a/app/src/main/res/values-night/themes_material3.xml
+++ b/app/src/main/res/values-night/themes_material3.xml
@@ -5,32 +5,7 @@
<resources>
- <style name="APSThemeM3" parent="Theme.Material3.DayNight">
- <item name="colorPrimary">@color/md_theme_dark_primary</item>
- <item name="colorOnPrimary">@color/md_theme_dark_onPrimary</item>
- <item name="colorPrimaryContainer">@color/md_theme_dark_primaryContainer</item>
- <item name="colorOnPrimaryContainer">@color/md_theme_dark_onPrimaryContainer</item>
- <item name="colorSecondary">@color/md_theme_dark_secondary</item>
- <item name="colorOnSecondary">@color/md_theme_dark_onSecondary</item>
- <item name="colorSecondaryContainer">@color/md_theme_dark_secondaryContainer</item>
- <item name="colorOnSecondaryContainer">@color/md_theme_dark_onSecondaryContainer</item>
- <item name="colorTertiary">@color/md_theme_dark_tertiary</item>
- <item name="colorOnTertiary">@color/md_theme_dark_onTertiary</item>
- <item name="colorTertiaryContainer">@color/md_theme_dark_tertiaryContainer</item>
- <item name="colorOnTertiaryContainer">@color/md_theme_dark_onTertiaryContainer</item>
- <item name="colorError">@color/md_theme_dark_error</item>
- <item name="colorErrorContainer">@color/md_theme_dark_errorContainer</item>
- <item name="colorOnError">@color/md_theme_dark_onError</item>
- <item name="colorOnErrorContainer">@color/md_theme_dark_onErrorContainer</item>
- <item name="android:colorBackground">@color/md_theme_dark_background</item>
- <item name="colorOnBackground">@color/md_theme_dark_onBackground</item>
- <item name="colorSurface">@color/md_theme_dark_surface</item>
- <item name="colorOnSurface">@color/md_theme_dark_onSurface</item>
- <item name="colorSurfaceVariant">@color/md_theme_dark_surfaceVariant</item>
- <item name="colorOnSurfaceVariant">@color/md_theme_dark_onSurfaceVariant</item>
- <item name="colorOutline">@color/md_theme_dark_outline</item>
- <item name="colorOnSurfaceInverse">@color/md_theme_dark_inverseOnSurface</item>
- <item name="colorSurfaceInverse">@color/md_theme_dark_inverseSurface</item>
- <item name="colorPrimaryInverse">@color/md_theme_dark_primaryInverse</item>
+ <style name="APSThemeM3" parent="android:Theme.Material.NoActionBar">
+ <item name="android:colorBackground">@android:color/system_neutral2_900</item>
</style>
</resources>
diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml
index 4d41b2f6..d130bc70 100644
--- a/app/src/main/res/values-pl-rPL/strings.xml
+++ b/app/src/main/res/values-pl-rPL/strings.xml
@@ -35,7 +35,6 @@
<string name="git_commit_move_text">Zmień nazwę %1$s na %2$s.</string>
<string name="git_commit_move_multiple_text">Przenieś hasła do %1$s.</string>
<!-- PGPHandler -->
- <string name="clipboard_copied_text">Skopiowano do schowka</string>
<string name="file_toast_text">Podaj nazwę pliku</string>
<string name="empty_toast_text">Nie możesz użyć pustego hasła lub pustej dodatkowej zawartości</string>
<!-- Git Async Task -->
@@ -227,7 +226,6 @@
<string name="pref_app_theme_title">Motyw aplikacji</string>
<string name="pref_app_theme_value_light">Jasny</string>
<string name="pref_app_theme_value_dark">Ciemny</string>
- <string name="pref_app_theme_value_battery_saver">Ustawiany przez funkcję oszczędzania energii</string>
<string name="pref_app_theme_value_follow_system">Domyślny</string>
<string name="connection_mode_ssh_key">Klucz SSH</string>
<string name="connection_mode_basic_authentication">Hasło</string>
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 14e862c5..4ee08b50 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -37,7 +37,6 @@
<string name="git_commit_move_multiple_text">Mova múltiplas senhas para %1$s.</string>
<string name="git_commit_gpg_id">Inicializar IDs GPG em %1$s.</string>
<!-- PGPHandler -->
- <string name="clipboard_copied_text">Copiado para a área de transferência</string>
<string name="file_toast_text">Por favor, informe um nome de arquivo</string>
<string name="empty_toast_text">Você não pode usar uma senha vazia ou conteúdo extra vazio</string>
<!-- Git Async Task -->
@@ -232,7 +231,6 @@
<string name="pref_app_theme_title">Tema do aplicativo</string>
<string name="pref_app_theme_value_light">Claro</string>
<string name="pref_app_theme_value_dark">Escuro</string>
- <string name="pref_app_theme_value_battery_saver">Configurado pela Economia de bateria</string>
<string name="pref_app_theme_value_follow_system">Padrão do sistema</string>
<string name="connection_mode_ssh_key">Chave SSH</string>
<string name="connection_mode_basic_authentication">Senha</string>
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index ddc33aa3..a966640d 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -41,7 +41,6 @@
<string name="git_commit_move_multiple_text">Переместить несколько паролей в %1$s.</string>
<string name="git_commit_gpg_id">Инициализация GPG ID через %1$s.</string>
<!-- PGPHandler -->
- <string name="clipboard_copied_text">Скопировано в буфер обмена</string>
<string name="file_toast_text">Пожалуйста, укажите имя файла</string>
<string name="empty_toast_text">Вы не можете использовать пустой пароль или пустое поле информации</string>
<!-- Git Async Task -->
@@ -225,7 +224,6 @@
<string name="pref_app_theme_title">Тема приложения</string>
<string name="pref_app_theme_value_light">Светлая</string>
<string name="pref_app_theme_value_dark">Темная</string>
- <string name="pref_app_theme_value_battery_saver">Задается экономией батареи</string>
<string name="pref_app_theme_value_follow_system">Системная</string>
<string name="connection_mode_ssh_key">SSH ключ</string>
<string name="connection_mode_basic_authentication">Пароль</string>
diff --git a/app/src/main/res/values-v29/arrays.xml b/app/src/main/res/values-v29/arrays.xml
deleted file mode 100644
index aa78032c..00000000
--- a/app/src/main/res/values-v29/arrays.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- ~ SPDX-License-Identifier: GPL-3.0-only
- -->
-
-<resources>
- <string-array name="app_theme_options">
- <item>@string/pref_app_theme_value_light</item>
- <item>@string/pref_app_theme_value_dark</item>
- <item>@string/pref_app_theme_value_follow_system</item>
- </string-array>
- <string-array name="app_theme_values">
- <item>light</item>
- <item>dark</item>
- <item>follow_system</item>
- </string-array>
-</resources>
diff --git a/app/src/main/res/values-v29/prefs.xml b/app/src/main/res/values-v29/prefs.xml
deleted file mode 100644
index 383b5f8b..00000000
--- a/app/src/main/res/values-v29/prefs.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- ~ SPDX-License-Identifier: GPL-3.0-only
- -->
-
-<resources>
- <string name="app_theme_def" translatable="false">follow_system</string>
-</resources>
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 3f6abeb3..dae54d05 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -36,7 +36,6 @@
<string name="git_commit_move_multiple_text">将多个密码移至%1$s</string>
<string name="git_commit_gpg_id">初始化 %1$s 中的 GPG ID</string>
<!-- PGPHandler -->
- <string name="clipboard_copied_text">复制到剪切板</string>
<string name="file_toast_text">请提供文件名</string>
<string name="empty_toast_text">您不能使用空密码或空额外内容</string>
<!-- Git Async Task -->
@@ -233,7 +232,6 @@
<string name="pref_app_theme_title">应用主题</string>
<string name="pref_app_theme_value_light">浅色</string>
<string name="pref_app_theme_value_dark">深色</string>
- <string name="pref_app_theme_value_battery_saver">由省电模式设定</string>
<string name="pref_app_theme_value_follow_system">系统默认</string>
<string name="connection_mode_ssh_key">SSH密钥</string>
<string name="connection_mode_basic_authentication">密码</string>
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 2891987d..dde1a99f 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -33,12 +33,12 @@
<string-array name="app_theme_options">
<item>@string/pref_app_theme_value_light</item>
<item>@string/pref_app_theme_value_dark</item>
- <item>@string/pref_app_theme_value_battery_saver</item>
+ <item>@string/pref_app_theme_value_follow_system</item>
</string-array>
<string-array name="app_theme_values">
<item>light</item>
<item>dark</item>
- <item>battery_saver</item>
+ <item>follow_system</item>
</string-array>
</resources>
diff --git a/app/src/main/res/values/colors_material3.xml b/app/src/main/res/values/colors_material3.xml
index ecd34921..47ffbc6a 100644
--- a/app/src/main/res/values/colors_material3.xml
+++ b/app/src/main/res/values/colors_material3.xml
@@ -5,59 +5,5 @@
<resources>
<color name="md_theme_light_primary">#006591</color>
- <color name="md_theme_light_onPrimary">#FFFFFF</color>
- <color name="md_theme_light_primaryContainer">#C7E6FF</color>
- <color name="md_theme_light_onPrimaryContainer">#001E30</color>
- <color name="md_theme_light_secondary">#4F606E</color>
- <color name="md_theme_light_onSecondary">#FFFFFF</color>
- <color name="md_theme_light_secondaryContainer">#D3E5F5</color>
- <color name="md_theme_light_onSecondaryContainer">#0B1D29</color>
- <color name="md_theme_light_tertiary">#006494</color>
- <color name="md_theme_light_onTertiary">#FFFFFF</color>
- <color name="md_theme_light_tertiaryContainer">#C8E6FF</color>
- <color name="md_theme_light_onTertiaryContainer">#001E31</color>
- <color name="md_theme_light_error">#BA1B1B</color>
- <color name="md_theme_light_errorContainer">#FFDAD4</color>
- <color name="md_theme_light_onError">#FFFFFF</color>
- <color name="md_theme_light_onErrorContainer">#410001</color>
- <color name="md_theme_light_background">#FCFCFF</color>
- <color name="md_theme_light_onBackground">#1A1C1E</color>
- <color name="md_theme_light_surface">#FCFCFF</color>
- <color name="md_theme_light_onSurface">#1A1C1E</color>
- <color name="md_theme_light_surfaceVariant">#DDE3EA</color>
- <color name="md_theme_light_onSurfaceVariant">#41474D</color>
- <color name="md_theme_light_outline">#72787E</color>
- <color name="md_theme_light_inverseOnSurface">#F0F0F3</color>
- <color name="md_theme_light_inverseSurface">#2E3133</color>
- <color name="md_theme_light_primaryInverse">#85CEFF</color>
- <color name="md_theme_dark_primary">#85CEFF</color>
- <color name="md_theme_dark_onPrimary">#00344E</color>
- <color name="md_theme_dark_primaryContainer">#004C6F</color>
- <color name="md_theme_dark_onPrimaryContainer">#C7E6FF</color>
- <color name="md_theme_dark_secondary">#B7C9D9</color>
- <color name="md_theme_dark_onSecondary">#21323E</color>
- <color name="md_theme_dark_secondaryContainer">#384956</color>
- <color name="md_theme_dark_onSecondaryContainer">#D3E5F5</color>
- <color name="md_theme_dark_tertiary">#8ACEFF</color>
- <color name="md_theme_dark_onTertiary">#003450</color>
- <color name="md_theme_dark_tertiaryContainer">#004B70</color>
- <color name="md_theme_dark_onTertiaryContainer">#C8E6FF</color>
- <color name="md_theme_dark_error">#FFB4A9</color>
- <color name="md_theme_dark_errorContainer">#930006</color>
- <color name="md_theme_dark_onError">#680003</color>
- <color name="md_theme_dark_onErrorContainer">#FFDAD4</color>
- <color name="md_theme_dark_background">#1A1C1E</color>
- <color name="md_theme_dark_onBackground">#E1E2E5</color>
- <color name="md_theme_dark_surface">#1A1C1E</color>
- <color name="md_theme_dark_onSurface">#E1E2E5</color>
- <color name="md_theme_dark_surfaceVariant">#41474D</color>
- <color name="md_theme_dark_onSurfaceVariant">#C1C7CE</color>
- <color name="md_theme_dark_outline">#8B9197</color>
- <color name="md_theme_dark_inverseOnSurface">#1A1C1E</color>
- <color name="md_theme_dark_inverseSurface">#E1E2E5</color>
- <color name="md_theme_dark_primaryInverse">#006591</color>
- <color name="seed">#003E5B</color>
- <color name="error">#BA1B1B</color>
- <!-- Multiselect color -->
</resources>
diff --git a/app/src/main/res/values/prefs.xml b/app/src/main/res/values/prefs.xml
index a5977d9d..383b5f8b 100644
--- a/app/src/main/res/values/prefs.xml
+++ b/app/src/main/res/values/prefs.xml
@@ -4,5 +4,5 @@
-->
<resources>
- <string name="app_theme_def" translatable="false">battery_saver</string>
+ <string name="app_theme_def" translatable="false">follow_system</string>
</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f1a858fd..a415e95f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -44,7 +44,6 @@
<string name="git_commit_gpg_id">Initialize GPG IDs in %1$s.</string>
<!-- PGPHandler -->
- <string name="clipboard_copied_text">Copied to clipboard</string>
<string name="file_toast_text">Please provide a file name</string>
<string name="empty_toast_text">You cannot use an empty password or empty extra content</string>
@@ -127,6 +126,13 @@
<string name="pref_rebase_on_pull_title">Rebase on pull</string>
<string name="pref_rebase_on_pull_summary">When pulling or syncing, create a merge commit with upstream changes</string>
<string name="pref_rebase_on_pull_summary_on">When pulling or syncing, rebase commits that are not present in the remote repository</string>
+ <string name="pref_export_log_title">Export log</string>
+ <string name="pref_export_log_summary">Save this app\'s logs to a file. They can show entry names, websites, apps and your git server, so read the file before sharing it</string>
+ <string name="log_exported">Log saved</string>
+ <string name="log_export_failed">Could not save the log</string>
+ <string name="pref_push_after_commit_title">Push after each change</string>
+ <string name="pref_push_after_commit_summary">Push to the remote repository after saving, moving or deleting in the app</string>
+ <string name="git_push_after_commit_failed">Saved on this device, but not pushed. Sync to try again</string>
<string name="pref_disable_sync_on_pull_title">Disable pull to sync action</string>
<string name="pref_disable_sync_on_pull_summary">Prevent pulling down on the password list from triggering a Git sync</string>
<string name="pref_import_pgp_key_title">Import PGP key</string>
@@ -263,7 +269,6 @@
<string name="pref_app_theme_title">App theme</string>
<string name="pref_app_theme_value_light">Light</string>
<string name="pref_app_theme_value_dark">Dark</string>
- <string name="pref_app_theme_value_battery_saver">Set by Battery Saver</string>
<string name="pref_app_theme_value_follow_system">System default</string>
<string name="connection_mode_ssh_key">SSH key</string>
<string name="connection_mode_basic_authentication">Password</string>
@@ -415,4 +420,20 @@
<string name="git_reset_confirm_message">Local commits and changes that are not on this remote branch are discarded.</string>
<string name="git_tool_done">%1$s: done</string>
<string name="store_delete_confirm">Delete the local store? Entries not pushed to the remote are lost.</string>
+ <string name="about">About</string>
+ <string name="about_summary">An Android client for pass, the standard Unix password manager. A maintained fork of Android Password Store.</string>
+ <string name="about_version">Version</string>
+ <string name="about_docs">Documentation</string>
+ <string name="about_changelog">What\'s new</string>
+ <string name="about_source">Source code</string>
+ <string name="about_issues">Report a problem</string>
+ <string name="about_support">Support on Ko-fi</string>
+ <string name="about_privacy_title">Privacy</string>
+ <string name="about_privacy">No telemetry, no analytics, no Google services. HardenedPass connects only to your git server, through your proxy if you set one.</string>
+ <string name="about_certificate">Signing certificate (SHA-256)</string>
+ <string name="about_license">License</string>
+ <string name="about_license_text">GNU General Public License v3.0 only. Copyright the Android Password Store authors and HardenedPass contributors.</string>
+ <string name="about_libraries">Open source licenses</string>
+ <string name="about_opens_browser">Opens in the browser</string>
+ <string name="about_no_browser">No browser found to open this link</string>
</resources>
diff --git a/app/src/main/res/values/themes_material3.xml b/app/src/main/res/values/themes_material3.xml
index 0ec74a45..7db01be0 100644
--- a/app/src/main/res/values/themes_material3.xml
+++ b/app/src/main/res/values/themes_material3.xml
@@ -6,18 +6,11 @@
<resources>
<style name="AppThemeM3" parent="APSThemeM3">
- <item name="fontFamily">@font/manrope</item>
<item name="android:windowLightStatusBar">@bool/light_status_bar</item>
<item name="android:statusBarColor">?android:colorBackground</item>
<item name="android:navigationBarColor">?android:colorBackground</item>
</style>
- <!-- AppCompat activities whose whole UI is Compose -->
- <style name="NoActionBarThemeM3" parent="AppThemeM3">
- <item name="windowActionBar">false</item>
- <item name="windowNoTitle">true</item>
- </style>
-
<style name="NoBackgroundThemeM3" parent="@style/AppThemeM3">
<item name="android:background">@android:color/transparent</item>
<item name="android:backgroundDimEnabled">true</item>
@@ -25,48 +18,17 @@
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentOverlay">@null</item>
- <item name="android:windowActionBar">false</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowEnterAnimation">@android:anim/fade_in</item>
<item name="android:windowExitAnimation">@android:anim/fade_out</item>
- <item name="colorPrimaryDark">@android:color/transparent</item>
- <item name="windowNoTitle">true</item>
</style>
<style name="DialogLikeThemeM3" parent="AppThemeM3">
- <item name="android:windowActionBar">false</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">true</item>
- <!-- Needs to be set without android: prefix as per https://stackoverflow.com/a/33417456 -->
- <item name="windowNoTitle">true</item>
</style>
- <style name="APSThemeM3" parent="Theme.Material3.DayNight">
- <item name="colorPrimary">@color/md_theme_light_primary</item>
- <item name="colorOnPrimary">@color/md_theme_light_onPrimary</item>
- <item name="colorPrimaryContainer">@color/md_theme_light_primaryContainer</item>
- <item name="colorOnPrimaryContainer">@color/md_theme_light_onPrimaryContainer</item>
- <item name="colorSecondary">@color/md_theme_light_secondary</item>
- <item name="colorOnSecondary">@color/md_theme_light_onSecondary</item>
- <item name="colorSecondaryContainer">@color/md_theme_light_secondaryContainer</item>
- <item name="colorOnSecondaryContainer">@color/md_theme_light_onSecondaryContainer</item>
- <item name="colorTertiary">@color/md_theme_light_tertiary</item>
- <item name="colorOnTertiary">@color/md_theme_light_onTertiary</item>
- <item name="colorTertiaryContainer">@color/md_theme_light_tertiaryContainer</item>
- <item name="colorOnTertiaryContainer">@color/md_theme_light_onTertiaryContainer</item>
- <item name="colorError">@color/md_theme_light_error</item>
- <item name="colorErrorContainer">@color/md_theme_light_errorContainer</item>
- <item name="colorOnError">@color/md_theme_light_onError</item>
- <item name="colorOnErrorContainer">@color/md_theme_light_onErrorContainer</item>
- <item name="android:colorBackground">@color/md_theme_light_background</item>
- <item name="colorOnBackground">@color/md_theme_light_onBackground</item>
- <item name="colorSurface">@color/md_theme_light_surface</item>
- <item name="colorOnSurface">@color/md_theme_light_onSurface</item>
- <item name="colorSurfaceVariant">@color/md_theme_light_surfaceVariant</item>
- <item name="colorOnSurfaceVariant">@color/md_theme_light_onSurfaceVariant</item>
- <item name="colorOutline">@color/md_theme_light_outline</item>
- <item name="colorOnSurfaceInverse">@color/md_theme_light_inverseOnSurface</item>
- <item name="colorSurfaceInverse">@color/md_theme_light_inverseSurface</item>
- <item name="colorPrimaryInverse">@color/md_theme_light_primaryInverse</item>
+ <style name="APSThemeM3" parent="android:Theme.Material.Light.NoActionBar">
+ <item name="android:colorBackground">@android:color/system_neutral2_10</item>
</style>
</resources>
diff --git a/app/src/test/java/app/passwordstore/ui/prompts/PromptsTest.kt b/app/src/test/java/app/passwordstore/ui/prompts/PromptsTest.kt
new file mode 100644
index 00000000..12d1671e
--- /dev/null
+++ b/app/src/test/java/app/passwordstore/ui/prompts/PromptsTest.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package app.passwordstore.ui.prompts
+
+import androidx.compose.material3.SnackbarDuration
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.async
+import kotlinx.coroutines.runBlocking
+
+class PromptsTest {
+
+ private fun Prompts.question() = current as Question
+
+ @Test
+ fun answerResumesAsk() = runBlocking {
+ val prompts = Prompts()
+ val asked = async(start = CoroutineStart.UNDISPATCHED) { prompts.ask("t", "m", "ok") }
+ prompts.answer(prompts.question(), Choice.Other)
+ assertEquals(Choice.Other, asked.await())
+ assertNull(prompts.current)
+ }
+
+ @Test
+ fun dismissingAnswersCancel() = runBlocking {
+ val prompts = Prompts()
+ val asked = async(start = CoroutineStart.UNDISPATCHED) { prompts.ask("t", "m", "ok") }
+ prompts.cancel(prompts.question())
+ assertEquals(Choice.Cancel, asked.await())
+ }
+
+ @Test
+ fun secondAnswerIsIgnored() = runBlocking {
+ val prompts = Prompts()
+ val asked = async(start = CoroutineStart.UNDISPATCHED) { prompts.ask("t", "m", "ok") }
+ val question = prompts.question()
+ prompts.answer(question, Choice.Confirm)
+ prompts.answer(question, Choice.Cancel)
+ assertEquals(Choice.Confirm, asked.await())
+ }
+
+ @Test
+ fun cancelledAskRemovesItsDialog() = runBlocking {
+ val prompts = Prompts()
+ val asked = async(start = CoroutineStart.UNDISPATCHED) { prompts.ask("t", "m", "ok") }
+ asked.cancel()
+ assertNull(prompts.current)
+ }
+
+ @Test
+ fun promptsShowOneAtATimeInOrder() =
+ runBlocking<Unit> {
+ val prompts = Prompts()
+ val first = async(start = CoroutineStart.UNDISPATCHED) { prompts.ask("1", "m", "ok") }
+ val second = async(start = CoroutineStart.UNDISPATCHED) { prompts.ask("2", "m", "ok") }
+ assertEquals("1", prompts.question().title)
+ prompts.answer(prompts.question(), Choice.Confirm)
+ assertEquals("2", prompts.question().title)
+ prompts.answer(prompts.question(), Choice.Confirm)
+ first.await()
+ second.await()
+ }
+
+ @Test
+ fun cancelAllAnswersAPendingCredentialWithNull() {
+ val prompts = Prompts()
+ val answers = mutableListOf<CredentialAnswer?>(CredentialAnswer("unset", false))
+ prompts.askCredential("m", "h", "r", null) { answers[0] = it }
+ prompts.cancelAll()
+ assertNull(answers[0])
+ assertNull(prompts.current)
+ }
+
+ @Test
+ fun askCredentialAfterCloseAnswersNullAtOnce() {
+ val prompts = Prompts()
+ prompts.close()
+ val answers = mutableListOf<CredentialAnswer?>(CredentialAnswer("unset", false))
+ prompts.askCredential("m", "h", "r", null) { answers[0] = it }
+ assertNull(answers[0])
+ assertNull(prompts.current)
+ }
+
+ @Test
+ fun newSnackbarReplacesTheCurrentOne() {
+ val prompts = Prompts()
+ val first = prompts.snackbar("one", SnackbarDuration.Indefinite)
+ val second = prompts.snackbar("two", SnackbarDuration.Short)
+ assertSame(second, prompts.snack)
+ // The finished operation dismisses its own snackbar only.
+ prompts.dismiss(first)
+ assertSame(second, prompts.snack)
+ prompts.dismiss(second)
+ assertNull(prompts.snack)
+ }
+}
diff --git a/app/src/test/java/app/passwordstore/ui/settings/AboutTest.kt b/app/src/test/java/app/passwordstore/ui/settings/AboutTest.kt
new file mode 100644
index 00000000..e692c229
--- /dev/null
+++ b/app/src/test/java/app/passwordstore/ui/settings/AboutTest.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package app.passwordstore.ui.settings
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class AboutTest {
+
+ @Test
+ fun versionLineWithoutCommit() {
+ assertEquals("0.3.0 (300)", versionLine("0.3.0", 300, ""))
+ }
+
+ @Test
+ fun versionLineWithCommit() {
+ assertEquals("0.3.0-debug (300) · daa7ee01", versionLine("0.3.0-debug", 300, "daa7ee01"))
+ }
+
+ @Test
+ fun changelogUrlPointsAtTheReleaseTag() {
+ assertEquals(
+ "https://github.com/danixland/HardenedPass/blob/v0.3.0/CHANGELOG.md",
+ changelogUrl("0.3.0-debug"),
+ )
+ }
+
+ @Test
+ fun fingerprintIsColonSeparatedUppercaseSha256() {
+ assertEquals(
+ "BA:78:16:BF:8F:01:CF:EA:41:41:40:DE:5D:AE:22:23:" +
+ "B0:03:61:A3:96:17:7A:9C:B4:10:FF:61:F2:00:15:AD",
+ certificateFingerprint("abc".encodeToByteArray()),
+ )
+ }
+}
diff --git a/app/src/test/java/app/passwordstore/util/settings/GitSettingsTest.kt b/app/src/test/java/app/passwordstore/util/settings/GitSettingsTest.kt
index 6b30c248..111e7f51 100644
--- a/app/src/test/java/app/passwordstore/util/settings/GitSettingsTest.kt
+++ b/app/src/test/java/app/passwordstore/util/settings/GitSettingsTest.kt
@@ -9,7 +9,9 @@ import app.passwordstore.util.extensions.getString
import com.github.ivanshafran.sharedpreferencesmock.SPMockBuilder
import kotlin.test.Test
import kotlin.test.assertEquals
+import kotlin.test.assertFalse
import kotlin.test.assertNull
+import kotlin.test.assertTrue
class GitSettingsTest {
@@ -24,4 +26,19 @@ class GitSettingsTest {
assertEquals("proxy-user", proxy.getString(PreferenceKeys.PROXY_USERNAME))
assertNull(settings.getString(PreferenceKeys.PROXY_USERNAME))
}
+
+ @Test
+ fun pushAfterCommitIsOptIn() {
+ val settings = SPMockBuilder().createSharedPreferences()
+ val gitSettings =
+ GitSettings(
+ settings,
+ SPMockBuilder().createSharedPreferences(),
+ SPMockBuilder().createSharedPreferences(),
+ "/nonexistent",
+ )
+ assertFalse(gitSettings.pushAfterCommit)
+ gitSettings.pushAfterCommit = true
+ assertTrue(settings.getBoolean(PreferenceKeys.PUSH_AFTER_COMMIT, false))
+ }
}
diff --git a/app/src/test/java/app/passwordstore/util/viewmodel/StrictDomainRegexTest.kt b/app/src/test/java/app/passwordstore/util/viewmodel/StrictDomainRegexTest.kt
index 0ddb72f8..59c1de05 100644
--- a/app/src/test/java/app/passwordstore/util/viewmodel/StrictDomainRegexTest.kt
+++ b/app/src/test/java/app/passwordstore/util/viewmodel/StrictDomainRegexTest.kt
@@ -10,13 +10,11 @@ import kotlin.test.assertNull
import kotlin.test.assertTrue
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
-import org.robolectric.annotation.Config
private infix fun String.matchedForDomain(domain: String) =
SearchableRepositoryViewModel.generateStrictDomainRegex(domain)?.containsMatchIn(this) == true
@RunWith(RobolectricTestRunner::class)
-@Config(sdk = [23])
class StrictDomainRegexTest {
@Test