From c70c1792f6ebb2e34fc4c7331c08b0de7e9129b4 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Fri, 2 Oct 2020 15:48:26 +0530 Subject: [PATCH 1/4] all: refactor ActivityResultContracts usages to adhere to API requirements The newest versions of AndroidX Activity and Fragments correctly enforce the requirement for all contracts to be registered at class init or before the lifecycle has reached `Lifecycle.State.STARTED`. To comply with these requirements, move all instances of `registerForActivityResult` being called at arbitrary points in the code to be done at class init. Signed-off-by: Harsh Shandilya (cherry picked from commit cf03c554785ee822be3408edcfb81fa4cf03d0e5) --- .../java/com/zeapo/pwdstore/PasswordStore.kt | 182 +++++++-------- .../java/com/zeapo/pwdstore/UserPreference.kt | 209 +++++++++--------- .../oreo/ui/AutofillDecryptActivity.kt | 24 +- .../oreo/ui/AutofillFilterActivity.kt | 14 +- .../crypto/PasswordCreationActivity.kt | 90 ++++---- .../dialogs/FolderCreationDialogFragment.kt | 48 ++-- 6 files changed, 300 insertions(+), 267 deletions(-) diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt index fc14d805..b7ada287 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt @@ -85,6 +85,97 @@ class PasswordStore : BaseGitActivity() { ViewModelProvider.AndroidViewModelFactory(application) } + private val storagePermissionRequest = registerForActivityResult(RequestPermission()) { granted -> + if (granted) checkLocalRepository() + } + + private val directorySelectAction = registerForActivityResult(StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + checkLocalRepository() + } + } + + private val listRefreshAction = registerForActivityResult(StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + refreshPasswordList() + } + } + + private val passwordMoveAction = registerForActivityResult(StartActivityForResult()) { result -> + val intentData = result.data ?: return@registerForActivityResult + val filesToMove = requireNotNull(intentData.getStringArrayExtra("Files")) + val target = File(requireNotNull(intentData.getStringExtra("SELECTED_FOLDER_PATH"))) + val repositoryPath = getRepositoryDirectory().absolutePath + if (!target.isDirectory) { + e { "Tried moving passwords to a non-existing folder." } + return@registerForActivityResult + } + + d { "Moving passwords to ${intentData.getStringExtra("SELECTED_FOLDER_PATH")}" } + d { filesToMove.joinToString(", ") } + + lifecycleScope.launch(Dispatchers.IO) { + for (file in filesToMove) { + val source = File(file) + if (!source.exists()) { + e { "Tried moving something that appears non-existent." } + continue + } + val destinationFile = File(target.absolutePath + "/" + source.name) + val basename = source.nameWithoutExtension + val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename) + val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename) + if (destinationFile.exists()) { + e { "Trying to move a file that already exists." } + withContext(Dispatchers.Main) { + MaterialAlertDialogBuilder(this@PasswordStore) + .setTitle(resources.getString(R.string.password_exists_title)) + .setMessage(resources.getString( + R.string.password_exists_message, + destinationLongName, + sourceLongName) + ) + .setPositiveButton(R.string.dialog_ok) { _, _ -> + launch(Dispatchers.IO) { + moveFile(source, destinationFile) + } + } + .setNegativeButton(R.string.dialog_cancel, null) + .show() + } + } else { + launch(Dispatchers.IO) { + moveFile(source, destinationFile) + } + } + } + when (filesToMove.size) { + 1 -> { + val source = File(filesToMove[0]) + val basename = source.nameWithoutExtension + val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename) + val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename) + withContext(Dispatchers.Main) { + commitChange( + resources.getString(R.string.git_commit_move_text, sourceLongName, destinationLongName), + ) + } + } + else -> { + val repoDir = getRepositoryDirectory().absolutePath + val relativePath = getRelativePath("${target.absolutePath}/", repoDir) + withContext(Dispatchers.Main) { + commitChange( + resources.getString(R.string.git_commit_move_multiple_text, relativePath), + ) + } + } + } + } + refreshPasswordList() + plist?.dismissActionMode() + } + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { // open search view on search key, or Ctr+F if ((keyCode == KeyEvent.KEYCODE_SEARCH || keyCode == KeyEvent.KEYCODE_F && event.isCtrlPressed) && @@ -288,9 +379,7 @@ class PasswordStore : BaseGitActivity() { Snackbar.LENGTH_INDEFINITE ).run { setAction(getString(R.string.snackbar_action_grant)) { - registerForActivityResult(RequestPermission()) { granted -> - if (granted) checkLocalRepository() - }.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + storagePermissionRequest.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) dismiss() } show() @@ -305,11 +394,7 @@ class PasswordStore : BaseGitActivity() { private fun checkLocalRepository() { val repo = initialize() if (repo == null) { - registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - checkLocalRepository() - } - }.launch(UserPreference.createDirectorySelectionIntent(this)) + directorySelectAction.launch(UserPreference.createDirectorySelectionIntent(this)) } else { checkLocalRepository(getRepositoryDirectory()) } @@ -422,11 +507,7 @@ class PasswordStore : BaseGitActivity() { val intent = Intent(this, PasswordCreationActivity::class.java) intent.putExtra("FILE_PATH", currentDir.absolutePath) intent.putExtra("REPO_PATH", getRepositoryDirectory().absolutePath) - registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - refreshPasswordList() - } - }.launch(intent) + listRefreshAction.launch(intent) } fun createFolder() { @@ -477,80 +558,7 @@ class PasswordStore : BaseGitActivity() { val intent = Intent(this, SelectFolderActivity::class.java) val fileLocations = values.map { it.file.absolutePath }.toTypedArray() intent.putExtra("Files", fileLocations) - registerForActivityResult(StartActivityForResult()) { result -> - val intentData = result.data ?: return@registerForActivityResult - val filesToMove = requireNotNull(intentData.getStringArrayExtra("Files")) - val target = File(requireNotNull(intentData.getStringExtra("SELECTED_FOLDER_PATH"))) - val repositoryPath = getRepositoryDirectory().absolutePath - if (!target.isDirectory) { - e { "Tried moving passwords to a non-existing folder." } - return@registerForActivityResult - } - - d { "Moving passwords to ${intentData.getStringExtra("SELECTED_FOLDER_PATH")}" } - d { filesToMove.joinToString(", ") } - - lifecycleScope.launch(Dispatchers.IO) { - for (file in filesToMove) { - val source = File(file) - if (!source.exists()) { - e { "Tried moving something that appears non-existent." } - continue - } - val destinationFile = File(target.absolutePath + "/" + source.name) - val basename = source.nameWithoutExtension - val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename) - val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename) - if (destinationFile.exists()) { - e { "Trying to move a file that already exists." } - withContext(Dispatchers.Main) { - MaterialAlertDialogBuilder(this@PasswordStore) - .setTitle(resources.getString(R.string.password_exists_title)) - .setMessage(resources.getString( - R.string.password_exists_message, - destinationLongName, - sourceLongName) - ) - .setPositiveButton(R.string.dialog_ok) { _, _ -> - launch(Dispatchers.IO) { - moveFile(source, destinationFile) - } - } - .setNegativeButton(R.string.dialog_cancel, null) - .show() - } - } else { - launch(Dispatchers.IO) { - moveFile(source, destinationFile) - } - } - } - when (filesToMove.size) { - 1 -> { - val source = File(filesToMove[0]) - val basename = source.nameWithoutExtension - val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename) - val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename) - withContext(Dispatchers.Main) { - commitChange( - resources.getString(R.string.git_commit_move_text, sourceLongName, destinationLongName), - ) - } - } - else -> { - val repoDir = getRepositoryDirectory().absolutePath - val relativePath = getRelativePath("${target.absolutePath}/", repoDir) - withContext(Dispatchers.Main) { - commitChange( - resources.getString(R.string.git_commit_move_multiple_text, relativePath), - ) - } - } - } - } - refreshPasswordList() - plist?.dismissActionMode() - }.launch(intent) + passwordMoveAction.launch(intent) } enum class CategoryRenameError(val resource: Int) { diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt index 62ce8b46..45915213 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt @@ -65,6 +65,106 @@ typealias ChangeListener = Preference.OnPreferenceChangeListener class UserPreference : AppCompatActivity() { private lateinit var prefsFragment: PrefsFragment + private var fromIntent = false + + @Suppress("DEPRECATION") + private val directorySelectAction = registerForActivityResult(OpenDocumentTree()) { uri: Uri? -> + if (uri == null) return@registerForActivityResult + + tag(TAG).d { "Selected repository URI is $uri" } + // TODO: This is fragile. Workaround until PasswordItem is backed by DocumentFile + val docId = DocumentsContract.getTreeDocumentId(uri) + val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val path = if (split.size > 1) split[1] else split[0] + val repoPath = "${Environment.getExternalStorageDirectory()}/$path" + val prefs = sharedPrefs + + tag(TAG).d { "Selected repository path is $repoPath" } + + if (Environment.getExternalStorageDirectory().path == repoPath) { + MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.sdcard_root_warning_title)) + .setMessage(getString(R.string.sdcard_root_warning_message)) + .setPositiveButton("Remove everything") { _, _ -> + prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, uri.path) } + } + .setNegativeButton(R.string.dialog_cancel, null) + .show() + } + prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, repoPath) } + if (fromIntent) { + setResult(RESULT_OK) + finish() + } + + } + + private val sshKeyImportAction = registerForActivityResult(OpenDocument()) { uri: Uri? -> + if (uri == null) return@registerForActivityResult + runCatching { + SshKey.import(uri) + + Toast.makeText(this, resources.getString(R.string.ssh_key_success_dialog_title), Toast.LENGTH_LONG).show() + setResult(RESULT_OK) + finish() + }.onFailure { e -> + MaterialAlertDialogBuilder(this) + .setTitle(resources.getString(R.string.ssh_key_error_dialog_title)) + .setMessage(e.message) + .setPositiveButton(resources.getString(R.string.dialog_ok), null) + .show() + } + } + + private val storeExportAction = registerForActivityResult(object : OpenDocumentTree() { + override fun createIntent(context: Context, input: Uri?): Intent { + return super.createIntent(context, input).apply { + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or + Intent.FLAG_GRANT_PREFIX_URI_PERMISSION + } + } + }) { uri: Uri? -> + if (uri == null) return@registerForActivityResult + val targetDirectory = DocumentFile.fromTreeUri(applicationContext, uri) + + if (targetDirectory != null) { + val service = Intent(applicationContext, PasswordExportService::class.java).apply { + action = PasswordExportService.ACTION_EXPORT_PASSWORD + putExtra("uri", uri) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(service) + } else { + startService(service) + } + } + } + + private val storeCustomXkpwdDictionaryAction = registerForActivityResult(OpenDocument()) { uri -> + if (uri == null) return@registerForActivityResult + + Toast.makeText( + this, + this.resources.getString(R.string.xkpwgen_custom_dict_imported, uri.path), + Toast.LENGTH_SHORT + ).show() + + sharedPrefs.edit { putString(PreferenceKeys.PREF_KEY_CUSTOM_DICT, uri.toString()) } + + val customDictPref = prefsFragment.findPreference(PreferenceKeys.PREF_KEY_CUSTOM_DICT) + setCustomDictSummary(customDictPref, uri) + // copy user selected file to internal storage + val inputStream = contentResolver.openInputStream(uri) + val customDictFile = File(filesDir.toString(), XkpwdDictionary.XKPWD_CUSTOM_DICT_FILE).outputStream() + inputStream?.copyTo(customDictFile, 1024) + inputStream?.close() + customDictFile.close() + + setResult(RESULT_OK) + } class PrefsFragment : PreferenceFragmentCompat() { @@ -471,7 +571,10 @@ class UserPreference : AppCompatActivity() { when (intent?.getStringExtra("operation")) { "get_ssh_key" -> getSshKey() "make_ssh_key" -> makeSshKey(false) - "git_external" -> selectExternalGitRepository(fromIntent = true) + "git_external" -> { + fromIntent = true + selectExternalGitRepository() + } } prefsFragment = PrefsFragment() @@ -484,41 +587,12 @@ class UserPreference : AppCompatActivity() { } @Suppress("Deprecation") // for Environment.getExternalStorageDirectory() - fun selectExternalGitRepository(fromIntent: Boolean = false) { + fun selectExternalGitRepository() { MaterialAlertDialogBuilder(this) .setTitle(this.resources.getString(R.string.external_repository_dialog_title)) .setMessage(this.resources.getString(R.string.external_repository_dialog_text)) .setPositiveButton(R.string.dialog_ok) { _, _ -> - registerForActivityResult(OpenDocumentTree()) { uri: Uri? -> - if (uri == null) return@registerForActivityResult - - tag(TAG).d { "Selected repository URI is $uri" } - // TODO: This is fragile. Workaround until PasswordItem is backed by DocumentFile - val docId = DocumentsContract.getTreeDocumentId(uri) - val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - val path = if (split.size > 1) split[1] else split[0] - val repoPath = "${Environment.getExternalStorageDirectory()}/$path" - val prefs = sharedPrefs - - tag(TAG).d { "Selected repository path is $repoPath" } - - if (Environment.getExternalStorageDirectory().path == repoPath) { - MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.sdcard_root_warning_title)) - .setMessage(getString(R.string.sdcard_root_warning_message)) - .setPositiveButton("Remove everything") { _, _ -> - prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, uri.path) } - } - .setNegativeButton(R.string.dialog_cancel, null) - .show() - } - prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, repoPath) } - if (fromIntent) { - setResult(RESULT_OK) - finish() - } - - }.launch(null) + directorySelectAction.launch(null) } .setNegativeButton(R.string.dialog_cancel, null) .show() @@ -539,26 +613,7 @@ class UserPreference : AppCompatActivity() { } private fun importSshKey() { - registerForActivityResult(OpenDocument()) { uri: Uri? -> - if (uri == null) return@registerForActivityResult - runCatching { - SshKey.import(uri) - - Toast.makeText( - this, - this.resources.getString(R.string.ssh_key_success_dialog_title), - Toast.LENGTH_LONG - ).show() - setResult(RESULT_OK) - finish() - }.onFailure { e -> - MaterialAlertDialogBuilder(this) - .setTitle(resources.getString(R.string.ssh_key_error_dialog_title)) - .setMessage(e.message) - .setPositiveButton(resources.getString(R.string.dialog_ok), null) - .show() - } - }.launch(arrayOf("*/*")) + sshKeyImportAction.launch(arrayOf("*/*")) } /** @@ -584,32 +639,7 @@ class UserPreference : AppCompatActivity() { * Exports the passwords */ private fun exportPasswords() { - registerForActivityResult(object : OpenDocumentTree() { - override fun createIntent(context: Context, input: Uri?): Intent { - return super.createIntent(context, input).apply { - flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION or - Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or - Intent.FLAG_GRANT_PREFIX_URI_PERMISSION - } - } - }) { uri: Uri? -> - if (uri == null) return@registerForActivityResult - val targetDirectory = DocumentFile.fromTreeUri(applicationContext, uri) - - if (targetDirectory != null) { - val service = Intent(applicationContext, PasswordExportService::class.java).apply { - action = PasswordExportService.ACTION_EXPORT_PASSWORD - putExtra("uri", uri) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(service) - } else { - startService(service) - } - } - }.launch(null) + storeExportAction.launch(null) } /** @@ -628,28 +658,7 @@ class UserPreference : AppCompatActivity() { * Pick custom xkpwd dictionary from sdcard */ private fun storeCustomDictionaryPath() { - registerForActivityResult(OpenDocument()) { uri -> - if (uri == null) return@registerForActivityResult - - Toast.makeText( - this, - this.resources.getString(R.string.xkpwgen_custom_dict_imported, uri.path), - Toast.LENGTH_SHORT - ).show() - - sharedPrefs.edit { putString(PreferenceKeys.PREF_KEY_CUSTOM_DICT, uri.toString()) } - - val customDictPref = prefsFragment.findPreference(PreferenceKeys.PREF_KEY_CUSTOM_DICT) - setCustomDictSummary(customDictPref, uri) - // copy user selected file to internal storage - val inputStream = contentResolver.openInputStream(uri) - val customDictFile = File(filesDir.toString(), XkpwdDictionary.XKPWD_CUSTOM_DICT_FILE).outputStream() - inputStream?.copyTo(customDictFile, 1024) - inputStream?.close() - customDictFile.close() - - setResult(RESULT_OK) - }.launch(arrayOf("*/*")) + storeCustomXkpwdDictionaryAction.launch(arrayOf("*/*")) } private val isAccessibilityServiceEnabled: Boolean diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillDecryptActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillDecryptActivity.kt index 9bab9e6f..a5bdcfe0 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillDecryptActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillDecryptActivity.kt @@ -81,6 +81,18 @@ class AutofillDecryptActivity : AppCompatActivity(), CoroutineScope { } } + private val decryptInteractionRequiredAction = registerForActivityResult(StartIntentSenderForResult()) { result -> + if (continueAfterUserInteraction != null) { + val data = result.data + if (result.resultCode == RESULT_OK && data != null) { + continueAfterUserInteraction?.resume(data) + } else { + continueAfterUserInteraction?.resumeWithException(Exception("OpenPgpApi ACTION_DECRYPT_VERIFY failed to continue after user interaction")) + } + continueAfterUserInteraction = null + } + } + private var continueAfterUserInteraction: Continuation? = null private lateinit var directoryStructure: DirectoryStructure @@ -198,17 +210,7 @@ class AutofillDecryptActivity : AppCompatActivity(), CoroutineScope { val intentToResume = withContext(Dispatchers.Main) { suspendCoroutine { cont -> continueAfterUserInteraction = cont - registerForActivityResult(StartIntentSenderForResult()) { result -> - if (continueAfterUserInteraction != null) { - val data = result.data - if (result.resultCode == RESULT_OK && data != null) { - continueAfterUserInteraction?.resume(data) - } else { - continueAfterUserInteraction?.resumeWithException(Exception("OpenPgpApi ACTION_DECRYPT_VERIFY failed to continue after user interaction")) - } - continueAfterUserInteraction = null - } - }.launch(IntentSenderRequest.Builder(pendingIntent.intentSender).build()) + decryptInteractionRequiredAction.launch(IntentSenderRequest.Builder(pendingIntent.intentSender).build()) } } decryptCredential(file, intentToResume) diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillFilterActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillFilterActivity.kt index 95e49fdd..f22c6596 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillFilterActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillFilterActivity.kt @@ -79,6 +79,13 @@ class AutofillFilterView : AppCompatActivity() { ViewModelProvider.AndroidViewModelFactory(application) } + private val decryptAction = registerForActivityResult(StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + setResult(RESULT_OK, result.data) + } + finish() + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) @@ -197,12 +204,7 @@ class AutofillFilterView : AppCompatActivity() { item.file ) // intent?.extras? is checked to be non-null in onCreate - registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - setResult(RESULT_OK, result.data) - } - finish() - }.launch(AutofillDecryptActivity.makeDecryptFileIntent( + decryptAction.launch(AutofillDecryptActivity.makeDecryptFileIntent( item.file, intent!!.extras!!, this diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt index b5ab0008..07694089 100644 --- a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt @@ -47,6 +47,7 @@ import java.io.File import java.io.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import me.msfjarvis.openpgpktx.util.OpenPgpApi import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection import me.msfjarvis.openpgpktx.util.OpenPgpUtils @@ -63,6 +64,7 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB private val oldFileName by lazy { intent.getStringExtra(EXTRA_FILE_NAME) } private var oldCategory: String? = null private var copy: Boolean = false + private var encryptionIntent: Intent = Intent() private val userInteractionRequiredResult = registerForActivityResult(StartIntentSenderForResult()) { result -> if (result.data == null) { @@ -80,6 +82,41 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB } } + private val otpImportAction = registerForActivityResult(StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + binding.otpImportButton.isVisible = false + val intentResult = IntentIntegrator.parseActivityResult(RESULT_OK, result.data) + val contents = "${intentResult.contents}\n" + val currentExtras = binding.extraContent.text.toString() + if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') + binding.extraContent.append("\n$contents") + else + binding.extraContent.append(contents) + snackbar(message = getString(R.string.otp_import_success)) + } else { + snackbar(message = getString(R.string.otp_import_failure)) + } + } + + private val gpgKeySelectAction = registerForActivityResult(StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> + lifecycleScope.launch { + val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id") + withContext(Dispatchers.IO) { + gpgIdentifierFile.writeText(keyIds.joinToString("\n")) + } + commitChange(getString( + R.string.git_commit_gpg_id, + getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name) + )).onSuccess { + encrypt(encryptionIntent) + } + } + } + } + } + private fun File.findTillRoot(fileName: String, rootPath: File): File? { val gpgFile = File(this, fileName) if (gpgFile.exists()) return gpgFile @@ -107,26 +144,11 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB setContentView(root) generatePassword.setOnClickListener { generatePassword() } otpImportButton.setOnClickListener { - registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - otpImportButton.isVisible = false - val intentResult = IntentIntegrator.parseActivityResult(RESULT_OK, result.data) - val contents = "${intentResult.contents}\n" - val currentExtras = extraContent.text.toString() - if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') - extraContent.append("\n$contents") - else - extraContent.append(contents) - snackbar(message = getString(R.string.otp_import_success)) - } else { - snackbar(message = getString(R.string.otp_import_failure)) - } - }.launch( - IntentIntegrator(this@PasswordCreationActivity) - .setOrientationLocked(false) - .setBeepEnabled(false) - .setDesiredBarcodeFormats(QR_CODE) - .createScanIntent() + otpImportAction.launch(IntentIntegrator(this@PasswordCreationActivity) + .setOrientationLocked(false) + .setBeepEnabled(false) + .setDesiredBarcodeFormats(QR_CODE) + .createScanIntent() ) } @@ -306,8 +328,8 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB copyPasswordToClipboard(editPass) } - val data = receivedIntent ?: Intent() - data.action = OpenPgpApi.ACTION_ENCRYPT + encryptionIntent = receivedIntent ?: Intent() + encryptionIntent.action = OpenPgpApi.ACTION_ENCRYPT // pass enters the key ID into `.gpg-id`. val repoRoot = PasswordRepository.getRepositoryDirectory() @@ -329,33 +351,19 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB } } if (gpgIdentifiers.isEmpty()) { - registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> - gpgIdentifierFile.writeText(keyIds.joinToString("\n")) - lifecycleScope.launch { - commitChange(getString( - R.string.git_commit_gpg_id, - getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name) - )).onSuccess { - encrypt(data) - } - } - } - } - }.launch(Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java)) + gpgKeySelectAction.launch(Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java)) return@with } val keyIds = gpgIdentifiers.filterIsInstance().map { it.id }.toLongArray() if (keyIds.isNotEmpty()) { - data.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keyIds) + encryptionIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keyIds) } val userIds = gpgIdentifiers.filterIsInstance().map { it.email }.toTypedArray() if (userIds.isNotEmpty()) { - data.putExtra(OpenPgpApi.EXTRA_USER_IDS, userIds) + encryptionIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, userIds) } - data.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true) + encryptionIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true) val content = "$editPass\n$editExtra" val inputStream = ByteArrayInputStream(content.toByteArray()) @@ -382,7 +390,7 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB } lifecycleScope.launch(Dispatchers.IO) { - api?.executeApiAsync(data, inputStream, outputStream) { result -> + api?.executeApiAsync(encryptionIntent, inputStream, outputStream) { result -> when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { OpenPgpApi.RESULT_CODE_SUCCESS -> { runCatching { diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt index 1eb1c95d..c06a6d63 100644 --- a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt @@ -31,6 +31,30 @@ import me.msfjarvis.openpgpktx.util.OpenPgpApi class FolderCreationDialogFragment : DialogFragment() { + private lateinit var newFolder: File + + private val keySelectAction = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == AppCompatActivity.RESULT_OK) { + result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> + val gpgIdentifierFile = File(newFolder, ".gpg-id") + gpgIdentifierFile.writeText(keyIds.joinToString("\n")) + val repo = PasswordRepository.getRepository(null) + if (repo != null) { + lifecycleScope.launch { + val repoPath = getRepositoryDirectory().absolutePath + requireActivity().commitChange( + getString( + R.string.git_commit_gpg_id, + BasePgpActivity.getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name) + ), + ) + dismiss() + } + } + } + } + } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val alertDialogBuilder = MaterialAlertDialogBuilder(requireContext()) alertDialogBuilder.setTitle(R.string.title_create_folder) @@ -53,7 +77,7 @@ class FolderCreationDialogFragment : DialogFragment() { val dialog = requireDialog() val folderNameView = dialog.findViewById(R.id.folder_name_text) val folderNameViewContainer = dialog.findViewById(R.id.folder_name_container) - val newFolder = File("$currentDir/${folderNameView.text}") + newFolder = File("$currentDir/${folderNameView.text}") folderNameViewContainer.error = when { newFolder.isFile -> getString(R.string.folder_creation_err_file_exists) newFolder.isDirectory -> getString(R.string.folder_creation_err_folder_exists) @@ -63,27 +87,7 @@ class FolderCreationDialogFragment : DialogFragment() { newFolder.mkdirs() (requireActivity() as PasswordStore).refreshPasswordList(newFolder) if (dialog.findViewById(R.id.set_gpg_key).isChecked) { - val gpgIdentifierFile = File(newFolder, ".gpg-id") - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == AppCompatActivity.RESULT_OK) { - result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> - gpgIdentifierFile.writeText(keyIds.joinToString("\n")) - val repo = PasswordRepository.getRepository(null) - if (repo != null) { - lifecycleScope.launch { - val repoPath = getRepositoryDirectory().absolutePath - requireActivity().commitChange( - getString( - R.string.git_commit_gpg_id, - BasePgpActivity.getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name) - ), - ) - dismiss() - } - } - } - } - }.launch(Intent(requireContext(), GetKeyIdsActivity::class.java)) + keySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java)) return } else { dismiss() From 6139693d4fbfd7f43ffbf70a73048848ae41bf3a Mon Sep 17 00:00:00 2001 From: Fabian Henneke Date: Thu, 8 Oct 2020 17:54:18 +0200 Subject: [PATCH 2/4] Temporarily work around SSHJ compatibility issues (#1142) Using ECDSA either as a key exchange or a host key algorithm fails with SSHJ 0.30.0 on Android, but should again become possible in 0.31.0. While we wait for the release, demote ECDSA in the list of key algorithms (as it should still be available for public key auth) and remove it from the list of key exchange algorithms. (cherry picked from commit 0d2788ab54b7898c88e8dc03d88323d70781a795) --- app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjConfig.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjConfig.kt b/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjConfig.kt index 49a587f5..cd280db5 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjConfig.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjConfig.kt @@ -214,9 +214,6 @@ class SshjConfig : ConfigImpl() { keyExchangeFactories = listOf( Curve25519SHA256.Factory(), FactoryLibSsh(), - ECDHNistP.Factory521(), - ECDHNistP.Factory384(), - ECDHNistP.Factory256(), DHGexSHA256.Factory(), // Sends "ext-info-c" with the list of key exchange algorithms. This is needed to get // rsa-sha2-* key types to work with some servers (e.g. GitHub). @@ -230,10 +227,10 @@ class SshjConfig : ConfigImpl() { KeyAlgorithms.EdDSA25519(), KeyAlgorithms.RSASHA512(), KeyAlgorithms.RSASHA256(), + KeyAlgorithms.SSHRSA(), KeyAlgorithms.ECDSASHANistp521(), KeyAlgorithms.ECDSASHANistp384(), KeyAlgorithms.ECDSASHANistp256(), - KeyAlgorithms.SSHRSA(), ).map { OpenKeychainWrappedKeyAlgorithmFactory(it) } From 4fbdc0e7232559f99aa27784f0a85ab13c024962 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Tue, 13 Oct 2020 12:14:20 +0530 Subject: [PATCH 3/4] CHANGELOG: add entries for #1142 and #1131 Signed-off-by: Harsh Shandilya --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e57ae0ca..5bc98f45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ All notable changes to this project will be documented in this file. ## [1.12.0] - 2020-09-24 +### Fixed + +- Certain operations like folder creation with GPG keys would fail with `java.lang.IllegalStateException`. +- ECDSA key exchanges failed resulting in users being unable to clone repositories. + ### Added - Allow sorting by recently used From 38fc3c743bf3e94f2561c017e4d700d91322877d Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Tue, 13 Oct 2020 12:54:26 +0530 Subject: [PATCH 4/4] build: bump version Signed-off-by: Harsh Shandilya --- CHANGELOG.md | 8 ++++++-- app/build.gradle.kts | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bc98f45..b4789e89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,15 @@ All notable changes to this project will be documented in this file. ## [Unreleased] -## [1.12.0] - 2020-09-24 +## [1.12.1] - 2020-10-13 ### Fixed - Certain operations like folder creation with GPG keys would fail with `java.lang.IllegalStateException`. - ECDSA key exchanges failed resulting in users being unable to clone repositories. +## [1.12.0] - 2020-09-24 + ### Added - Allow sorting by recently used @@ -352,7 +354,9 @@ All notable changes to this project will be documented in this file. - Fix elements overlapping. -[Unreleased]: https://github.com/android-password-store/Android-Password-Store/compare/v1.12.0...HEAD +[Unreleased]: https://github.com/android-password-store/Android-Password-Store/compare/v1.12.1...HEAD + +[1.12.1]: https://github.com/android-password-store/Android-Password-Store/compare/v1.12.0...v1.12.1 [1.12.0]: https://github.com/android-password-store/Android-Password-Store/compare/v1.11.3...v1.12.0 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3311efe2..013e2e4e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,8 +31,8 @@ android { defaultConfig { applicationId = "dev.msfjarvis.aps" - versionCode = 11200 - versionName = "1.12.0" + versionCode = 11210 + versionName = "1.12.1" } lintOptions {