aboutsummaryrefslogtreecommitdiff
path: root/app/src/main
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2022-01-09 17:04:16 +0530
committerGitHub <noreply@github.com>2022-01-09 17:04:16 +0530
commit1738879fb3ba1b2c2d0ca7933c861bf99ae32535 (patch)
tree42b339b07e3c45922310afd01e8ce357f99beeb6 /app/src/main
parent799f1393e49955d05f68b81af26d6cfaf9beadfd (diff)
Integrate PGPainless backend into the UI properly (#1647)
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/AndroidManifest.xml2
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/data/crypto/CryptoRepository.kt58
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/injection/crypto/KeyManagerModule.kt38
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt69
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt33
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt14
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordDialog.kt42
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/pgp/PGPKeyImportActivity.kt67
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/settings/PGPSettings.kt29
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt15
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt8
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/extensions/AndroidExtensions.kt6
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/extensions/Extensions.kt13
-rw-r--r--app/src/main/res/drawable/ic_password_24px.xml14
-rw-r--r--app/src/main/res/layout/dialog_password_entry.xml29
-rw-r--r--app/src/main/res/values/strings.xml4
16 files changed, 378 insertions, 63 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c3ff6302..239cb4f8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -158,6 +158,8 @@
android:configChanges="orientation|keyboardHidden"
android:theme="@style/DialogLikeThemeM3"
android:windowSoftInputMode="adjustNothing" />
+ <activity android:name=".ui.pgp.PGPKeyImportActivity"
+ android:theme="@style/NoBackgroundThemeM3" />
</application>
</manifest>
diff --git a/app/src/main/java/dev/msfjarvis/aps/data/crypto/CryptoRepository.kt b/app/src/main/java/dev/msfjarvis/aps/data/crypto/CryptoRepository.kt
new file mode 100644
index 00000000..b0cde380
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/data/crypto/CryptoRepository.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.data.crypto
+
+import com.github.michaelbull.result.runCatching
+import com.github.michaelbull.result.unwrap
+import dev.msfjarvis.aps.crypto.PGPKeyManager
+import dev.msfjarvis.aps.crypto.PGPainlessCryptoHandler
+import dev.msfjarvis.aps.util.extensions.isOk
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+class CryptoRepository
+@Inject
+constructor(
+ private val pgpKeyManager: PGPKeyManager,
+ private val pgpCryptoHandler: PGPainlessCryptoHandler,
+) {
+
+ suspend fun decrypt(
+ password: String,
+ message: ByteArrayInputStream,
+ out: ByteArrayOutputStream,
+ ) {
+ withContext(Dispatchers.IO) { decryptPgp(password, message, out) }
+ }
+
+ suspend fun encrypt(content: ByteArrayInputStream, out: ByteArrayOutputStream) {
+ withContext(Dispatchers.IO) { encryptPgp(content, out) }
+ }
+
+ private suspend fun decryptPgp(
+ password: String,
+ message: ByteArrayInputStream,
+ out: ByteArrayOutputStream,
+ ) {
+ val keys = pgpKeyManager.getAllKeys().unwrap()
+ // Iterates through the keys until the first successful decryption, then returns.
+ keys.first { key ->
+ runCatching { pgpCryptoHandler.decrypt(key, password, message, out) }.isOk()
+ }
+ }
+
+ private suspend fun encryptPgp(content: ByteArrayInputStream, out: ByteArrayOutputStream) {
+ val keys = pgpKeyManager.getAllKeys().unwrap()
+ pgpCryptoHandler.encrypt(
+ keys,
+ content,
+ out,
+ )
+ }
+}
diff --git a/app/src/main/java/dev/msfjarvis/aps/injection/crypto/KeyManagerModule.kt b/app/src/main/java/dev/msfjarvis/aps/injection/crypto/KeyManagerModule.kt
new file mode 100644
index 00000000..0bc1c43d
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/injection/crypto/KeyManagerModule.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.injection.crypto
+
+import android.content.Context
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import dev.msfjarvis.aps.crypto.PGPKeyManager
+import javax.inject.Qualifier
+import kotlinx.coroutines.Dispatchers
+
+@Module
+@InstallIn(SingletonComponent::class)
+object KeyManagerModule {
+ @Provides
+ fun providePGPKeyManager(
+ @PGPKeyDir keyDir: String,
+ ): PGPKeyManager {
+ return PGPKeyManager(
+ keyDir,
+ Dispatchers.IO,
+ )
+ }
+
+ @Provides
+ @PGPKeyDir
+ fun providePGPKeyDir(@ApplicationContext context: Context): String {
+ return context.filesDir.resolve("pgp_keys").absolutePath
+ }
+}
+
+@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class PGPKeyDir
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt
index 8f4578cf..697d628d 100644
--- a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt
@@ -21,10 +21,9 @@ import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.onSuccess
import com.github.michaelbull.result.runCatching
import dagger.hilt.android.AndroidEntryPoint
-import dev.msfjarvis.aps.crypto.Key
+import dev.msfjarvis.aps.data.crypto.CryptoRepository
import dev.msfjarvis.aps.data.passfile.PasswordEntry
-import dev.msfjarvis.aps.injection.crypto.CryptoSet
-import dev.msfjarvis.aps.ui.crypto.DecryptActivityV2
+import dev.msfjarvis.aps.ui.crypto.PasswordDialog
import dev.msfjarvis.aps.util.autofill.AutofillPreferences
import dev.msfjarvis.aps.util.autofill.AutofillResponseBuilder
import dev.msfjarvis.aps.util.autofill.DirectoryStructure
@@ -33,6 +32,7 @@ import java.io.ByteArrayOutputStream
import java.io.File
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import logcat.LogPriority.ERROR
@@ -74,7 +74,7 @@ class AutofillDecryptActivityV2 : AppCompatActivity() {
}
@Inject lateinit var passwordEntryFactory: PasswordEntry.Factory
- @Inject lateinit var cryptos: CryptoSet
+ @Inject lateinit var repository: CryptoRepository
private lateinit var directoryStructure: DirectoryStructure
@@ -98,43 +98,58 @@ class AutofillDecryptActivityV2 : AppCompatActivity() {
val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match
directoryStructure = AutofillPreferences.directoryStructure(this)
logcat { action.toString() }
+ val dialog = PasswordDialog()
lifecycleScope.launch {
- val credentials = decryptCredential(File(filePath))
- if (credentials == null) {
- setResult(RESULT_CANCELED)
- } else {
- val fillInDataset =
- AutofillResponseBuilder.makeFillInDataset(
- this@AutofillDecryptActivityV2,
- credentials,
- clientState,
- action
- )
- withContext(Dispatchers.Main) {
- setResult(
- RESULT_OK,
- Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) }
- )
+ withContext(Dispatchers.Main) {
+ dialog.password.collectLatest { value ->
+ if (value != null) {
+ decrypt(File(filePath), clientState, action, value)
+ }
}
}
- withContext(Dispatchers.Main) { finish() }
}
+ dialog.show(supportFragmentManager, "PASSWORD_DIALOG")
+ }
+
+ private suspend fun decrypt(
+ filePath: File,
+ clientState: Bundle,
+ action: AutofillAction,
+ password: String,
+ ) {
+ val credentials = decryptCredential(filePath, password)
+ if (credentials == null) {
+ setResult(RESULT_CANCELED)
+ } else {
+ val fillInDataset =
+ AutofillResponseBuilder.makeFillInDataset(
+ this@AutofillDecryptActivityV2,
+ credentials,
+ clientState,
+ action
+ )
+ withContext(Dispatchers.Main) {
+ setResult(
+ RESULT_OK,
+ Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) }
+ )
+ }
+ }
+ withContext(Dispatchers.Main) { finish() }
}
- private suspend fun decryptCredential(file: File): Credentials? {
- runCatching { file.inputStream() }
+ private suspend fun decryptCredential(file: File, password: String): Credentials? {
+ runCatching { file.readBytes().inputStream() }
.onFailure { e ->
logcat(ERROR) { e.asLog("File to decrypt not found") }
return null
}
.onSuccess { encryptedInput ->
runCatching {
- val crypto = cryptos.first { it.canHandle(file.absolutePath) }
withContext(Dispatchers.IO) {
val outputStream = ByteArrayOutputStream()
- crypto.decrypt(
- Key(DecryptActivityV2.PRIV_KEY.encodeToByteArray()),
- DecryptActivityV2.PASS,
+ repository.decrypt(
+ password,
encryptedInput,
outputStream,
)
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt
index 799ca32d..33ecdc2b 100644
--- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt
@@ -12,11 +12,10 @@ import android.view.MenuItem
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import dev.msfjarvis.aps.R
-import dev.msfjarvis.aps.crypto.Key
+import dev.msfjarvis.aps.data.crypto.CryptoRepository
import dev.msfjarvis.aps.data.passfile.PasswordEntry
import dev.msfjarvis.aps.data.password.FieldItem
import dev.msfjarvis.aps.databinding.DecryptLayoutBinding
-import dev.msfjarvis.aps.injection.crypto.CryptoSet
import dev.msfjarvis.aps.ui.adapters.FieldItemAdapter
import dev.msfjarvis.aps.util.extensions.unsafeLazy
import dev.msfjarvis.aps.util.extensions.viewBinding
@@ -29,6 +28,7 @@ import kotlin.time.ExperimentalTime
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -38,7 +38,7 @@ class DecryptActivityV2 : BasePgpActivity() {
private val binding by viewBinding(DecryptLayoutBinding::inflate)
@Inject lateinit var passwordEntryFactory: PasswordEntry.Factory
- @Inject lateinit var cryptos: CryptoSet
+ @Inject lateinit var repository: CryptoRepository
private val relativeParentPath by unsafeLazy { getParentPath(fullPath, repoPath) }
private var passwordEntry: PasswordEntry? = null
@@ -127,16 +127,25 @@ class DecryptActivityV2 : BasePgpActivity() {
}
private fun decrypt() {
+ val dialog = PasswordDialog()
+ lifecycleScope.launch(Dispatchers.Main) {
+ dialog.password.collectLatest { value ->
+ if (value != null) {
+ decrypt(value)
+ }
+ }
+ }
+ dialog.show(supportFragmentManager, "PASSWORD_DIALOG")
+ }
+
+ private fun decrypt(password: String) {
lifecycleScope.launch {
- // TODO(msfjarvis): native methods are fallible, add error handling once out of testing
- val message = withContext(Dispatchers.IO) { File(fullPath).inputStream() }
+ val message = withContext(Dispatchers.IO) { File(fullPath).readBytes().inputStream() }
val result =
withContext(Dispatchers.IO) {
- val crypto = cryptos.first { it.canHandle(fullPath) }
val outputStream = ByteArrayOutputStream()
- crypto.decrypt(
- Key(PRIV_KEY.encodeToByteArray()),
- PASS,
+ repository.decrypt(
+ password,
message,
outputStream,
)
@@ -179,10 +188,4 @@ class DecryptActivityV2 : BasePgpActivity() {
}
}
}
-
- companion object {
- // TODO(msfjarvis): source these from storage and user input
- const val PRIV_KEY = ""
- const val PASS = ""
- }
}
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt
index ab5b6371..a191ba61 100644
--- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt
@@ -36,10 +36,9 @@ import com.google.zxing.integration.android.IntentIntegrator.QR_CODE
import com.google.zxing.qrcode.QRCodeReader
import dagger.hilt.android.AndroidEntryPoint
import dev.msfjarvis.aps.R
-import dev.msfjarvis.aps.crypto.Key
+import dev.msfjarvis.aps.data.crypto.CryptoRepository
import dev.msfjarvis.aps.data.passfile.PasswordEntry
import dev.msfjarvis.aps.databinding.PasswordCreationActivityBinding
-import dev.msfjarvis.aps.injection.crypto.CryptoSet
import dev.msfjarvis.aps.ui.dialogs.DicewarePasswordGeneratorDialogFragment
import dev.msfjarvis.aps.ui.dialogs.OtpImportDialogFragment
import dev.msfjarvis.aps.ui.dialogs.PasswordGeneratorDialogFragment
@@ -70,7 +69,7 @@ class PasswordCreationActivityV2 : BasePgpActivity() {
private val binding by viewBinding(PasswordCreationActivityBinding::inflate)
@Inject lateinit var passwordEntryFactory: PasswordEntry.Factory
- @Inject lateinit var cryptos: CryptoSet
+ @Inject lateinit var repository: CryptoRepository
private val suggestedName by unsafeLazy { intent.getStringExtra(EXTRA_FILE_NAME) }
private val suggestedPass by unsafeLazy { intent.getStringExtra(EXTRA_PASSWORD) }
@@ -364,15 +363,10 @@ class PasswordCreationActivityV2 : BasePgpActivity() {
lifecycleScope.launch(Dispatchers.Main) {
runCatching {
- val crypto = cryptos.first { it.canHandle(path) }
val result =
withContext(Dispatchers.IO) {
val outputStream = ByteArrayOutputStream()
- crypto.encrypt(
- listOf(Key(PUB_KEY.encodeToByteArray())),
- content.byteInputStream(),
- outputStream,
- )
+ repository.encrypt(content.byteInputStream(), outputStream)
outputStream
}
val file = File(path)
@@ -484,7 +478,5 @@ class PasswordCreationActivityV2 : BasePgpActivity() {
const val EXTRA_EXTRA_CONTENT = "EXTRA_CONTENT"
const val EXTRA_GENERATE_PASSWORD = "GENERATE_PASSWORD"
const val EXTRA_EDITING = "EDITING"
- // TODO(msfjarvis): source this from storage
- const val PUB_KEY = ""
}
}
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordDialog.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordDialog.kt
new file mode 100644
index 00000000..3542422a
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordDialog.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.ui.crypto
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import dev.msfjarvis.aps.R
+import dev.msfjarvis.aps.databinding.DialogPasswordEntryBinding
+import dev.msfjarvis.aps.util.extensions.finish
+import dev.msfjarvis.aps.util.extensions.unsafeLazy
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** [DialogFragment] to request a password from the user and forward it along. */
+class PasswordDialog : DialogFragment() {
+
+ private val binding by unsafeLazy { DialogPasswordEntryBinding.inflate(layoutInflater) }
+ private val _password = MutableStateFlow<String?>(null)
+ val password = _password.asStateFlow()
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val builder = MaterialAlertDialogBuilder(requireContext())
+ builder.setView(binding.root)
+ builder.setTitle(R.string.password)
+ builder.setPositiveButton(android.R.string.ok) { _, _ ->
+ do {} while (!_password.tryEmit(binding.passwordEditText.text.toString()))
+ dismiss()
+ }
+ return builder.create()
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ finish()
+ }
+}
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/pgp/PGPKeyImportActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/pgp/PGPKeyImportActivity.kt
new file mode 100644
index 00000000..3cd6930c
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/pgp/PGPKeyImportActivity.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+@file:Suppress("BlockingMethodInNonBlockingContext")
+
+package dev.msfjarvis.aps.ui.pgp
+
+import android.os.Bundle
+import androidx.activity.result.contract.ActivityResultContracts.OpenDocument
+import androidx.appcompat.app.AppCompatActivity
+import com.github.michaelbull.result.mapBoth
+import com.github.michaelbull.result.runCatching
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import dagger.hilt.android.AndroidEntryPoint
+import dev.msfjarvis.aps.R
+import dev.msfjarvis.aps.crypto.Key
+import dev.msfjarvis.aps.crypto.KeyUtils.tryGetId
+import dev.msfjarvis.aps.crypto.PGPKeyManager
+import javax.inject.Inject
+import kotlinx.coroutines.runBlocking
+
+@AndroidEntryPoint
+class PGPKeyImportActivity : AppCompatActivity() {
+
+ @Inject lateinit var keyManager: PGPKeyManager
+
+ private val pgpKeyImportAction =
+ registerForActivityResult(OpenDocument()) { uri ->
+ runCatching {
+ if (uri == null) {
+ throw IllegalStateException("Selected URI was null")
+ }
+ val keyInputStream =
+ contentResolver.openInputStream(uri)
+ ?: throw IllegalStateException("Failed to open selected file")
+ val bytes = keyInputStream.readBytes()
+ val (key, error) = runBlocking { keyManager.addKey(Key(bytes)) }
+ if (error != null) throw error
+ key
+ }
+ .mapBoth(
+ { key ->
+ require(key != null) { "Key cannot be null here" }
+ MaterialAlertDialogBuilder(this)
+ .setTitle(getString(R.string.pgp_key_import_succeeded))
+ .setMessage(getString(R.string.pgp_key_import_succeeded_message, tryGetId(key)))
+ .setPositiveButton(android.R.string.ok) { _, _ -> finish() }
+ .setOnCancelListener { finish() }
+ .show()
+ },
+ { throwable ->
+ MaterialAlertDialogBuilder(this)
+ .setTitle(getString(R.string.pgp_key_import_failed))
+ .setMessage(throwable.message)
+ .setPositiveButton(android.R.string.ok) { _, _ -> finish() }
+ .setOnCancelListener { finish() }
+ .show()
+ }
+ )
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ pgpKeyImportAction.launch(arrayOf("*/*"))
+ }
+}
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/PGPSettings.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/PGPSettings.kt
new file mode 100644
index 00000000..81418ccb
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/PGPSettings.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.ui.settings
+
+import androidx.fragment.app.FragmentActivity
+import de.Maxr1998.modernpreferences.PreferenceScreen
+import de.Maxr1998.modernpreferences.helpers.onClick
+import de.Maxr1998.modernpreferences.helpers.pref
+import dev.msfjarvis.aps.ui.pgp.PGPKeyImportActivity
+import dev.msfjarvis.aps.util.extensions.launchActivity
+
+class PGPSettings(private val activity: FragmentActivity) : SettingsProvider {
+
+ override fun provideSettings(builder: PreferenceScreen.Builder) {
+ builder.apply {
+ pref("_") {
+ title = "Import PGP key"
+ persistent = false
+ onClick {
+ activity.launchActivity(PGPKeyImportActivity::class.java)
+ false
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt
index 8671b5cb..cbd9c18f 100644
--- a/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt
@@ -36,6 +36,7 @@ import dev.msfjarvis.aps.ui.sshkeygen.ShowSshKeyFragment
import dev.msfjarvis.aps.ui.sshkeygen.SshKeyGenActivity
import dev.msfjarvis.aps.ui.sshkeygen.SshKeyImportActivity
import dev.msfjarvis.aps.util.extensions.getString
+import dev.msfjarvis.aps.util.extensions.launchActivity
import dev.msfjarvis.aps.util.extensions.sharedPrefs
import dev.msfjarvis.aps.util.extensions.snackbar
import dev.msfjarvis.aps.util.extensions.unsafeLazy
@@ -59,16 +60,12 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
private var showSshKeyPref: Preference? = null
- private fun <T : FragmentActivity> launchActivity(clazz: Class<T>) {
- activity.startActivity(Intent(activity, clazz))
- }
-
private fun selectExternalGitRepository() {
MaterialAlertDialogBuilder(activity)
.setTitle(activity.resources.getString(R.string.external_repository_dialog_title))
.setMessage(activity.resources.getString(R.string.external_repository_dialog_text))
.setPositiveButton(R.string.dialog_ok) { _, _ ->
- launchActivity(DirectorySelectionActivity::class.java)
+ activity.launchActivity(DirectorySelectionActivity::class.java)
}
.setNegativeButton(R.string.dialog_cancel, null)
.show()
@@ -89,7 +86,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
titleRes = R.string.pref_edit_git_server_settings
visible = PasswordRepository.isGitRepo()
onClick {
- launchActivity(GitServerConfigActivity::class.java)
+ activity.launchActivity(GitServerConfigActivity::class.java)
true
}
}
@@ -97,7 +94,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
titleRes = R.string.pref_edit_proxy_settings
visible = gitSettings.url?.startsWith("https") == true && PasswordRepository.isGitRepo()
onClick {
- launchActivity(ProxySelectorActivity::class.java)
+ activity.launchActivity(ProxySelectorActivity::class.java)
true
}
}
@@ -105,7 +102,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
titleRes = R.string.pref_edit_git_config
visible = PasswordRepository.isGitRepo()
onClick {
- launchActivity(GitConfigActivity::class.java)
+ activity.launchActivity(GitConfigActivity::class.java)
true
}
}
@@ -113,7 +110,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
titleRes = R.string.pref_import_ssh_key_title
visible = PasswordRepository.isGitRepo()
onClick {
- launchActivity(SshKeyImportActivity::class.java)
+ activity.launchActivity(SshKeyImportActivity::class.java)
true
}
}
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt
index 26f060ad..d31aa630 100644
--- a/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt
@@ -24,6 +24,7 @@ class SettingsActivity : AppCompatActivity() {
private val passwordSettings = PasswordSettings(this)
private val repositorySettings = RepositorySettings(this)
private val generalSettings = GeneralSettings(this)
+ private val pgpSettings = PGPSettings(this)
private val binding by viewBinding(ActivityPreferenceRecyclerviewBinding::inflate)
private val preferencesAdapter: PreferencesAdapter
@@ -47,7 +48,7 @@ class SettingsActivity : AppCompatActivity() {
}
subScreen {
titleRes = R.string.pref_category_passwords_title
- iconRes = R.drawable.ic_lock_open_24px
+ iconRes = R.drawable.ic_password_24px
passwordSettings.provideSettings(this)
}
subScreen {
@@ -60,6 +61,11 @@ class SettingsActivity : AppCompatActivity() {
iconRes = R.drawable.ic_miscellaneous_services_24px
miscSettings.provideSettings(this)
}
+ subScreen {
+ titleRes = R.string.pref_category_pgp_title
+ iconRes = R.drawable.ic_lock_open_24px
+ pgpSettings.provideSettings(this)
+ }
}
val adapter = PreferencesAdapter(screen)
adapter.onScreenChangeListener =
diff --git a/app/src/main/java/dev/msfjarvis/aps/util/extensions/AndroidExtensions.kt b/app/src/main/java/dev/msfjarvis/aps/util/extensions/AndroidExtensions.kt
index 7671adb4..4e045715 100644
--- a/app/src/main/java/dev/msfjarvis/aps/util/extensions/AndroidExtensions.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/util/extensions/AndroidExtensions.kt
@@ -8,6 +8,7 @@ package dev.msfjarvis.aps.util.extensions
import android.app.KeyguardManager
import android.content.ClipboardManager
import android.content.Context
+import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Build
@@ -119,6 +120,11 @@ fun FragmentActivity.snackbar(
return snackbar
}
+/** Launch an activity denoted by [clazz]. */
+fun <T : FragmentActivity> FragmentActivity.launchActivity(clazz: Class<T>) {
+ startActivity(Intent(this, clazz))
+}
+
/** 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/dev/msfjarvis/aps/util/extensions/Extensions.kt b/app/src/main/java/dev/msfjarvis/aps/util/extensions/Extensions.kt
index 6c779af6..6e14a29d 100644
--- a/app/src/main/java/dev/msfjarvis/aps/util/extensions/Extensions.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/util/extensions/Extensions.kt
@@ -4,6 +4,9 @@
*/
package dev.msfjarvis.aps.util.extensions
+import com.github.michaelbull.result.Err
+import com.github.michaelbull.result.Ok
+import com.github.michaelbull.result.Result
import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.runCatching
import dev.msfjarvis.aps.data.repo.PasswordRepository
@@ -82,3 +85,13 @@ fun <T> unsafeLazy(initializer: () -> T) = lazy(LazyThreadSafetyMode.NONE) { ini
/** A convenience extension to turn a [Throwable] with a message into a loggable string. */
fun Throwable.asLog(message: String): String = "$message\n${asLog()}"
+
+/** Extension on [Result] that returns if the type is [Ok] */
+fun <V, E> Result<V, E>.isOk(): Boolean {
+ return this is Ok<V>
+}
+
+/** Extension on [Result] that returns if the type is [Err] */
+fun <V, E> Result<V, E>.isErr(): Boolean {
+ return this is Err<E>
+}
diff --git a/app/src/main/res/drawable/ic_password_24px.xml b/app/src/main/res/drawable/ic_password_24px.xml
new file mode 100644
index 00000000..a41a383a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_password_24px.xml
@@ -0,0 +1,14 @@
+<!--
+ ~ Copyright © 2014-2021 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"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M2,17h20v2H2V17zM3.15,12.95L4,11.47l0.85,1.48l1.3,-0.75L5.3,10.72H7v-1.5H5.3l0.85,-1.47L4.85,7L4,8.47L3.15,7l-1.3,0.75L2.7,9.22H1v1.5h1.7L1.85,12.2L3.15,12.95zM9.85,12.2l1.3,0.75L12,11.47l0.85,1.48l1.3,-0.75l-0.85,-1.48H15v-1.5h-1.7l0.85,-1.47L12.85,7L12,8.47L11.15,7l-1.3,0.75l0.85,1.47H9v1.5h1.7L9.85,12.2zM23,9.22h-1.7l0.85,-1.47L20.85,7L20,8.47L19.15,7l-1.3,0.75l0.85,1.47H17v1.5h1.7l-0.85,1.48l1.3,0.75L20,11.47l0.85,1.48l1.3,-0.75l-0.85,-1.48H23V9.22z"
+ android:fillColor="#000000"/>
+</vector>
diff --git a/app/src/main/res/layout/dialog_password_entry.xml b/app/src/main/res/layout/dialog_password_entry.xml
new file mode 100644
index 00000000..3729fb4f
--- /dev/null
+++ b/app/src/main/res/layout/dialog_password_entry.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2014-2022 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="@dimen/activity_horizontal_margin">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/password_field"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/password"
+ app:endIconMode="password_toggle"
+ app:hintAnimationEnabled="true"
+ app:hintEnabled="true">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/password_edit_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textPassword" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+</LinearLayout>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b2eda6fa..432ee6ea 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -390,5 +390,9 @@
<string name="gpg_key_select_mandatory">Selecting a GPG key is necessary to proceed</string>
<string name="place_shortcut_on_home_screen">Place shortcut on home screen</string>
<string name="password_list_fab_content_description">Create new password or folder</string>
+ <string name="pgp_key_import_failed">Failed to import PGP key</string>
+ <string name="pgp_key_import_succeeded">Successfully imported PGP key</string>
+ <string name="pgp_key_import_succeeded_message">The key ID of the imported key is given below, please review it for correctness:\n%1$s</string>
+ <string name="pref_category_pgp_title">PGP settings</string>
</resources>