diff options
4 files changed, 136 insertions, 1 deletions
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt index 71e409a7..7b196ce8 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt @@ -21,6 +21,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar +import com.google.zxing.qrcode.QRCodeReader import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanOptions import com.wireguard.android.Application @@ -31,6 +32,7 @@ import com.wireguard.android.databinding.TunnelListFragmentBinding import com.wireguard.android.databinding.TunnelListItemBinding import com.wireguard.android.model.ObservableTunnel import com.wireguard.android.util.ErrorMessages +import com.wireguard.android.util.QrCodeFromFileScanner import com.wireguard.android.util.TunnelImporter import com.wireguard.android.widget.MultiselectableRelativeLayout import kotlinx.coroutines.SupervisorJob @@ -52,7 +54,20 @@ class TunnelListFragment : BaseFragment() { val activity = activity ?: return@registerForActivityResult val contentResolver = activity.contentResolver ?: return@registerForActivityResult activity.lifecycleScope.launch { - TunnelImporter.importTunnel(contentResolver, data) { showSnackbar(it) } + val qrCodeFromFileScanner = QrCodeFromFileScanner(contentResolver, QRCodeReader()) + if (qrCodeFromFileScanner.validContentType(data)) { + try { + val result = qrCodeFromFileScanner.scan(data) + TunnelImporter.importTunnel(parentFragmentManager, result.text) { showSnackbar(it) } + } catch (e: Exception) { + val error = ErrorMessages[e] + val message = requireContext().getString(R.string.import_error, error) + Log.e(TAG, message, e) + showSnackbar(message) + } + } else { + TunnelImporter.importTunnel(contentResolver, data) { showSnackbar(it) } + } } } diff --git a/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt index 9369415e..60c6b878 100644 --- a/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt +++ b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt @@ -6,6 +6,8 @@ package com.wireguard.android.util import android.content.res.Resources import android.os.RemoteException +import com.google.zxing.ChecksumException +import com.google.zxing.NotFoundException import com.wireguard.android.Application import com.wireguard.android.R import com.wireguard.android.backend.BackendException @@ -84,6 +86,12 @@ object ErrorMessages { rootCause is RootShellException -> { resources.getString(RSE_REASON_MAP.getValue(rootCause.reason), *rootCause.format) } + rootCause is NotFoundException -> { + resources.getString(R.string.error_no_qr_found) + } + rootCause is ChecksumException -> { + resources.getString(R.string.error_qr_checksum) + } rootCause.message != null -> { rootCause.message!! } diff --git a/ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt b/ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt new file mode 100644 index 00000000..5d1007e4 --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt @@ -0,0 +1,110 @@ +/* + * Copyright © 2017-2022 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.util + +import android.content.ContentResolver +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.util.Log +import com.google.zxing.BinaryBitmap +import com.google.zxing.DecodeHintType +import com.google.zxing.NotFoundException +import com.google.zxing.RGBLuminanceSource +import com.google.zxing.Reader +import com.google.zxing.Result +import com.google.zxing.common.HybridBinarizer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Encapsulates the logic of scanning a barcode from a file, + * @property contentResolver - Resolver to read the incoming data + * @property reader - An instance of zxing's [Reader] class to parse the image + */ +class QrCodeFromFileScanner( + private val contentResolver: ContentResolver, + private val reader: Reader, +) { + + private fun scanBitmapForResult(source: Bitmap): Result { + val width = source.width + val height = source.height + val pixels = IntArray(width * height) + source.getPixels(pixels, 0, width, 0, 0, width, height) + + val bBitmap = BinaryBitmap(HybridBinarizer(RGBLuminanceSource(width, height, pixels))) + return reader.decode(bBitmap, mapOf(DecodeHintType.TRY_HARDER to true)) + } + + private fun downscaleBitmap(source: Bitmap, scaledSize: Int): Bitmap { + + val originalWidth = source.width + val originalHeight = source.height + + var newWidth = -1 + var newHeight = -1 + val multFactor: Float + + when { + originalHeight > originalWidth -> { + newHeight = scaledSize + multFactor = originalWidth.toFloat() / originalHeight.toFloat() + newWidth = (newHeight * multFactor).toInt() + } + originalWidth > originalHeight -> { + newWidth = scaledSize + multFactor = originalHeight.toFloat() / originalWidth.toFloat() + newHeight = (newWidth * multFactor).toInt() + } + originalHeight == originalWidth -> { + newHeight = scaledSize + newWidth = scaledSize + } + } + return Bitmap.createScaledBitmap(source, newWidth, newHeight, false) + } + + private fun doScan(data: Uri): Result { + Log.d(TAG, "Starting to scan an image: $data") + contentResolver.openInputStream(data).use { inputStream -> + val originalBitmap = BitmapFactory.decodeStream(inputStream) + ?: throw IllegalArgumentException("Can't decode stream to Bitmap") + + return try { + scanBitmapForResult(originalBitmap).also { + Log.d(TAG, "Found result in original image") + } + } catch (e: Exception) { + Log.e(TAG, "Original image scan finished with error: $e, will try downscaled image") + val scaleBitmap = downscaleBitmap(originalBitmap, 500) + scanBitmapForResult(originalBitmap).also { scaleBitmap.recycle() } + } finally { + originalBitmap.recycle() + } + } + + } + + /** + * Attempts to parse incoming data + * @return result of the decoding operation + * @throws NotFoundException when parser didn't find QR code in the image + */ + suspend fun scan(data: Uri) = withContext(Dispatchers.Default) { doScan(data) } + + /** + * Given a reference to a file, check if this file could be parsed by this class + * @return true if the file can be parsed, false if not + */ + fun validContentType(data: Uri): Boolean { + return contentResolver.getType(data)?.startsWith("image/") == true + } + + companion object { + private const val TAG = "QrCodeFromFileScanner" + } +} diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 2754c632..6c090199 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -80,6 +80,8 @@ <string name="bad_config_reason_unknown_section">Unknown section</string> <string name="bad_config_reason_value_out_of_range">Value out of range</string> <string name="bad_extension_error">File must be .conf or .zip</string> + <string name="error_no_qr_found">QR code not found in image</string> + <string name="error_qr_checksum">QR code checksum verification failed</string> <string name="cancel">Cancel</string> <string name="config_delete_error">Cannot delete configuration file %s</string> <string name="config_exists_error">Configuration for “%s” already exists</string> |