mirror of
https://github.com/android-password-store/Android-Password-Store
synced 2025-08-31 14:25:28 +00:00
Integrate PGPainless backend into the UI properly (#1647)
This commit is contained in:
@@ -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>
|
||||
|
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@@ -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
|
@@ -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 decryptCredential(file: File): Credentials? {
|
||||
runCatching { file.inputStream() }
|
||||
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, 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,
|
||||
)
|
||||
|
@@ -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 = ""
|
||||
}
|
||||
}
|
||||
|
@@ -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 = ""
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -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("*/*"))
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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 =
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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>
|
||||
}
|
||||
|
14
app/src/main/res/drawable/ic_password_24px.xml
Normal file
14
app/src/main/res/drawable/ic_password_24px.xml
Normal file
@@ -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>
|
29
app/src/main/res/layout/dialog_password_entry.xml
Normal file
29
app/src/main/res/layout/dialog_password_entry.xml
Normal file
@@ -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>
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user