Skip to content

Commit 0bd3930

Browse files
Deishelonzx2c4
authored andcommitted
ui: allow importing tunnel from an QR image stored on the device
Add a new feature to import a tunnel from a saved QR image, this feature integrates into 'import from file' flow, however adds a condition, if file is an image, attempt to parse it as QR image file. My use case for this feature, is to allow easier sharing of tunnels to family. Scanning QR code is ok when you have an external display to show it, but if you sent QR code to someone, there is no way to import it in the app. If you share a config file, that becomes way harder for a non-technical person to import as now they need to find a file with that name in the file picker etc etc, Where the images are very visible in the file picker, and user can easily recognize it for import. Testing: - Click "+" blue button, try to import a valid `.conf` file - the 'original' file flow should not be affected - Click "+" blue button, try to import a valid QR code image - if QR code was parsed, then a new tunnel will be added. - Click "+" blue button, try to import an invalid QR code image - Error message will be shown Signed-off-by: Nikita Pustovoi <[email protected]> Signed-off-by: Harsh Shandilya <[email protected]>
1 parent 751ce54 commit 0bd3930

File tree

4 files changed

+136
-1
lines changed

4 files changed

+136
-1
lines changed

ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import androidx.appcompat.app.AppCompatActivity
2121
import androidx.appcompat.view.ActionMode
2222
import androidx.lifecycle.lifecycleScope
2323
import com.google.android.material.snackbar.Snackbar
24+
import com.google.zxing.qrcode.QRCodeReader
2425
import com.journeyapps.barcodescanner.ScanContract
2526
import com.journeyapps.barcodescanner.ScanOptions
2627
import com.wireguard.android.Application
@@ -31,6 +32,7 @@ import com.wireguard.android.databinding.TunnelListFragmentBinding
3132
import com.wireguard.android.databinding.TunnelListItemBinding
3233
import com.wireguard.android.model.ObservableTunnel
3334
import com.wireguard.android.util.ErrorMessages
35+
import com.wireguard.android.util.QrCodeFromFileScanner
3436
import com.wireguard.android.util.TunnelImporter
3537
import com.wireguard.android.widget.MultiselectableRelativeLayout
3638
import kotlinx.coroutines.SupervisorJob
@@ -52,7 +54,20 @@ class TunnelListFragment : BaseFragment() {
5254
val activity = activity ?: return@registerForActivityResult
5355
val contentResolver = activity.contentResolver ?: return@registerForActivityResult
5456
activity.lifecycleScope.launch {
55-
TunnelImporter.importTunnel(contentResolver, data) { showSnackbar(it) }
57+
val qrCodeFromFileScanner = QrCodeFromFileScanner(contentResolver, QRCodeReader())
58+
if (qrCodeFromFileScanner.validContentType(data)) {
59+
try {
60+
val result = qrCodeFromFileScanner.scan(data)
61+
TunnelImporter.importTunnel(parentFragmentManager, result.text) { showSnackbar(it) }
62+
} catch (e: Exception) {
63+
val error = ErrorMessages[e]
64+
val message = requireContext().getString(R.string.import_error, error)
65+
Log.e(TAG, message, e)
66+
showSnackbar(message)
67+
}
68+
} else {
69+
TunnelImporter.importTunnel(contentResolver, data) { showSnackbar(it) }
70+
}
5671
}
5772
}
5873

ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package com.wireguard.android.util
66

77
import android.content.res.Resources
88
import android.os.RemoteException
9+
import com.google.zxing.ChecksumException
10+
import com.google.zxing.NotFoundException
911
import com.wireguard.android.Application
1012
import com.wireguard.android.R
1113
import com.wireguard.android.backend.BackendException
@@ -84,6 +86,12 @@ object ErrorMessages {
8486
rootCause is RootShellException -> {
8587
resources.getString(RSE_REASON_MAP.getValue(rootCause.reason), *rootCause.format)
8688
}
89+
rootCause is NotFoundException -> {
90+
resources.getString(R.string.error_no_qr_found)
91+
}
92+
rootCause is ChecksumException -> {
93+
resources.getString(R.string.error_qr_checksum)
94+
}
8795
rootCause.message != null -> {
8896
rootCause.message!!
8997
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright © 2017-2022 WireGuard LLC. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package com.wireguard.android.util
7+
8+
import android.content.ContentResolver
9+
import android.graphics.Bitmap
10+
import android.graphics.BitmapFactory
11+
import android.net.Uri
12+
import android.util.Log
13+
import com.google.zxing.BinaryBitmap
14+
import com.google.zxing.DecodeHintType
15+
import com.google.zxing.NotFoundException
16+
import com.google.zxing.RGBLuminanceSource
17+
import com.google.zxing.Reader
18+
import com.google.zxing.Result
19+
import com.google.zxing.common.HybridBinarizer
20+
import kotlinx.coroutines.Dispatchers
21+
import kotlinx.coroutines.withContext
22+
23+
/**
24+
* Encapsulates the logic of scanning a barcode from a file,
25+
* @property contentResolver - Resolver to read the incoming data
26+
* @property reader - An instance of zxing's [Reader] class to parse the image
27+
*/
28+
class QrCodeFromFileScanner(
29+
private val contentResolver: ContentResolver,
30+
private val reader: Reader,
31+
) {
32+
33+
private fun scanBitmapForResult(source: Bitmap): Result {
34+
val width = source.width
35+
val height = source.height
36+
val pixels = IntArray(width * height)
37+
source.getPixels(pixels, 0, width, 0, 0, width, height)
38+
39+
val bBitmap = BinaryBitmap(HybridBinarizer(RGBLuminanceSource(width, height, pixels)))
40+
return reader.decode(bBitmap, mapOf(DecodeHintType.TRY_HARDER to true))
41+
}
42+
43+
private fun downscaleBitmap(source: Bitmap, scaledSize: Int): Bitmap {
44+
45+
val originalWidth = source.width
46+
val originalHeight = source.height
47+
48+
var newWidth = -1
49+
var newHeight = -1
50+
val multFactor: Float
51+
52+
when {
53+
originalHeight > originalWidth -> {
54+
newHeight = scaledSize
55+
multFactor = originalWidth.toFloat() / originalHeight.toFloat()
56+
newWidth = (newHeight * multFactor).toInt()
57+
}
58+
originalWidth > originalHeight -> {
59+
newWidth = scaledSize
60+
multFactor = originalHeight.toFloat() / originalWidth.toFloat()
61+
newHeight = (newWidth * multFactor).toInt()
62+
}
63+
originalHeight == originalWidth -> {
64+
newHeight = scaledSize
65+
newWidth = scaledSize
66+
}
67+
}
68+
return Bitmap.createScaledBitmap(source, newWidth, newHeight, false)
69+
}
70+
71+
private fun doScan(data: Uri): Result {
72+
Log.d(TAG, "Starting to scan an image: $data")
73+
contentResolver.openInputStream(data).use { inputStream ->
74+
val originalBitmap = BitmapFactory.decodeStream(inputStream)
75+
?: throw IllegalArgumentException("Can't decode stream to Bitmap")
76+
77+
return try {
78+
scanBitmapForResult(originalBitmap).also {
79+
Log.d(TAG, "Found result in original image")
80+
}
81+
} catch (e: Exception) {
82+
Log.e(TAG, "Original image scan finished with error: $e, will try downscaled image")
83+
val scaleBitmap = downscaleBitmap(originalBitmap, 500)
84+
scanBitmapForResult(originalBitmap).also { scaleBitmap.recycle() }
85+
} finally {
86+
originalBitmap.recycle()
87+
}
88+
}
89+
90+
}
91+
92+
/**
93+
* Attempts to parse incoming data
94+
* @return result of the decoding operation
95+
* @throws NotFoundException when parser didn't find QR code in the image
96+
*/
97+
suspend fun scan(data: Uri) = withContext(Dispatchers.Default) { doScan(data) }
98+
99+
/**
100+
* Given a reference to a file, check if this file could be parsed by this class
101+
* @return true if the file can be parsed, false if not
102+
*/
103+
fun validContentType(data: Uri): Boolean {
104+
return contentResolver.getType(data)?.startsWith("image/") == true
105+
}
106+
107+
companion object {
108+
private const val TAG = "QrCodeFromFileScanner"
109+
}
110+
}

ui/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@
8080
<string name="bad_config_reason_unknown_section">Unknown section</string>
8181
<string name="bad_config_reason_value_out_of_range">Value out of range</string>
8282
<string name="bad_extension_error">File must be .conf or .zip</string>
83+
<string name="error_no_qr_found">QR code not found in image</string>
84+
<string name="error_qr_checksum">QR code checksum verification failed</string>
8385
<string name="cancel">Cancel</string>
8486
<string name="config_delete_error">Cannot delete configuration file %s</string>
8587
<string name="config_exists_error">Configuration for “%s” already exists</string>

0 commit comments

Comments
 (0)