diff options
author | Aditya Wasan <adityawasan55@gmail.com> | 2021-12-07 21:59:03 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-12-07 16:29:03 +0000 |
commit | 17f640bf468383c791bd4f8e7934e84d9d262079 (patch) | |
tree | 505357897270ccce5dcda8da9a2aa07b757387f7 | |
parent | 1df01a2f5486f400cac35af49f2fc474c5206204 (diff) |
Allow importing TOTP from images (#1580)
* feat(aps): allow importing TOTP code from an image containing a QR code
Signed-off-by: Aditya <adityawasan55@gmail.com>
* Reorder OTP import options and implement it for V2
* Replace try-catch with runCatching
* Use the correct TextWatcher extension at the right place
Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivity.kt | 57 | ||||
-rw-r--r-- | app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt | 57 | ||||
-rw-r--r-- | app/src/main/res/values/strings.xml | 1 |
4 files changed, 104 insertions, 12 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index f3119fbe..3edf1d7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. - Improve search result filtering logic - Allow pinning shortcuts directly to the launcher home screen - Another workaround for SteamGuard's non-standard OTP format +- Allow importing QR code from images ### Fixed diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivity.kt index d10f4ef6..b22ee706 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivity.kt @@ -8,25 +8,35 @@ package dev.msfjarvis.aps.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.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult import androidx.core.content.edit import androidx.core.view.isVisible -import androidx.core.widget.doOnTextChanged +import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.lifecycleScope 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.android.material.snackbar.Snackbar +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 dev.msfjarvis.aps.R import dev.msfjarvis.aps.data.passfile.PasswordEntry @@ -112,6 +122,39 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB } } + 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 >= Build.VERSION_CODES.P) { + 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)) } + } + private val gpgKeySelectAction = registerForActivityResult(StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { @@ -185,7 +228,8 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB val items = arrayOf( getString(R.string.otp_import_qr_code), - getString(R.string.otp_import_manual_entry) + getString(R.string.otp_import_from_file), + getString(R.string.otp_import_manual_entry), ) MaterialAlertDialogBuilder(this@PasswordCreationActivity) .setItems(items) { _, index -> @@ -198,7 +242,8 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB .setDesiredBarcodeFormats(QR_CODE) .createScanIntent() ) - 1 -> OtpImportDialogFragment().show(supportFragmentManager, "OtpImport") + 1 -> imageImportAction.launch("image/*") + 2 -> OtpImportDialogFragment().show(supportFragmentManager, "OtpImport") } } .show() @@ -264,9 +309,6 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB } } } - listOf(filename, extraContent).forEach { - it.doOnTextChanged { _, _, _, _ -> updateViewState() } - } } suggestedPass?.let { password.setText(it) @@ -278,6 +320,9 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD } } + listOf(binding.filename, binding.extraContent).forEach { + it.doAfterTextChanged { updateViewState() } + } updateViewState() } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt index 71793a40..b99d5f6d 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt @@ -8,22 +8,32 @@ package dev.msfjarvis.aps.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.doOnTextChanged +import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.lifecycleScope 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 dev.msfjarvis.aps.R import dev.msfjarvis.aps.data.passfile.PasswordEntry @@ -88,6 +98,39 @@ class PasswordCreationActivityV2 : BasePgpActivity() { } } + 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 >= Build.VERSION_CODES.P) { + 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) @@ -115,7 +158,8 @@ class PasswordCreationActivityV2 : BasePgpActivity() { val items = arrayOf( getString(R.string.otp_import_qr_code), - getString(R.string.otp_import_manual_entry) + getString(R.string.otp_import_from_file), + getString(R.string.otp_import_manual_entry), ) MaterialAlertDialogBuilder(this@PasswordCreationActivityV2) .setItems(items) { _, index -> @@ -128,7 +172,8 @@ class PasswordCreationActivityV2 : BasePgpActivity() { .setDesiredBarcodeFormats(QR_CODE) .createScanIntent() ) - 1 -> OtpImportDialogFragment().show(supportFragmentManager, "OtpImport") + 1 -> imageImportAction.launch("image/*") + 2 -> OtpImportDialogFragment().show(supportFragmentManager, "OtpImport") } } .show() @@ -194,9 +239,6 @@ class PasswordCreationActivityV2 : BasePgpActivity() { } } } - listOf(filename, extraContent).forEach { - it.doOnTextChanged { _, _, _, _ -> updateViewState() } - } } suggestedPass?.let { password.setText(it) @@ -208,6 +250,9 @@ class PasswordCreationActivityV2 : BasePgpActivity() { password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD } } + listOf(binding.filename, binding.extraContent).forEach { + it.doAfterTextChanged { updateViewState() } + } updateViewState() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 16269719..489a5a2d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -394,6 +394,7 @@ <string name="clear_saved_host_key">Clear saved host key</string> <string name="clear_saved_host_key_success">Successfully cleared saved host key!</string> <string name="otp_import_qr_code">Scan QR code</string> + <string name="otp_import_from_file">Choose an image</string> <string name="otp_import_manual_entry">Enter manually</string> <string name="otp_import_manual_hint_secret">Secret</string> <string name="otp_import_manual_hint_account">Account</string> |