From a1f838151ec5423510fd9d52c89f3705e04040ce Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Fri, 15 Jul 2022 14:02:42 +0530 Subject: Remove unnecessary V2 suffix --- app/src/main/AndroidManifest.xml | 6 +- .../ui/autofill/AutofillDecryptActivity.kt | 180 ++++++++ .../ui/autofill/AutofillDecryptActivityV2.kt | 180 -------- .../ui/autofill/AutofillFilterView.kt | 2 +- .../ui/autofill/AutofillSaveActivity.kt | 10 +- .../app/passwordstore/ui/crypto/DecryptActivity.kt | 206 +++++++++ .../passwordstore/ui/crypto/DecryptActivityV2.kt | 209 --------- .../ui/crypto/PasswordCreationActivity.kt | 478 +++++++++++++++++++++ .../ui/crypto/PasswordCreationActivityV2.kt | 478 --------------------- .../DicewarePasswordGeneratorDialogFragment.kt | 6 +- .../ui/dialogs/OtpImportDialogFragment.kt | 6 +- .../ui/dialogs/PasswordGeneratorDialogFragment.kt | 6 +- .../app/passwordstore/ui/main/LaunchActivity.kt | 4 +- .../passwordstore/ui/passwords/PasswordStore.kt | 8 +- .../util/autofill/Api30AutofillResponseBuilder.kt | 4 +- .../util/autofill/AutofillResponseBuilder.kt | 4 +- app/src/main/res/layout/decrypt_layout.xml | 2 +- .../main/res/layout/password_creation_activity.xml | 2 +- 18 files changed, 894 insertions(+), 897 deletions(-) create mode 100644 app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivityV2.kt create mode 100644 app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/crypto/DecryptActivityV2.kt create mode 100644 app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivityV2.kt (limited to 'app') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0e79441d..c08c284e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -49,7 +49,7 @@ android:windowSoftInputMode="adjustResize" /> @@ -135,7 +135,7 @@ android:label="@string/pref_ssh_keygen_title" android:windowSoftInputMode="adjustResize" /> = 31) { + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE + } else { + PendingIntent.FLAG_CANCEL_CURRENT + }, + ) + .intentSender + } + } + + @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory + @Inject lateinit var repository: CryptoRepository + + private lateinit var directoryStructure: DirectoryStructure + + override fun onStart() { + super.onStart() + val filePath = + intent?.getStringExtra(EXTRA_FILE_PATH) + ?: run { + logcat(ERROR) { "AutofillDecryptActivity started without EXTRA_FILE_PATH" } + finish() + return + } + val clientState = + intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE) + ?: run { + logcat(ERROR) { "AutofillDecryptActivity started without EXTRA_CLIENT_STATE" } + finish() + return + } + val isSearchAction = intent?.getBooleanExtra(EXTRA_SEARCH_ACTION, true)!! + val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match + directoryStructure = AutofillPreferences.directoryStructure(this) + logcat { action.toString() } + val dialog = PasswordDialog() + lifecycleScope.launch { + withContext(Dispatchers.Main) { + dialog.password.collectLatest { value -> + if (value != null) { + decrypt(File(filePath), clientState, action, value) + } + } + } + } + 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@AutofillDecryptActivity, + 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 { + withContext(Dispatchers.IO) { + val outputStream = ByteArrayOutputStream() + repository.decrypt( + password, + encryptedInput, + outputStream, + ) + outputStream + } + } + .onFailure { e -> + logcat(ERROR) { e.asLog("Decryption failed") } + return null + } + .onSuccess { result -> + return runCatching { + val entry = passwordEntryFactory.create(result.toByteArray()) + AutofillPreferences.credentialsFromStoreEntry(this, file, entry, directoryStructure) + } + .getOrElse { e -> + logcat(ERROR) { e.asLog("Failed to parse password entry") } + return null + } + } + } + return null + } +} diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivityV2.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivityV2.kt deleted file mode 100644 index 35d6ca44..00000000 --- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivityV2.kt +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package app.passwordstore.ui.autofill - -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.annotation.RequiresApi -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.lifecycleScope -import app.passwordstore.data.crypto.CryptoRepository -import app.passwordstore.data.passfile.PasswordEntry -import app.passwordstore.ui.crypto.PasswordDialog -import app.passwordstore.util.autofill.AutofillPreferences -import app.passwordstore.util.autofill.AutofillResponseBuilder -import app.passwordstore.util.autofill.DirectoryStructure -import app.passwordstore.util.extensions.asLog -import com.github.androidpasswordstore.autofillparser.AutofillAction -import com.github.androidpasswordstore.autofillparser.Credentials -import com.github.michaelbull.result.getOrElse -import com.github.michaelbull.result.onFailure -import com.github.michaelbull.result.onSuccess -import com.github.michaelbull.result.runCatching -import dagger.hilt.android.AndroidEntryPoint -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 -import logcat.logcat - -@RequiresApi(26) -@AndroidEntryPoint -class AutofillDecryptActivityV2 : AppCompatActivity() { - - companion object { - - private const val EXTRA_FILE_PATH = "app.passwordstore.autofill.oreo.EXTRA_FILE_PATH" - private const val EXTRA_SEARCH_ACTION = "app.passwordstore.autofill.oreo.EXTRA_SEARCH_ACTION" - - private var decryptFileRequestCode = 1 - - fun makeDecryptFileIntent(file: File, forwardedExtras: Bundle, context: Context): Intent { - return Intent(context, AutofillDecryptActivityV2::class.java).apply { - putExtras(forwardedExtras) - putExtra(EXTRA_SEARCH_ACTION, true) - putExtra(EXTRA_FILE_PATH, file.absolutePath) - } - } - - fun makeDecryptFileIntentSender(file: File, context: Context): IntentSender { - val intent = - Intent(context, AutofillDecryptActivityV2::class.java).apply { - putExtra(EXTRA_SEARCH_ACTION, false) - putExtra(EXTRA_FILE_PATH, file.absolutePath) - } - return PendingIntent.getActivity( - context, - decryptFileRequestCode++, - intent, - if (Build.VERSION.SDK_INT >= 31) { - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE - } else { - PendingIntent.FLAG_CANCEL_CURRENT - }, - ) - .intentSender - } - } - - @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory - @Inject lateinit var repository: CryptoRepository - - private lateinit var directoryStructure: DirectoryStructure - - override fun onStart() { - super.onStart() - val filePath = - intent?.getStringExtra(EXTRA_FILE_PATH) - ?: run { - logcat(ERROR) { "AutofillDecryptActivityV2 started without EXTRA_FILE_PATH" } - finish() - return - } - val clientState = - intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE) - ?: run { - logcat(ERROR) { "AutofillDecryptActivityV2 started without EXTRA_CLIENT_STATE" } - finish() - return - } - val isSearchAction = intent?.getBooleanExtra(EXTRA_SEARCH_ACTION, true)!! - val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match - directoryStructure = AutofillPreferences.directoryStructure(this) - logcat { action.toString() } - val dialog = PasswordDialog() - lifecycleScope.launch { - withContext(Dispatchers.Main) { - dialog.password.collectLatest { value -> - if (value != null) { - decrypt(File(filePath), clientState, action, value) - } - } - } - } - 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, password: String): Credentials? { - runCatching { file.readBytes().inputStream() } - .onFailure { e -> - logcat(ERROR) { e.asLog("File to decrypt not found") } - return null - } - .onSuccess { encryptedInput -> - runCatching { - withContext(Dispatchers.IO) { - val outputStream = ByteArrayOutputStream() - repository.decrypt( - password, - encryptedInput, - outputStream, - ) - outputStream - } - } - .onFailure { e -> - logcat(ERROR) { e.asLog("Decryption failed") } - return null - } - .onSuccess { result -> - return runCatching { - val entry = passwordEntryFactory.create(result.toByteArray()) - AutofillPreferences.credentialsFromStoreEntry(this, file, entry, directoryStructure) - } - .getOrElse { e -> - logcat(ERROR) { e.asLog("Failed to parse password entry") } - return null - } - } - } - return null - } -} 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 af8f563b..66927ca3 100644 --- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt +++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt @@ -227,7 +227,7 @@ class AutofillFilterView : AppCompatActivity() { AutofillMatcher.addMatchFor(applicationContext, formOrigin, item.file) // intent?.extras? is checked to be non-null in onCreate decryptAction.launch( - AutofillDecryptActivityV2.makeDecryptFileIntent(item.file, intent!!.extras!!, this) + AutofillDecryptActivity.makeDecryptFileIntent(item.file, intent!!.extras!!, this) ) } } 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 65ec4229..6f3a6462 100644 --- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt @@ -15,7 +15,7 @@ import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.core.os.bundleOf import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.ui.crypto.PasswordCreationActivityV2 +import app.passwordstore.ui.crypto.PasswordCreationActivity import app.passwordstore.util.autofill.AutofillMatcher import app.passwordstore.util.autofill.AutofillPreferences import app.passwordstore.util.autofill.AutofillResponseBuilder @@ -108,14 +108,14 @@ class AutofillSaveActivity : AppCompatActivity() { super.onCreate(savedInstanceState) val repo = PasswordRepository.getRepositoryDirectory() val saveIntent = - Intent(this, PasswordCreationActivityV2::class.java).apply { + Intent(this, PasswordCreationActivity::class.java).apply { putExtras( bundleOf( "REPO_PATH" to repo.absolutePath, "FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath, - PasswordCreationActivityV2.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME), - PasswordCreationActivityV2.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD), - PasswordCreationActivityV2.EXTRA_GENERATE_PASSWORD to + PasswordCreationActivity.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME), + PasswordCreationActivity.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD), + PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) ) ) diff --git a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt new file mode 100644 index 00000000..7b288ca6 --- /dev/null +++ b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt @@ -0,0 +1,206 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.ui.crypto + +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.lifecycle.lifecycleScope +import app.passwordstore.R +import app.passwordstore.data.crypto.CryptoRepository +import app.passwordstore.data.passfile.PasswordEntry +import app.passwordstore.data.password.FieldItem +import app.passwordstore.databinding.DecryptLayoutBinding +import app.passwordstore.ui.adapters.FieldItemAdapter +import app.passwordstore.util.extensions.isErr +import app.passwordstore.util.extensions.unsafeLazy +import app.passwordstore.util.extensions.viewBinding +import app.passwordstore.util.settings.PreferenceKeys +import com.github.michaelbull.result.runCatching +import dagger.hilt.android.AndroidEntryPoint +import java.io.ByteArrayOutputStream +import java.io.File +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds +import kotlin.time.ExperimentalTime +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@OptIn(ExperimentalTime::class) +@AndroidEntryPoint +class DecryptActivity : BasePgpActivity() { + + private val binding by viewBinding(DecryptLayoutBinding::inflate) + private val relativeParentPath by unsafeLazy { getParentPath(fullPath, repoPath) } + @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory + @Inject lateinit var repository: CryptoRepository + + private var passwordEntry: PasswordEntry? = null + private var retries = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + title = name + with(binding) { + setContentView(root) + passwordCategory.text = relativeParentPath + passwordFile.text = name + passwordFile.setOnLongClickListener { + copyTextToClipboard(name) + true + } + } + decrypt(isError = false) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.pgp_handler, menu) + passwordEntry?.let { entry -> + menu.findItem(R.id.edit_password).isVisible = true + if (!entry.password.isNullOrBlank()) { + menu.findItem(R.id.share_password_as_plaintext).isVisible = true + menu.findItem(R.id.copy_password).isVisible = true + } + } + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> onBackPressed() + R.id.edit_password -> editPassword() + R.id.share_password_as_plaintext -> shareAsPlaintext() + R.id.copy_password -> copyPasswordToClipboard(passwordEntry?.password) + else -> return super.onOptionsItemSelected(item) + } + return true + } + + /** + * Automatically finishes the activity 60 seconds after decryption succeeded to prevent + * information leaks from stale activities. + */ + private fun startAutoDismissTimer() { + lifecycleScope.launch { + delay(60.seconds) + finish() + } + } + + /** + * Edit the current password and hide all the fields populated by encrypted data so that when the + * result triggers they can be repopulated with new data. + */ + private fun editPassword() { + val intent = Intent(this, PasswordCreationActivity::class.java) + intent.putExtra("FILE_PATH", relativeParentPath) + intent.putExtra("REPO_PATH", repoPath) + intent.putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, name) + intent.putExtra(PasswordCreationActivity.EXTRA_PASSWORD, passwordEntry?.password) + intent.putExtra(PasswordCreationActivity.EXTRA_EXTRA_CONTENT, passwordEntry?.extraContentString) + intent.putExtra(PasswordCreationActivity.EXTRA_EDITING, true) + startActivity(intent) + finish() + } + + private fun shareAsPlaintext() { + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, passwordEntry?.password) + type = "text/plain" + } + // Always show a picker to give the user a chance to cancel + startActivity( + Intent.createChooser(sendIntent, resources.getText(R.string.send_plaintext_password_to)) + ) + } + + private fun decrypt(isError: Boolean) { + if (retries < MAX_RETRIES) { + retries += 1 + } else { + finish() + } + val dialog = PasswordDialog() + if (isError) { + dialog.setError() + } + lifecycleScope.launch(Dispatchers.Main) { + dialog.password.collectLatest { value -> + if (value != null) { + if (runCatching { decrypt(value) }.isErr()) { + decrypt(isError = true) + } + } + } + } + dialog.show(supportFragmentManager, "PASSWORD_DIALOG") + } + + private suspend fun decrypt(password: String) { + val message = withContext(Dispatchers.IO) { File(fullPath).readBytes().inputStream() } + val result = + withContext(Dispatchers.IO) { + val outputStream = ByteArrayOutputStream() + repository.decrypt( + password, + message, + outputStream, + ) + outputStream + } + require(result.size() != 0) { "Incorrect password" } + startAutoDismissTimer() + + val entry = passwordEntryFactory.create(result.toByteArray()) + passwordEntry = entry + createPasswordUi(entry) + } + + private suspend fun createPasswordUi(entry: PasswordEntry) = + withContext(Dispatchers.Main) { + val showPassword = settings.getBoolean(PreferenceKeys.SHOW_PASSWORD, true) + invalidateOptionsMenu() + + val items = arrayListOf() + if (!entry.password.isNullOrBlank()) { + items.add(FieldItem.createPasswordField(entry.password!!)) + } + + if (entry.hasTotp()) { + items.add(FieldItem.createOtpField(entry.totp.first())) + } + + if (!entry.username.isNullOrBlank()) { + items.add(FieldItem.createUsernameField(entry.username!!)) + } + + entry.extraContent.forEach { (key, value) -> + items.add(FieldItem(key, value, FieldItem.ActionType.COPY)) + } + + val adapter = FieldItemAdapter(items, showPassword) { text -> copyTextToClipboard(text) } + binding.recyclerView.adapter = adapter + binding.recyclerView.itemAnimator = null + + if (entry.hasTotp()) { + entry.totp.onEach(adapter::updateOTPCode).launchIn(lifecycleScope) + } + } + + private companion object { + private const val MAX_RETRIES = 3 + } +} diff --git a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivityV2.kt b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivityV2.kt deleted file mode 100644 index fc5cb90b..00000000 --- a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivityV2.kt +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.crypto - -import android.content.Intent -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import androidx.lifecycle.lifecycleScope -import app.passwordstore.R -import app.passwordstore.data.crypto.CryptoRepository -import app.passwordstore.data.passfile.PasswordEntry -import app.passwordstore.data.password.FieldItem -import app.passwordstore.databinding.DecryptLayoutBinding -import app.passwordstore.ui.adapters.FieldItemAdapter -import app.passwordstore.util.extensions.isErr -import app.passwordstore.util.extensions.unsafeLazy -import app.passwordstore.util.extensions.viewBinding -import app.passwordstore.util.settings.PreferenceKeys -import com.github.michaelbull.result.runCatching -import dagger.hilt.android.AndroidEntryPoint -import java.io.ByteArrayOutputStream -import java.io.File -import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds -import kotlin.time.ExperimentalTime -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -@OptIn(ExperimentalTime::class) -@AndroidEntryPoint -class DecryptActivityV2 : BasePgpActivity() { - - private val binding by viewBinding(DecryptLayoutBinding::inflate) - @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory - @Inject lateinit var repository: CryptoRepository - private val relativeParentPath by unsafeLazy { getParentPath(fullPath, repoPath) } - - private var passwordEntry: PasswordEntry? = null - private var retries = 0 - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - title = name - with(binding) { - setContentView(root) - passwordCategory.text = relativeParentPath - passwordFile.text = name - passwordFile.setOnLongClickListener { - copyTextToClipboard(name) - true - } - } - decrypt(isError = false) - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.pgp_handler, menu) - passwordEntry?.let { entry -> - menu.findItem(R.id.edit_password).isVisible = true - if (!entry.password.isNullOrBlank()) { - menu.findItem(R.id.share_password_as_plaintext).isVisible = true - menu.findItem(R.id.copy_password).isVisible = true - } - } - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> onBackPressed() - R.id.edit_password -> editPassword() - R.id.share_password_as_plaintext -> shareAsPlaintext() - R.id.copy_password -> copyPasswordToClipboard(passwordEntry?.password) - else -> return super.onOptionsItemSelected(item) - } - return true - } - - /** - * Automatically finishes the activity 60 seconds after decryption succeeded to prevent - * information leaks from stale activities. - */ - private fun startAutoDismissTimer() { - lifecycleScope.launch { - delay(60.seconds) - finish() - } - } - - /** - * Edit the current password and hide all the fields populated by encrypted data so that when the - * result triggers they can be repopulated with new data. - */ - private fun editPassword() { - val intent = Intent(this, PasswordCreationActivityV2::class.java) - intent.putExtra("FILE_PATH", relativeParentPath) - intent.putExtra("REPO_PATH", repoPath) - intent.putExtra(PasswordCreationActivityV2.EXTRA_FILE_NAME, name) - intent.putExtra(PasswordCreationActivityV2.EXTRA_PASSWORD, passwordEntry?.password) - intent.putExtra( - PasswordCreationActivityV2.EXTRA_EXTRA_CONTENT, - passwordEntry?.extraContentString - ) - intent.putExtra(PasswordCreationActivityV2.EXTRA_EDITING, true) - startActivity(intent) - finish() - } - - private fun shareAsPlaintext() { - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, passwordEntry?.password) - type = "text/plain" - } - // Always show a picker to give the user a chance to cancel - startActivity( - Intent.createChooser(sendIntent, resources.getText(R.string.send_plaintext_password_to)) - ) - } - - private fun decrypt(isError: Boolean) { - if (retries < MAX_RETRIES) { - retries += 1 - } else { - finish() - } - val dialog = PasswordDialog() - if (isError) { - dialog.setError() - } - lifecycleScope.launch(Dispatchers.Main) { - dialog.password.collectLatest { value -> - if (value != null) { - if (runCatching { decrypt(value) }.isErr()) { - decrypt(isError = true) - } - } - } - } - dialog.show(supportFragmentManager, "PASSWORD_DIALOG") - } - - private suspend fun decrypt(password: String) { - val message = withContext(Dispatchers.IO) { File(fullPath).readBytes().inputStream() } - val result = - withContext(Dispatchers.IO) { - val outputStream = ByteArrayOutputStream() - repository.decrypt( - password, - message, - outputStream, - ) - outputStream - } - require(result.size() != 0) { "Incorrect password" } - startAutoDismissTimer() - - val entry = passwordEntryFactory.create(result.toByteArray()) - passwordEntry = entry - createPasswordUi(entry) - } - - private suspend fun createPasswordUi(entry: PasswordEntry) = - withContext(Dispatchers.Main) { - val showPassword = settings.getBoolean(PreferenceKeys.SHOW_PASSWORD, true) - invalidateOptionsMenu() - - val items = arrayListOf() - if (!entry.password.isNullOrBlank()) { - items.add(FieldItem.createPasswordField(entry.password!!)) - } - - if (entry.hasTotp()) { - items.add(FieldItem.createOtpField(entry.totp.first())) - } - - if (!entry.username.isNullOrBlank()) { - items.add(FieldItem.createUsernameField(entry.username!!)) - } - - entry.extraContent.forEach { (key, value) -> - items.add(FieldItem(key, value, FieldItem.ActionType.COPY)) - } - - val adapter = FieldItemAdapter(items, showPassword) { text -> copyTextToClipboard(text) } - binding.recyclerView.adapter = adapter - binding.recyclerView.itemAnimator = null - - if (entry.hasTotp()) { - entry.totp.onEach(adapter::updateOTPCode).launchIn(lifecycleScope) - } - } - - private companion object { - private const val MAX_RETRIES = 3 - } -} diff --git a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt new file mode 100644 index 00000000..b8221a46 --- /dev/null +++ b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt @@ -0,0 +1,478 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.ui.crypto + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.ImageDecoder +import android.os.Build +import android.os.Bundle +import android.provider.MediaStore +import android.text.InputType +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import androidx.core.content.edit +import androidx.core.view.isVisible +import androidx.core.widget.doAfterTextChanged +import androidx.lifecycle.lifecycleScope +import app.passwordstore.R +import app.passwordstore.data.crypto.CryptoRepository +import app.passwordstore.data.passfile.PasswordEntry +import app.passwordstore.databinding.PasswordCreationActivityBinding +import app.passwordstore.ui.dialogs.DicewarePasswordGeneratorDialogFragment +import app.passwordstore.ui.dialogs.OtpImportDialogFragment +import app.passwordstore.ui.dialogs.PasswordGeneratorDialogFragment +import app.passwordstore.util.autofill.AutofillPreferences +import app.passwordstore.util.autofill.DirectoryStructure +import app.passwordstore.util.extensions.asLog +import app.passwordstore.util.extensions.base64 +import app.passwordstore.util.extensions.commitChange +import app.passwordstore.util.extensions.getString +import app.passwordstore.util.extensions.isInsideRepository +import app.passwordstore.util.extensions.snackbar +import app.passwordstore.util.extensions.unsafeLazy +import app.passwordstore.util.extensions.viewBinding +import app.passwordstore.util.settings.PreferenceKeys +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess +import com.github.michaelbull.result.runCatching +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.zxing.BinaryBitmap +import com.google.zxing.LuminanceSource +import com.google.zxing.RGBLuminanceSource +import com.google.zxing.common.HybridBinarizer +import com.google.zxing.integration.android.IntentIntegrator +import com.google.zxing.integration.android.IntentIntegrator.QR_CODE +import com.google.zxing.qrcode.QRCodeReader +import dagger.hilt.android.AndroidEntryPoint +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.IOException +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import logcat.LogPriority.ERROR +import logcat.asLog +import logcat.logcat + +@AndroidEntryPoint +class PasswordCreationActivity : BasePgpActivity() { + + private val binding by viewBinding(PasswordCreationActivityBinding::inflate) + @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory + @Inject lateinit var repository: CryptoRepository + + private val suggestedName by unsafeLazy { intent.getStringExtra(EXTRA_FILE_NAME) } + private val suggestedPass by unsafeLazy { intent.getStringExtra(EXTRA_PASSWORD) } + private val suggestedExtra by unsafeLazy { intent.getStringExtra(EXTRA_EXTRA_CONTENT) } + private val shouldGeneratePassword by unsafeLazy { + intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) + } + private val editing by unsafeLazy { intent.getBooleanExtra(EXTRA_EDITING, false) } + private val oldFileName by unsafeLazy { intent.getStringExtra(EXTRA_FILE_NAME) } + private var oldCategory: String? = null + private var copy: Boolean = false + + 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 imageImportAction = + registerForActivityResult(ActivityResultContracts.GetContent()) { imageUri -> + if (imageUri == null) { + snackbar(message = getString(R.string.otp_import_failure)) + return@registerForActivityResult + } + val bitmap = + if (Build.VERSION.SDK_INT >= 28) { + ImageDecoder.decodeBitmap(ImageDecoder.createSource(contentResolver, imageUri)) + .copy(Bitmap.Config.ARGB_8888, true) + } else { + @Suppress("DEPRECATION") MediaStore.Images.Media.getBitmap(contentResolver, imageUri) + } + val intArray = IntArray(bitmap.width * bitmap.height) + // copy pixel data from the Bitmap into the 'intArray' array + bitmap.getPixels(intArray, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) + val source: LuminanceSource = RGBLuminanceSource(bitmap.width, bitmap.height, intArray) + val binaryBitmap = BinaryBitmap(HybridBinarizer(source)) + + val reader = QRCodeReader() + runCatching { + val result = reader.decode(binaryBitmap) + val text = result.text + val currentExtras = binding.extraContent.text.toString() + if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') + binding.extraContent.append("\n$text") + else binding.extraContent.append(text) + snackbar(message = getString(R.string.otp_import_success)) + binding.otpImportButton.isVisible = false + } + .onFailure { snackbar(message = getString(R.string.otp_import_failure)) } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + title = + if (editing) getString(R.string.edit_password) else getString(R.string.new_password_title) + with(binding) { + setContentView(root) + generatePassword.setOnClickListener { generatePassword() } + otpImportButton.setOnClickListener { + supportFragmentManager.setFragmentResultListener( + OTP_RESULT_REQUEST_KEY, + this@PasswordCreationActivity + ) { requestKey, bundle -> + if (requestKey == OTP_RESULT_REQUEST_KEY) { + val contents = bundle.getString(RESULT) + val currentExtras = binding.extraContent.text.toString() + if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') + binding.extraContent.append("\n$contents") + else binding.extraContent.append(contents) + } + } + val hasCamera = packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) == true + if (hasCamera) { + val items = + arrayOf( + getString(R.string.otp_import_qr_code), + getString(R.string.otp_import_from_file), + getString(R.string.otp_import_manual_entry), + ) + MaterialAlertDialogBuilder(this@PasswordCreationActivity) + .setItems(items) { _, index -> + when (index) { + 0 -> + otpImportAction.launch( + IntentIntegrator(this@PasswordCreationActivity) + .setOrientationLocked(false) + .setBeepEnabled(false) + .setDesiredBarcodeFormats(QR_CODE) + .createScanIntent() + ) + 1 -> imageImportAction.launch("image/*") + 2 -> OtpImportDialogFragment().show(supportFragmentManager, "OtpImport") + } + } + .show() + } else { + OtpImportDialogFragment().show(supportFragmentManager, "OtpImport") + } + } + + directoryInputLayout.apply { + if (suggestedName != null || suggestedPass != null || shouldGeneratePassword) { + isEnabled = true + } else { + setBackgroundColor(getColor(android.R.color.transparent)) + } + val path = getRelativePath(fullPath, repoPath) + // Keep empty path field visible if it is editable. + if (path.isEmpty() && !isEnabled) visibility = View.GONE + else { + directory.setText(path) + oldCategory = path + } + } + if (suggestedName != null) { + filename.setText(suggestedName) + } else { + filename.requestFocus() + } + // Allow the user to quickly switch between storing the username as the filename or + // in the encrypted extras. This only makes sense if the directory structure is + // FileBased. + if ( + suggestedName == null && + AutofillPreferences.directoryStructure(this@PasswordCreationActivity) == + DirectoryStructure.FileBased + ) { + encryptUsername.apply { + visibility = View.VISIBLE + setOnClickListener { + if (isChecked) { + // User wants to enable username encryption, so we add it to the + // encrypted extras as the first line. + val username = filename.text.toString() + val extras = "username:$username\n${extraContent.text}" + + filename.text?.clear() + extraContent.setText(extras) + } else { + // User wants to disable username encryption, so we extract the + // username from the encrypted extras and use it as the filename. + val entry = + passwordEntryFactory.create("PASSWORD\n${extraContent.text}".encodeToByteArray()) + val username = entry.username + + // username should not be null here by the logic in + // updateViewState, but it could still happen due to + // input lag. + if (username != null) { + filename.setText(username) + extraContent.setText(entry.extraContentWithoutAuthData) + } + } + } + } + } + suggestedPass?.let { + password.setText(it) + password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + } + suggestedExtra?.let { extraContent.setText(it) } + if (shouldGeneratePassword) { + generatePassword() + password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + } + } + listOf(binding.filename, binding.extraContent).forEach { + it.doAfterTextChanged { updateViewState() } + } + updateViewState() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.pgp_handler_new_password, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + setResult(RESULT_CANCELED) + onBackPressed() + } + R.id.save_password -> { + copy = false + encrypt() + } + R.id.save_and_copy_password -> { + copy = true + encrypt() + } + else -> return super.onOptionsItemSelected(item) + } + return true + } + + private fun generatePassword() { + supportFragmentManager.setFragmentResultListener(PASSWORD_RESULT_REQUEST_KEY, this) { + requestKey, + bundle -> + if (requestKey == PASSWORD_RESULT_REQUEST_KEY) { + binding.password.setText(bundle.getString(RESULT)) + } + } + when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) ?: KEY_PWGEN_TYPE_CLASSIC) { + KEY_PWGEN_TYPE_CLASSIC -> + PasswordGeneratorDialogFragment().show(supportFragmentManager, "generator") + KEY_PWGEN_TYPE_DICEWARE -> + DicewarePasswordGeneratorDialogFragment().show(supportFragmentManager, "generator") + } + } + + private fun updateViewState() = + with(binding) { + // Use PasswordEntry to parse extras for username + val entry = + passwordEntryFactory.create("PLACEHOLDER\n${extraContent.text}".encodeToByteArray()) + encryptUsername.apply { + if (visibility != View.VISIBLE) return@apply + val hasUsernameInFileName = filename.text.toString().isNotBlank() + val hasUsernameInExtras = !entry.username.isNullOrBlank() + isEnabled = hasUsernameInFileName xor hasUsernameInExtras + isChecked = hasUsernameInExtras + } + otpImportButton.isVisible = !entry.hasTotp() + } + + /** Encrypts the password and the extra content */ + private fun encrypt() { + with(binding) { + val editName = filename.text.toString().trim() + val editPass = password.text.toString() + val editExtra = extraContent.text.toString() + + if (editName.isEmpty()) { + snackbar(message = resources.getString(R.string.file_toast_text)) + return@with + } else if (editName.contains('/')) { + snackbar(message = resources.getString(R.string.invalid_filename_text)) + return@with + } + + if (editPass.isEmpty() && editExtra.isEmpty()) { + snackbar(message = resources.getString(R.string.empty_toast_text)) + return@with + } + + if (copy) { + copyPasswordToClipboard(editPass) + } + + val content = "$editPass\n$editExtra" + val path = + when { + // If we allowed the user to edit the relative path, we have to consider it here + // instead + // of fullPath. + directoryInputLayout.isEnabled -> { + val editRelativePath = directory.text.toString().trim() + if (editRelativePath.isEmpty()) { + snackbar(message = resources.getString(R.string.path_toast_text)) + return + } + val passwordDirectory = File("$repoPath/${editRelativePath.trim('/')}") + if (!passwordDirectory.exists() && !passwordDirectory.mkdir()) { + snackbar(message = "Failed to create directory ${editRelativePath.trim('/')}") + return + } + + "${passwordDirectory.path}/$editName.gpg" + } + else -> "$fullPath/$editName.gpg" + } + + lifecycleScope.launch(Dispatchers.Main) { + runCatching { + val result = + withContext(Dispatchers.IO) { + val outputStream = ByteArrayOutputStream() + repository.encrypt(content.byteInputStream(), outputStream) + outputStream + } + val file = File(path) + // If we're not editing, this file should not already exist! + // Additionally, if we were editing and the incoming and outgoing + // filenames differ, it means we renamed. Ensure that the target + // doesn't already exist to prevent an accidental overwrite. + if ( + (!editing || (editing && suggestedName != file.nameWithoutExtension)) && file.exists() + ) { + snackbar(message = getString(R.string.password_creation_duplicate_error)) + return@runCatching + } + + if (!file.isInsideRepository()) { + snackbar(message = getString(R.string.message_error_destination_outside_repo)) + return@runCatching + } + + withContext(Dispatchers.IO) { file.writeBytes(result.toByteArray()) } + + // associate the new password name with the last name's timestamp in + // history + val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) + val oldFilePathHash = "$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg".base64() + val timestamp = preference.getString(oldFilePathHash) + if (timestamp != null) { + preference.edit { + remove(oldFilePathHash) + putString(file.absolutePath.base64(), timestamp) + } + } + + val returnIntent = Intent() + returnIntent.putExtra(RETURN_EXTRA_CREATED_FILE, path) + returnIntent.putExtra(RETURN_EXTRA_NAME, editName) + returnIntent.putExtra(RETURN_EXTRA_LONG_NAME, getLongName(fullPath, repoPath, editName)) + + if (shouldGeneratePassword) { + val directoryStructure = AutofillPreferences.directoryStructure(applicationContext) + val entry = passwordEntryFactory.create(content.encodeToByteArray()) + returnIntent.putExtra(RETURN_EXTRA_PASSWORD, entry.password) + val username = entry.username ?: directoryStructure.getUsernameFor(file) + returnIntent.putExtra(RETURN_EXTRA_USERNAME, username) + } + + if ( + directoryInputLayout.isVisible && + directoryInputLayout.isEnabled && + oldFileName != null + ) { + val oldFile = File("$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg") + if (oldFile.path != file.path && !oldFile.delete()) { + setResult(RESULT_CANCELED) + MaterialAlertDialogBuilder(this@PasswordCreationActivity) + .setTitle(R.string.password_creation_file_fail_title) + .setMessage( + getString(R.string.password_creation_file_delete_fail_message, oldFileName) + ) + .setCancelable(false) + .setPositiveButton(android.R.string.ok) { _, _ -> finish() } + .show() + return@runCatching + } + } + + val commitMessageRes = + if (editing) R.string.git_commit_edit_text else R.string.git_commit_add_text + lifecycleScope.launch { + commitChange( + resources.getString(commitMessageRes, getLongName(fullPath, repoPath, editName)) + ) + .onSuccess { + setResult(RESULT_OK, returnIntent) + finish() + } + } + } + .onFailure { e -> + if (e is IOException) { + logcat(ERROR) { e.asLog("Failed to write password file") } + setResult(RESULT_CANCELED) + MaterialAlertDialogBuilder(this@PasswordCreationActivity) + .setTitle(getString(R.string.password_creation_file_fail_title)) + .setMessage(getString(R.string.password_creation_file_write_fail_message)) + .setCancelable(false) + .setPositiveButton(android.R.string.ok) { _, _ -> finish() } + .show() + } else { + logcat(ERROR) { e.asLog() } + } + } + } + } + } + + companion object { + + private const val KEY_PWGEN_TYPE_CLASSIC = "classic" + private const val KEY_PWGEN_TYPE_DICEWARE = "diceware" + const val PASSWORD_RESULT_REQUEST_KEY = "PASSWORD_GENERATOR" + const val OTP_RESULT_REQUEST_KEY = "OTP_IMPORT" + const val RESULT = "RESULT" + const val RETURN_EXTRA_CREATED_FILE = "CREATED_FILE" + const val RETURN_EXTRA_NAME = "NAME" + const val RETURN_EXTRA_LONG_NAME = "LONG_NAME" + const val RETURN_EXTRA_USERNAME = "USERNAME" + const val RETURN_EXTRA_PASSWORD = "PASSWORD" + const val EXTRA_FILE_NAME = "FILENAME" + const val EXTRA_PASSWORD = "PASSWORD" + const val EXTRA_EXTRA_CONTENT = "EXTRA_CONTENT" + const val EXTRA_GENERATE_PASSWORD = "GENERATE_PASSWORD" + const val EXTRA_EDITING = "EDITING" + } +} diff --git a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivityV2.kt b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivityV2.kt deleted file mode 100644 index 50a6e825..00000000 --- a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivityV2.kt +++ /dev/null @@ -1,478 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.crypto - -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.graphics.ImageDecoder -import android.os.Build -import android.os.Bundle -import android.provider.MediaStore -import android.text.InputType -import android.view.Menu -import android.view.MenuItem -import android.view.View -import androidx.activity.result.contract.ActivityResultContracts -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult -import androidx.core.content.edit -import androidx.core.view.isVisible -import androidx.core.widget.doAfterTextChanged -import androidx.lifecycle.lifecycleScope -import app.passwordstore.R -import app.passwordstore.data.crypto.CryptoRepository -import app.passwordstore.data.passfile.PasswordEntry -import app.passwordstore.databinding.PasswordCreationActivityBinding -import app.passwordstore.ui.dialogs.DicewarePasswordGeneratorDialogFragment -import app.passwordstore.ui.dialogs.OtpImportDialogFragment -import app.passwordstore.ui.dialogs.PasswordGeneratorDialogFragment -import app.passwordstore.util.autofill.AutofillPreferences -import app.passwordstore.util.autofill.DirectoryStructure -import app.passwordstore.util.extensions.asLog -import app.passwordstore.util.extensions.base64 -import app.passwordstore.util.extensions.commitChange -import app.passwordstore.util.extensions.getString -import app.passwordstore.util.extensions.isInsideRepository -import app.passwordstore.util.extensions.snackbar -import app.passwordstore.util.extensions.unsafeLazy -import app.passwordstore.util.extensions.viewBinding -import app.passwordstore.util.settings.PreferenceKeys -import com.github.michaelbull.result.onFailure -import com.github.michaelbull.result.onSuccess -import com.github.michaelbull.result.runCatching -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.zxing.BinaryBitmap -import com.google.zxing.LuminanceSource -import com.google.zxing.RGBLuminanceSource -import com.google.zxing.common.HybridBinarizer -import com.google.zxing.integration.android.IntentIntegrator -import com.google.zxing.integration.android.IntentIntegrator.QR_CODE -import com.google.zxing.qrcode.QRCodeReader -import dagger.hilt.android.AndroidEntryPoint -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.IOException -import javax.inject.Inject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import logcat.LogPriority.ERROR -import logcat.asLog -import logcat.logcat - -@AndroidEntryPoint -class PasswordCreationActivityV2 : BasePgpActivity() { - - private val binding by viewBinding(PasswordCreationActivityBinding::inflate) - @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory - @Inject lateinit var repository: CryptoRepository - - private val suggestedName by unsafeLazy { intent.getStringExtra(EXTRA_FILE_NAME) } - private val suggestedPass by unsafeLazy { intent.getStringExtra(EXTRA_PASSWORD) } - private val suggestedExtra by unsafeLazy { intent.getStringExtra(EXTRA_EXTRA_CONTENT) } - private val shouldGeneratePassword by unsafeLazy { - intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) - } - private val editing by unsafeLazy { intent.getBooleanExtra(EXTRA_EDITING, false) } - private val oldFileName by unsafeLazy { intent.getStringExtra(EXTRA_FILE_NAME) } - private var oldCategory: String? = null - private var copy: Boolean = false - - 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 imageImportAction = - registerForActivityResult(ActivityResultContracts.GetContent()) { imageUri -> - if (imageUri == null) { - snackbar(message = getString(R.string.otp_import_failure)) - return@registerForActivityResult - } - val bitmap = - if (Build.VERSION.SDK_INT >= 28) { - ImageDecoder.decodeBitmap(ImageDecoder.createSource(contentResolver, imageUri)) - .copy(Bitmap.Config.ARGB_8888, true) - } else { - @Suppress("DEPRECATION") MediaStore.Images.Media.getBitmap(contentResolver, imageUri) - } - val intArray = IntArray(bitmap.width * bitmap.height) - // copy pixel data from the Bitmap into the 'intArray' array - bitmap.getPixels(intArray, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) - val source: LuminanceSource = RGBLuminanceSource(bitmap.width, bitmap.height, intArray) - val binaryBitmap = BinaryBitmap(HybridBinarizer(source)) - - val reader = QRCodeReader() - runCatching { - val result = reader.decode(binaryBitmap) - val text = result.text - val currentExtras = binding.extraContent.text.toString() - if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') - binding.extraContent.append("\n$text") - else binding.extraContent.append(text) - snackbar(message = getString(R.string.otp_import_success)) - binding.otpImportButton.isVisible = false - } - .onFailure { snackbar(message = getString(R.string.otp_import_failure)) } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - title = - if (editing) getString(R.string.edit_password) else getString(R.string.new_password_title) - with(binding) { - setContentView(root) - generatePassword.setOnClickListener { generatePassword() } - otpImportButton.setOnClickListener { - supportFragmentManager.setFragmentResultListener( - OTP_RESULT_REQUEST_KEY, - this@PasswordCreationActivityV2 - ) { requestKey, bundle -> - if (requestKey == OTP_RESULT_REQUEST_KEY) { - val contents = bundle.getString(RESULT) - val currentExtras = binding.extraContent.text.toString() - if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') - binding.extraContent.append("\n$contents") - else binding.extraContent.append(contents) - } - } - val hasCamera = packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) == true - if (hasCamera) { - val items = - arrayOf( - getString(R.string.otp_import_qr_code), - getString(R.string.otp_import_from_file), - getString(R.string.otp_import_manual_entry), - ) - MaterialAlertDialogBuilder(this@PasswordCreationActivityV2) - .setItems(items) { _, index -> - when (index) { - 0 -> - otpImportAction.launch( - IntentIntegrator(this@PasswordCreationActivityV2) - .setOrientationLocked(false) - .setBeepEnabled(false) - .setDesiredBarcodeFormats(QR_CODE) - .createScanIntent() - ) - 1 -> imageImportAction.launch("image/*") - 2 -> OtpImportDialogFragment().show(supportFragmentManager, "OtpImport") - } - } - .show() - } else { - OtpImportDialogFragment().show(supportFragmentManager, "OtpImport") - } - } - - directoryInputLayout.apply { - if (suggestedName != null || suggestedPass != null || shouldGeneratePassword) { - isEnabled = true - } else { - setBackgroundColor(getColor(android.R.color.transparent)) - } - val path = getRelativePath(fullPath, repoPath) - // Keep empty path field visible if it is editable. - if (path.isEmpty() && !isEnabled) visibility = View.GONE - else { - directory.setText(path) - oldCategory = path - } - } - if (suggestedName != null) { - filename.setText(suggestedName) - } else { - filename.requestFocus() - } - // Allow the user to quickly switch between storing the username as the filename or - // in the encrypted extras. This only makes sense if the directory structure is - // FileBased. - if ( - suggestedName == null && - AutofillPreferences.directoryStructure(this@PasswordCreationActivityV2) == - DirectoryStructure.FileBased - ) { - encryptUsername.apply { - visibility = View.VISIBLE - setOnClickListener { - if (isChecked) { - // User wants to enable username encryption, so we add it to the - // encrypted extras as the first line. - val username = filename.text.toString() - val extras = "username:$username\n${extraContent.text}" - - filename.text?.clear() - extraContent.setText(extras) - } else { - // User wants to disable username encryption, so we extract the - // username from the encrypted extras and use it as the filename. - val entry = - passwordEntryFactory.create("PASSWORD\n${extraContent.text}".encodeToByteArray()) - val username = entry.username - - // username should not be null here by the logic in - // updateViewState, but it could still happen due to - // input lag. - if (username != null) { - filename.setText(username) - extraContent.setText(entry.extraContentWithoutAuthData) - } - } - } - } - } - suggestedPass?.let { - password.setText(it) - password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD - } - suggestedExtra?.let { extraContent.setText(it) } - if (shouldGeneratePassword) { - generatePassword() - password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD - } - } - listOf(binding.filename, binding.extraContent).forEach { - it.doAfterTextChanged { updateViewState() } - } - updateViewState() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.pgp_handler_new_password, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - setResult(RESULT_CANCELED) - onBackPressed() - } - R.id.save_password -> { - copy = false - encrypt() - } - R.id.save_and_copy_password -> { - copy = true - encrypt() - } - else -> return super.onOptionsItemSelected(item) - } - return true - } - - private fun generatePassword() { - supportFragmentManager.setFragmentResultListener(PASSWORD_RESULT_REQUEST_KEY, this) { - requestKey, - bundle -> - if (requestKey == PASSWORD_RESULT_REQUEST_KEY) { - binding.password.setText(bundle.getString(RESULT)) - } - } - when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) ?: KEY_PWGEN_TYPE_CLASSIC) { - KEY_PWGEN_TYPE_CLASSIC -> - PasswordGeneratorDialogFragment().show(supportFragmentManager, "generator") - KEY_PWGEN_TYPE_DICEWARE -> - DicewarePasswordGeneratorDialogFragment().show(supportFragmentManager, "generator") - } - } - - private fun updateViewState() = - with(binding) { - // Use PasswordEntry to parse extras for username - val entry = - passwordEntryFactory.create("PLACEHOLDER\n${extraContent.text}".encodeToByteArray()) - encryptUsername.apply { - if (visibility != View.VISIBLE) return@apply - val hasUsernameInFileName = filename.text.toString().isNotBlank() - val hasUsernameInExtras = !entry.username.isNullOrBlank() - isEnabled = hasUsernameInFileName xor hasUsernameInExtras - isChecked = hasUsernameInExtras - } - otpImportButton.isVisible = !entry.hasTotp() - } - - /** Encrypts the password and the extra content */ - private fun encrypt() { - with(binding) { - val editName = filename.text.toString().trim() - val editPass = password.text.toString() - val editExtra = extraContent.text.toString() - - if (editName.isEmpty()) { - snackbar(message = resources.getString(R.string.file_toast_text)) - return@with - } else if (editName.contains('/')) { - snackbar(message = resources.getString(R.string.invalid_filename_text)) - return@with - } - - if (editPass.isEmpty() && editExtra.isEmpty()) { - snackbar(message = resources.getString(R.string.empty_toast_text)) - return@with - } - - if (copy) { - copyPasswordToClipboard(editPass) - } - - val content = "$editPass\n$editExtra" - val path = - when { - // If we allowed the user to edit the relative path, we have to consider it here - // instead - // of fullPath. - directoryInputLayout.isEnabled -> { - val editRelativePath = directory.text.toString().trim() - if (editRelativePath.isEmpty()) { - snackbar(message = resources.getString(R.string.path_toast_text)) - return - } - val passwordDirectory = File("$repoPath/${editRelativePath.trim('/')}") - if (!passwordDirectory.exists() && !passwordDirectory.mkdir()) { - snackbar(message = "Failed to create directory ${editRelativePath.trim('/')}") - return - } - - "${passwordDirectory.path}/$editName.gpg" - } - else -> "$fullPath/$editName.gpg" - } - - lifecycleScope.launch(Dispatchers.Main) { - runCatching { - val result = - withContext(Dispatchers.IO) { - val outputStream = ByteArrayOutputStream() - repository.encrypt(content.byteInputStream(), outputStream) - outputStream - } - val file = File(path) - // If we're not editing, this file should not already exist! - // Additionally, if we were editing and the incoming and outgoing - // filenames differ, it means we renamed. Ensure that the target - // doesn't already exist to prevent an accidental overwrite. - if ( - (!editing || (editing && suggestedName != file.nameWithoutExtension)) && file.exists() - ) { - snackbar(message = getString(R.string.password_creation_duplicate_error)) - return@runCatching - } - - if (!file.isInsideRepository()) { - snackbar(message = getString(R.string.message_error_destination_outside_repo)) - return@runCatching - } - - withContext(Dispatchers.IO) { file.writeBytes(result.toByteArray()) } - - // associate the new password name with the last name's timestamp in - // history - val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) - val oldFilePathHash = "$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg".base64() - val timestamp = preference.getString(oldFilePathHash) - if (timestamp != null) { - preference.edit { - remove(oldFilePathHash) - putString(file.absolutePath.base64(), timestamp) - } - } - - val returnIntent = Intent() - returnIntent.putExtra(RETURN_EXTRA_CREATED_FILE, path) - returnIntent.putExtra(RETURN_EXTRA_NAME, editName) - returnIntent.putExtra(RETURN_EXTRA_LONG_NAME, getLongName(fullPath, repoPath, editName)) - - if (shouldGeneratePassword) { - val directoryStructure = AutofillPreferences.directoryStructure(applicationContext) - val entry = passwordEntryFactory.create(content.encodeToByteArray()) - returnIntent.putExtra(RETURN_EXTRA_PASSWORD, entry.password) - val username = entry.username ?: directoryStructure.getUsernameFor(file) - returnIntent.putExtra(RETURN_EXTRA_USERNAME, username) - } - - if ( - directoryInputLayout.isVisible && - directoryInputLayout.isEnabled && - oldFileName != null - ) { - val oldFile = File("$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg") - if (oldFile.path != file.path && !oldFile.delete()) { - setResult(RESULT_CANCELED) - MaterialAlertDialogBuilder(this@PasswordCreationActivityV2) - .setTitle(R.string.password_creation_file_fail_title) - .setMessage( - getString(R.string.password_creation_file_delete_fail_message, oldFileName) - ) - .setCancelable(false) - .setPositiveButton(android.R.string.ok) { _, _ -> finish() } - .show() - return@runCatching - } - } - - val commitMessageRes = - if (editing) R.string.git_commit_edit_text else R.string.git_commit_add_text - lifecycleScope.launch { - commitChange( - resources.getString(commitMessageRes, getLongName(fullPath, repoPath, editName)) - ) - .onSuccess { - setResult(RESULT_OK, returnIntent) - finish() - } - } - } - .onFailure { e -> - if (e is IOException) { - logcat(ERROR) { e.asLog("Failed to write password file") } - setResult(RESULT_CANCELED) - MaterialAlertDialogBuilder(this@PasswordCreationActivityV2) - .setTitle(getString(R.string.password_creation_file_fail_title)) - .setMessage(getString(R.string.password_creation_file_write_fail_message)) - .setCancelable(false) - .setPositiveButton(android.R.string.ok) { _, _ -> finish() } - .show() - } else { - logcat(ERROR) { e.asLog() } - } - } - } - } - } - - companion object { - - private const val KEY_PWGEN_TYPE_CLASSIC = "classic" - private const val KEY_PWGEN_TYPE_DICEWARE = "diceware" - const val PASSWORD_RESULT_REQUEST_KEY = "PASSWORD_GENERATOR" - const val OTP_RESULT_REQUEST_KEY = "OTP_IMPORT" - const val RESULT = "RESULT" - const val RETURN_EXTRA_CREATED_FILE = "CREATED_FILE" - const val RETURN_EXTRA_NAME = "NAME" - const val RETURN_EXTRA_LONG_NAME = "LONG_NAME" - const val RETURN_EXTRA_USERNAME = "USERNAME" - const val RETURN_EXTRA_PASSWORD = "PASSWORD" - const val EXTRA_FILE_NAME = "FILENAME" - const val EXTRA_PASSWORD = "PASSWORD" - const val EXTRA_EXTRA_CONTENT = "EXTRA_CONTENT" - const val EXTRA_GENERATE_PASSWORD = "GENERATE_PASSWORD" - const val EXTRA_EDITING = "EDITING" - } -} diff --git a/app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt b/app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt index 04488e90..bb3b6f6f 100644 --- a/app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt +++ b/app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt @@ -19,7 +19,7 @@ import app.passwordstore.R import app.passwordstore.databinding.FragmentPwgenDicewareBinding import app.passwordstore.injection.prefs.PasswordGeneratorPreferences import app.passwordstore.passgen.diceware.DicewarePassphraseGenerator -import app.passwordstore.ui.crypto.PasswordCreationActivityV2 +import app.passwordstore.ui.crypto.PasswordCreationActivity import app.passwordstore.util.extensions.getString import app.passwordstore.util.settings.PreferenceKeys.DICEWARE_LENGTH import app.passwordstore.util.settings.PreferenceKeys.DICEWARE_SEPARATOR @@ -58,8 +58,8 @@ class DicewarePasswordGeneratorDialogFragment : DialogFragment() { setTitle(R.string.pwgen_title) setPositiveButton(R.string.dialog_ok) { _, _ -> setFragmentResult( - PasswordCreationActivityV2.PASSWORD_RESULT_REQUEST_KEY, - bundleOf(PasswordCreationActivityV2.RESULT to "${binding.passwordText.text}") + PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY, + bundleOf(PasswordCreationActivity.RESULT to "${binding.passwordText.text}") ) } setNeutralButton(R.string.dialog_cancel) { _, _ -> } diff --git a/app/src/main/java/app/passwordstore/ui/dialogs/OtpImportDialogFragment.kt b/app/src/main/java/app/passwordstore/ui/dialogs/OtpImportDialogFragment.kt index cd36be04..4d2413d4 100644 --- a/app/src/main/java/app/passwordstore/ui/dialogs/OtpImportDialogFragment.kt +++ b/app/src/main/java/app/passwordstore/ui/dialogs/OtpImportDialogFragment.kt @@ -13,7 +13,7 @@ import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment import androidx.fragment.app.setFragmentResult import app.passwordstore.databinding.FragmentManualOtpEntryBinding -import app.passwordstore.ui.crypto.PasswordCreationActivityV2 +import app.passwordstore.ui.crypto.PasswordCreationActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder class OtpImportDialogFragment : DialogFragment() { @@ -24,8 +24,8 @@ class OtpImportDialogFragment : DialogFragment() { builder.setView(binding.root) builder.setPositiveButton(android.R.string.ok) { _, _ -> setFragmentResult( - PasswordCreationActivityV2.OTP_RESULT_REQUEST_KEY, - bundleOf(PasswordCreationActivityV2.RESULT to getTOTPUri(binding)) + PasswordCreationActivity.OTP_RESULT_REQUEST_KEY, + bundleOf(PasswordCreationActivity.RESULT to getTOTPUri(binding)) ) } val dialog = builder.create() diff --git a/app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt b/app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt index 352c755f..06ce2d92 100644 --- a/app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt +++ b/app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt @@ -26,7 +26,7 @@ import app.passwordstore.passgen.random.NoCharactersIncludedException import app.passwordstore.passgen.random.PasswordGenerator import app.passwordstore.passgen.random.PasswordLengthTooShortException import app.passwordstore.passgen.random.PasswordOption -import app.passwordstore.ui.crypto.PasswordCreationActivityV2 +import app.passwordstore.ui.crypto.PasswordCreationActivity import app.passwordstore.util.settings.PreferenceKeys import com.github.michaelbull.result.getOrElse import com.github.michaelbull.result.runCatching @@ -72,8 +72,8 @@ class PasswordGeneratorDialogFragment : DialogFragment() { setTitle(R.string.pwgen_title) setPositiveButton(R.string.dialog_ok) { _, _ -> setFragmentResult( - PasswordCreationActivityV2.PASSWORD_RESULT_REQUEST_KEY, - bundleOf(PasswordCreationActivityV2.RESULT to "${binding.passwordText.text}") + PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY, + bundleOf(PasswordCreationActivity.RESULT to "${binding.passwordText.text}") ) } setNeutralButton(R.string.dialog_cancel) { _, _ -> } 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 1a2e1a60..0c38e3da 100644 --- a/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt @@ -11,7 +11,7 @@ import android.os.Looper import androidx.appcompat.app.AppCompatActivity import androidx.core.content.edit import app.passwordstore.ui.crypto.BasePgpActivity -import app.passwordstore.ui.crypto.DecryptActivityV2 +import app.passwordstore.ui.crypto.DecryptActivity import app.passwordstore.ui.passwords.PasswordStore import app.passwordstore.util.auth.BiometricAuthenticator import app.passwordstore.util.auth.BiometricAuthenticator.Result @@ -52,7 +52,7 @@ class LaunchActivity : AppCompatActivity() { } private fun getDecryptIntent(): Intent { - return Intent(this, DecryptActivityV2::class.java) + return Intent(this, DecryptActivity::class.java) } private fun startTargetActivity(noAuth: Boolean) { diff --git a/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt b/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt index eb8967f5..0078f08e 100644 --- a/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt +++ b/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt @@ -29,8 +29,8 @@ import app.passwordstore.data.password.PasswordItem import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.ui.crypto.BasePgpActivity import app.passwordstore.ui.crypto.BasePgpActivity.Companion.getLongName -import app.passwordstore.ui.crypto.DecryptActivityV2 -import app.passwordstore.ui.crypto.PasswordCreationActivityV2 +import app.passwordstore.ui.crypto.DecryptActivity +import app.passwordstore.ui.crypto.PasswordCreationActivity import app.passwordstore.ui.dialogs.FolderCreationDialogFragment import app.passwordstore.ui.folderselect.SelectFolderActivity import app.passwordstore.ui.git.base.BaseGitActivity @@ -383,7 +383,7 @@ class PasswordStore : BaseGitActivity() { val authDecryptIntent = item.createAuthEnabledIntent(this) val decryptIntent = (authDecryptIntent.clone() as Intent).setComponent( - ComponentName(this, DecryptActivityV2::class.java) + ComponentName(this, DecryptActivity::class.java) ) startActivity(decryptIntent) @@ -407,7 +407,7 @@ class PasswordStore : BaseGitActivity() { if (!validateState()) return val currentDir = currentDir logcat(INFO) { "Adding file to : ${currentDir.absolutePath}" } - val intent = Intent(this, PasswordCreationActivityV2::class.java) + val intent = Intent(this, PasswordCreationActivity::class.java) intent.putExtra(BasePgpActivity.EXTRA_FILE_PATH, currentDir.absolutePath) intent.putExtra( BasePgpActivity.EXTRA_REPO_PATH, 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 45cc1f61..f46d3c1a 100644 --- a/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt +++ b/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt @@ -15,7 +15,7 @@ import android.view.inputmethod.InlineSuggestionsRequest import android.widget.inline.InlinePresentationSpec import androidx.annotation.RequiresApi import app.passwordstore.autofill.oreo.ui.AutofillSmsActivity -import app.passwordstore.ui.autofill.AutofillDecryptActivityV2 +import app.passwordstore.ui.autofill.AutofillDecryptActivity import app.passwordstore.ui.autofill.AutofillFilterView import app.passwordstore.ui.autofill.AutofillPublisherChangedActivity import app.passwordstore.ui.autofill.AutofillSaveActivity @@ -81,7 +81,7 @@ constructor( ): Dataset? { if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null val metadata = makeFillMatchMetadata(context, file) - val intentSender = AutofillDecryptActivityV2.makeDecryptFileIntentSender(file, context) + val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context) return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata, imeSpec) } 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 8f96a67d..94a4a564 100644 --- a/app/src/main/java/app/passwordstore/util/autofill/AutofillResponseBuilder.kt +++ b/app/src/main/java/app/passwordstore/util/autofill/AutofillResponseBuilder.kt @@ -14,7 +14,7 @@ import android.service.autofill.FillResponse import android.service.autofill.SaveInfo import androidx.annotation.RequiresApi import app.passwordstore.autofill.oreo.ui.AutofillSmsActivity -import app.passwordstore.ui.autofill.AutofillDecryptActivityV2 +import app.passwordstore.ui.autofill.AutofillDecryptActivity import app.passwordstore.ui.autofill.AutofillFilterView import app.passwordstore.ui.autofill.AutofillPublisherChangedActivity import app.passwordstore.ui.autofill.AutofillSaveActivity @@ -70,7 +70,7 @@ constructor( private fun makeMatchDataset(context: Context, file: File): Dataset? { if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null val metadata = makeFillMatchMetadata(context, file) - val intentSender = AutofillDecryptActivityV2.makeDecryptFileIntentSender(file, context) + val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context) return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata) } diff --git a/app/src/main/res/layout/decrypt_layout.xml b/app/src/main/res/layout/decrypt_layout.xml index 61cf6d12..a5fbbeec 100644 --- a/app/src/main/res/layout/decrypt_layout.xml +++ b/app/src/main/res/layout/decrypt_layout.xml @@ -10,7 +10,7 @@ android:layout_height="match_parent" android:orientation="vertical" android:paddingTop="16dp" - tools:context="app.passwordstore.ui.crypto.DecryptActivityV2"> + tools:context="app.passwordstore.ui.crypto.DecryptActivity"> + tools:context="app.passwordstore.ui.crypto.PasswordCreationActivity">