diff options
Diffstat (limited to 'ui/src/main')
-rw-r--r-- | ui/src/main/AndroidManifest.xml | 3 | ||||
-rw-r--r-- | ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt | 17 | ||||
-rw-r--r-- | ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt | 8 | ||||
-rw-r--r-- | ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt | 110 | ||||
-rw-r--r-- | ui/src/main/res/layout/tunnel_list_fragment.xml | 2 | ||||
-rw-r--r-- | ui/src/main/res/values/dimens.xml | 1 | ||||
-rw-r--r-- | ui/src/main/res/values/strings.xml | 2 | ||||
-rw-r--r-- | ui/src/main/res/xml/preferences.xml | 5 |
8 files changed, 146 insertions, 2 deletions
diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml index 4c957bd..4dd38cb 100644 --- a/ui/src/main/AndroidManifest.xml +++ b/ui/src/main/AndroidManifest.xml @@ -41,7 +41,8 @@ <activity android:name=".activity.TunnelToggleActivity" - android:theme="@style/NoBackgroundTheme" /> + android:theme="@style/NoBackgroundTheme" + android:excludeFromRecents="true"/> <activity android:name=".activity.MainActivity"> <intent-filter> 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 71e409a..390a639 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) } + if (QrCodeFromFileScanner.validContentType(contentResolver, data)) { + try { + val qrCodeFromFileScanner = QrCodeFromFileScanner(contentResolver, QRCodeReader()) + 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 9369415..60c6b87 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 0000000..3e54a52 --- /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) } + + companion object { + private const val TAG = "QrCodeFromFileScanner" + + /** + * 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(contentResolver: ContentResolver, data: Uri): Boolean { + return contentResolver.getType(data)?.startsWith("image/") == true + } + } +} diff --git a/ui/src/main/res/layout/tunnel_list_fragment.xml b/ui/src/main/res/layout/tunnel_list_fragment.xml index 436b63d..42a6ced 100644 --- a/ui/src/main/res/layout/tunnel_list_fragment.xml +++ b/ui/src/main/res/layout/tunnel_list_fragment.xml @@ -60,6 +60,8 @@ android:src="@mipmap/ic_launcher" /> <TextView + android:layout_marginStart="@dimen/tunnel_list_placeholder_margin" + android:layout_marginEnd="@dimen/tunnel_list_placeholder_margin" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" diff --git a/ui/src/main/res/values/dimens.xml b/ui/src/main/res/values/dimens.xml index c6abf8e..ddb4dea 100644 --- a/ui/src/main/res/values/dimens.xml +++ b/ui/src/main/res/values/dimens.xml @@ -6,4 +6,5 @@ <dimen name="normal_margin">8dp</dimen> <dimen name="bottom_sheet_top_padding">8dp</dimen> <dimen name="bottom_sheet_icon_padding">16dp</dimen> + <dimen name="tunnel_list_placeholder_margin">16dp</dimen> </resources> diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 2754c63..6c09019 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> diff --git a/ui/src/main/res/xml/preferences.xml b/ui/src/main/res/xml/preferences.xml index 5ab5c79..5c9505d 100644 --- a/ui/src/main/res/xml/preferences.xml +++ b/ui/src/main/res/xml/preferences.xml @@ -7,23 +7,27 @@ <CheckBoxPreference android:defaultValue="false" android:key="restore_on_boot" + android:singleLineTitle="false" android:summaryOff="@string/restore_on_boot_summary_off" android:summaryOn="@string/restore_on_boot_summary_on" android:title="@string/restore_on_boot_title" /> <com.wireguard.android.preference.ZipExporterPreference android:key="zip_exporter" /> <Preference android:key="log_viewer" + android:singleLineTitle="false" android:summary="@string/log_viewer_pref_summary" android:title="@string/log_viewer_pref_title" /> <CheckBoxPreference android:defaultValue="false" android:key="dark_theme" + android:singleLineTitle="false" android:summaryOff="@string/dark_theme_summary_off" android:summaryOn="@string/dark_theme_summary_on" android:title="@string/dark_theme_title" /> <CheckBoxPreference android:defaultValue="false" android:key="multiple_tunnels" + android:singleLineTitle="false" android:summaryOff="@string/multiple_tunnels_summary_off" android:summaryOn="@string/multiple_tunnels_summary_on" android:title="@string/multiple_tunnels_title" /> @@ -32,6 +36,7 @@ <CheckBoxPreference android:defaultValue="false" android:key="allow_remote_control_intents" + android:singleLineTitle="false" android:summaryOff="@string/allow_remote_control_intents_summary_off" android:summaryOn="@string/allow_remote_control_intents_summary_on" android:title="@string/allow_remote_control_intents_title" /> |