aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivity.kt57
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt57
-rw-r--r--app/src/main/res/values/strings.xml1
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>