diff --git a/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerView.kt b/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerView.kt index 184a6b5a..92cf928b 100644 --- a/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerView.kt +++ b/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerView.kt @@ -36,7 +36,9 @@ import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat.startActivity import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.Observer -import com.google.zxing.* +import com.google.zxing.BinaryBitmap +import com.google.zxing.NotFoundException +import com.google.zxing.RGBLuminanceSource import com.google.zxing.common.HybridBinarizer import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodChannel @@ -290,11 +292,6 @@ internal class QRScannerView( ) : ImageAnalysis.Analyzer { var analysisPaused = false - - val multiFormatReader = MultiFormatReader().also { - it.setHints(mapOf(DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE))) - } - var analyzedImagesCount = 0 private fun ByteBuffer.toByteArray(lastRowPadding: Int): ByteArray { @@ -392,14 +389,14 @@ internal class QRScannerView( fullSize } - val result: com.google.zxing.Result = multiFormatReader.decode(bitmapToProcess) + val result = QrCodeScanner.scan(bitmapToProcess) if (analysisPaused) { return } analysisPaused = true // pause - Log.v(TAG, "Analysis result: ${result.text}") - listener.invoke(Result.success(result.text)) + Log.v(TAG, "Analysis result: $result") + listener.invoke(Result.success(result)) } catch (_: NotFoundException) { if (analyzedImagesCount == 0) { Log.v(TAG, " No QR code found (NotFoundException)") diff --git a/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerZxingPlugin.kt b/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerZxingPlugin.kt index 64707a74..803402b8 100644 --- a/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerZxingPlugin.kt +++ b/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerZxingPlugin.kt @@ -16,6 +16,12 @@ package com.yubico.authenticator.flutter_plugins.qrscanner_zxing +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Log +import com.google.zxing.BinaryBitmap +import com.google.zxing.RGBLuminanceSource +import com.google.zxing.common.HybridBinarizer import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -24,6 +30,7 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.PluginRegistry +import java.lang.Exception class PermissionsResultRegistrar { @@ -65,10 +72,48 @@ class QRScannerZxingPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, } override fun onMethodCall(call: MethodCall, result: Result) { - if (call.method == "getPlatformVersion") { - result.success("Android ${android.os.Build.VERSION.RELEASE}") - } else { - result.notImplemented() + when (call.method) { + "getPlatformVersion" -> { + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } + "scanBitmap" -> { + val imageFileData = call.argument("bitmap") + var bitmap: Bitmap? = null + try { + imageFileData?.let { byteArray -> + Log.i(TAG, "Received ${byteArray.size} bytes") + val options = BitmapFactory.Options() + options.inSampleSize = 4 + bitmap = BitmapFactory.decodeByteArray(imageFileData, 0, byteArray.size, options) + bitmap?.let { + val intArray = IntArray(it.allocationByteCount) + it.getPixels(intArray, 0, it.width, 0, 0, it.width, it.height) + val luminanceSource = + RGBLuminanceSource(it.rowBytes, it.height, intArray) + val binaryBitmap = BinaryBitmap(HybridBinarizer(luminanceSource)) + val scanResult = QrCodeScanner.scan(binaryBitmap) + Log.i(TAG, "Scan result: $scanResult") + result.success(scanResult) + return + } + } + } catch (e: Exception) { + Log.e(TAG, "Failure decoding data: $e") + result.error("Failed to decode", e.message, e) + return + } finally { + bitmap?.let { + it.recycle() + bitmap = null + } + } + + Log.e(TAG, "Failure decoding data: Invalid image format ") + result.error("Failed to decode", "Invalid image format", null) + } + else -> { + result.notImplemented() + } } } @@ -97,4 +142,8 @@ class QRScannerZxingPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, ): Boolean { return registrar.onResult(requestCode, permissions, grantResults) } + + companion object { + const val TAG = "QRScannerZxPlugin" + } } diff --git a/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QrScanner.kt b/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QrScanner.kt new file mode 100644 index 00000000..c34ad3ad --- /dev/null +++ b/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QrScanner.kt @@ -0,0 +1,18 @@ +package com.yubico.authenticator.flutter_plugins.qrscanner_zxing + +import com.google.zxing.BarcodeFormat +import com.google.zxing.BinaryBitmap +import com.google.zxing.DecodeHintType +import com.google.zxing.MultiFormatReader + +object QrCodeScanner { + + private val qrCodeScanner = MultiFormatReader().also { + it.setHints(mapOf(DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE))) + } + + fun scan(binaryBitmap: BinaryBitmap) : String { + val result: com.google.zxing.Result = qrCodeScanner.decode(binaryBitmap) + return result.text + } +} \ No newline at end of file diff --git a/android/flutter_plugins/qrscanner_zxing/example/android/app/build.gradle b/android/flutter_plugins/qrscanner_zxing/example/android/app/build.gradle index 69894779..ba756aea 100644 --- a/android/flutter_plugins/qrscanner_zxing/example/android/app/build.gradle +++ b/android/flutter_plugins/qrscanner_zxing/example/android/app/build.gradle @@ -26,9 +26,11 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 33 + compileSdk 34 ndkVersion flutter.ndkVersion + namespace 'com.yubico.authenticator.flutter_plugins.qrscanner_zxing_example' + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -48,7 +50,7 @@ android { // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } @@ -60,6 +62,10 @@ android { signingConfig signingConfigs.debug } } + + buildFeatures { + buildConfig true + } } flutter { diff --git a/android/flutter_plugins/qrscanner_zxing/example/android/build.gradle b/android/flutter_plugins/qrscanner_zxing/example/android/build.gradle index 717cd8ce..ad5d690e 100644 --- a/android/flutter_plugins/qrscanner_zxing/example/android/build.gradle +++ b/android/flutter_plugins/qrscanner_zxing/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.7.21' + ext.kotlin_version = '1.9.10' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' + classpath 'com.android.tools.build:gradle:8.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register('clean', Delete) { delete rootProject.buildDir } diff --git a/android/flutter_plugins/qrscanner_zxing/example/android/gradle/wrapper/gradle-wrapper.properties b/android/flutter_plugins/qrscanner_zxing/example/android/gradle/wrapper/gradle-wrapper.properties index cc5527d7..d07c7fcf 100644 --- a/android/flutter_plugins/qrscanner_zxing/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/flutter_plugins/qrscanner_zxing/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip diff --git a/android/flutter_plugins/qrscanner_zxing/example/lib/main.dart b/android/flutter_plugins/qrscanner_zxing/example/lib/main.dart index ba12d6e0..12c2c55c 100644 --- a/android/flutter_plugins/qrscanner_zxing/example/lib/main.dart +++ b/android/flutter_plugins/qrscanner_zxing/example/lib/main.dart @@ -14,7 +14,9 @@ * limitations under the License. */ +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:qrscanner_zxing/qrscanner_zxing_method_channel.dart'; import 'package:qrscanner_zxing/qrscanner_zxing_view.dart'; import 'cutout_overlay.dart'; @@ -64,6 +66,32 @@ class AppHomePage extends StatelessWidget { )); }, child: const Text("Open QR Scanner")), + ElevatedButton( + onPressed: () async { + final result = await FilePicker.platform.pickFiles( + allowedExtensions: ['png', 'jpg'], + type: FileType.custom, + allowMultiple: false, + lockParentWindow: true, + withData: true, + dialogTitle: 'Select file with QR code'); + + if (result == null || !result.isSinglePick) { + // no result + return; + } + + final bytes = result.files.first.bytes; + + if (bytes != null) { + var channel = MethodChannelQRScannerZxing(); + var value = await channel.scanBitmap(bytes); + debugPrint(value); + } else { + // no files selected + } + }, + child: const Text("Scan from file")), ], ), ), diff --git a/android/flutter_plugins/qrscanner_zxing/example/pubspec.lock b/android/flutter_plugins/qrscanner_zxing/example/pubspec.lock index 9c76c768..ffd0042d 100644 --- a/android/flutter_plugins/qrscanner_zxing/example/pubspec.lock +++ b/android/flutter_plugins/qrscanner_zxing/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" cupertino_icons: dependency: "direct main" description: @@ -57,6 +57,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 + url: "https://pub.dev" + source: hosted + version: "5.5.0" flutter: dependency: "direct main" description: flutter @@ -70,19 +86,24 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c + url: "https://pub.dev" + source: hosted + version: "2.0.16" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" - js: + flutter_web_plugins: dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" + description: flutter + source: sdk + version: "0.0.0" lints: dependency: transitive description: @@ -95,18 +116,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: @@ -127,10 +148,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.6" qrscanner_zxing: dependency: "direct main" description: @@ -147,10 +168,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: @@ -187,10 +208,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" vector_math: dependency: transitive description: @@ -199,6 +220,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" + win32: + dependency: transitive + description: + name: win32 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + url: "https://pub.dev" + source: hosted + version: "5.0.9" sdks: - dart: ">=3.0.0-0 <4.0.0" - flutter: ">=2.5.0" + dart: ">=3.1.0-185.0.dev <4.0.0" + flutter: ">=3.7.0" diff --git a/android/flutter_plugins/qrscanner_zxing/example/pubspec.yaml b/android/flutter_plugins/qrscanner_zxing/example/pubspec.yaml index ab2e89d5..067b0b84 100644 --- a/android/flutter_plugins/qrscanner_zxing/example/pubspec.yaml +++ b/android/flutter_plugins/qrscanner_zxing/example/pubspec.yaml @@ -4,7 +4,7 @@ description: Demonstrates how to use the qrscanner_zxing plugin. publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: - sdk: ">=2.17.0-266.1.beta <3.0.0" + sdk: '>=3.0.0 <4.0.0' dependencies: flutter: @@ -14,6 +14,7 @@ dependencies: path: ../ cupertino_icons: ^1.0.2 + file_picker: ^5.3.2 dev_dependencies: flutter_test: diff --git a/android/flutter_plugins/qrscanner_zxing/lib/qrscanner_zxing.dart b/android/flutter_plugins/qrscanner_zxing/lib/qrscanner_zxing.dart index 568bc096..2a339999 100644 --- a/android/flutter_plugins/qrscanner_zxing/lib/qrscanner_zxing.dart +++ b/android/flutter_plugins/qrscanner_zxing/lib/qrscanner_zxing.dart @@ -14,10 +14,16 @@ * limitations under the License. */ +import 'dart:typed_data'; + import 'qrscanner_zxing_platform_interface.dart'; class QRScannerZxing { Future getPlatformVersion() { return QRScannerZxingPlatform.instance.getPlatformVersion(); } + + Future scanBitmap(Uint8List bitmap) { + return QRScannerZxingPlatform.instance.scanBitmap(bitmap); + } } diff --git a/android/flutter_plugins/qrscanner_zxing/lib/qrscanner_zxing_method_channel.dart b/android/flutter_plugins/qrscanner_zxing/lib/qrscanner_zxing_method_channel.dart index 1e46314c..8642fbbb 100644 --- a/android/flutter_plugins/qrscanner_zxing/lib/qrscanner_zxing_method_channel.dart +++ b/android/flutter_plugins/qrscanner_zxing/lib/qrscanner_zxing_method_channel.dart @@ -31,4 +31,11 @@ class MethodChannelQRScannerZxing extends QRScannerZxingPlatform { await methodChannel.invokeMethod('getPlatformVersion'); return version; } + + @override + Future scanBitmap(Uint8List bitmap) async { + final version = await methodChannel + .invokeMethod('scanBitmap', {'bitmap': bitmap}); + return version; + } } diff --git a/android/flutter_plugins/qrscanner_zxing/lib/qrscanner_zxing_platform_interface.dart b/android/flutter_plugins/qrscanner_zxing/lib/qrscanner_zxing_platform_interface.dart index 54aa6cc0..54740987 100644 --- a/android/flutter_plugins/qrscanner_zxing/lib/qrscanner_zxing_platform_interface.dart +++ b/android/flutter_plugins/qrscanner_zxing/lib/qrscanner_zxing_platform_interface.dart @@ -14,6 +14,8 @@ * limitations under the License. */ +import 'dart:typed_data'; + import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'qrscanner_zxing_method_channel.dart'; @@ -42,4 +44,8 @@ abstract class QRScannerZxingPlatform extends PlatformInterface { Future getPlatformVersion() { throw UnimplementedError('platformVersion() has not been implemented.'); } + + Future scanBitmap(Uint8List bitmap) { + throw UnimplementedError('platformVersion() has not been implemented.'); + } } diff --git a/android/flutter_plugins/qrscanner_zxing/test/qrscanner_zxing_test.dart b/android/flutter_plugins/qrscanner_zxing/test/qrscanner_zxing_test.dart index 7020c926..85ccd7d1 100644 --- a/android/flutter_plugins/qrscanner_zxing/test/qrscanner_zxing_test.dart +++ b/android/flutter_plugins/qrscanner_zxing/test/qrscanner_zxing_test.dart @@ -14,6 +14,8 @@ * limitations under the License. */ +import 'dart:typed_data'; + import 'package:flutter_test/flutter_test.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:qrscanner_zxing/qrscanner_zxing.dart'; @@ -25,6 +27,10 @@ class MockQRScannerZxingPlatform implements QRScannerZxingPlatform { @override Future getPlatformVersion() => Future.value('42'); + + @override + Future scanBitmap(Uint8List bitmap) => + Future.value(bitmap.length.toString()); } void main() { @@ -42,4 +48,12 @@ void main() { expect(await qrscannerZxingPlugin.getPlatformVersion(), '42'); }); + + test('scanBitmap', () async { + QRScannerZxing qrscannerZxingPlugin = QRScannerZxing(); + MockQRScannerZxingPlatform fakePlatform = MockQRScannerZxingPlatform(); + QRScannerZxingPlatform.instance = fakePlatform; + + expect(await qrscannerZxingPlugin.scanBitmap(Uint8List(10)), '10'); + }); } diff --git a/lib/android/keys.dart b/lib/android/keys.dart index 75167483..2dcee72a 100755 --- a/lib/android/keys.dart +++ b/lib/android/keys.dart @@ -22,6 +22,7 @@ const _prefix = 'android.keys'; const okButton = Key('$_prefix.ok'); const manualEntryButton = Key('$_prefix.manual_entry'); +const readFromImage = Key('$_prefix.read_image_file'); const nfcBypassTouchSetting = Key('$_prefix.nfc_bypass_touch'); const nfcSilenceSoundsSettings = Key('$_prefix.nfc_silence_sounds'); diff --git a/lib/android/qr_scanner/qr_scanner_provider.dart b/lib/android/qr_scanner/qr_scanner_provider.dart index 764099bd..d5865665 100644 --- a/lib/android/qr_scanner/qr_scanner_provider.dart +++ b/lib/android/qr_scanner/qr_scanner_provider.dart @@ -14,39 +14,50 @@ * limitations under the License. */ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:yubico_authenticator/app/state.dart'; import 'package:yubico_authenticator/exception/cancellation_exception.dart'; import 'package:yubico_authenticator/theme.dart'; +import 'package:qrscanner_zxing/qrscanner_zxing_method_channel.dart'; + import 'qr_scanner_view.dart'; class AndroidQrScanner implements QrScanner { final WithContext _withContext; + AndroidQrScanner(this._withContext); @override - Future scanQr([String? _]) async { - var scannedCode = await _withContext( - (context) async => await Navigator.of(context).push(PageRouteBuilder( - pageBuilder: (_, __, ___) => - Theme(data: AppTheme.darkTheme, child: const QrScannerView()), - settings: const RouteSettings(name: 'android_qr_scanner_view'), - transitionDuration: const Duration(seconds: 0), - reverseTransitionDuration: const Duration(seconds: 0), - ))); - if (scannedCode == null) { - // user has cancelled the scan - throw CancellationException(); + Future scanQr([String? imageData]) async { + if (imageData == null) { + var scannedCode = await _withContext( + (context) async => + await Navigator.of(context).push(PageRouteBuilder( + pageBuilder: (_, __, ___) => + Theme(data: AppTheme.darkTheme, child: const QrScannerView()), + settings: const RouteSettings(name: 'android_qr_scanner_view'), + transitionDuration: const Duration(seconds: 0), + reverseTransitionDuration: const Duration(seconds: 0), + ))); + if (scannedCode == null) { + // user has cancelled the scan + throw CancellationException(); + } + if (scannedCode == '') { + return null; + } + return scannedCode; + } else { + var zxingChannel = MethodChannelQRScannerZxing(); + return await zxingChannel.scanBitmap(base64Decode(imageData)); } - if (scannedCode == '') { - return null; - } - return scannedCode; } } QrScanner? Function(dynamic) androidQrScannerProvider(hasCamera) { return (ref) => - hasCamera ? AndroidQrScanner(ref.watch(withContextProvider)) : null; + hasCamera ? AndroidQrScanner(ref.watch(withContextProvider)) : null; } diff --git a/lib/android/qr_scanner/qr_scanner_ui_view.dart b/lib/android/qr_scanner/qr_scanner_ui_view.dart index 45c396e8..421b113d 100644 --- a/lib/android/qr_scanner/qr_scanner_ui_view.dart +++ b/lib/android/qr_scanner/qr_scanner_ui_view.dart @@ -14,13 +14,18 @@ * limitations under the License. */ +import 'dart:convert'; + +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:yubico_authenticator/app/state.dart'; import '../keys.dart' as keys; import 'qr_scanner_scan_status.dart'; -class QRScannerUI extends StatelessWidget { +class QRScannerUI extends ConsumerWidget { final ScanStatus status; final Size screenSize; final GlobalKey overlayWidgetKey; @@ -32,7 +37,7 @@ class QRScannerUI extends StatelessWidget { required this.overlayWidgetKey}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context)!; return Stack( @@ -73,15 +78,51 @@ class QRScannerUI extends StatelessWidget { textScaleFactor: 0.7, style: const TextStyle(color: Colors.white), ), - OutlinedButton( - onPressed: () { - Navigator.of(context).pop(''); - }, - key: keys.manualEntryButton, - child: Text( - l10n.s_enter_manually, - style: const TextStyle(color: Colors.white), - )), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + OutlinedButton( + onPressed: () { + Navigator.of(context).pop(''); + }, + key: keys.manualEntryButton, + child: Text( + l10n.s_enter_manually, + style: const TextStyle(color: Colors.white), + )), + OutlinedButton( + onPressed: () async { + Navigator.of(context).pop(''); + final result = await FilePicker.platform.pickFiles( + allowedExtensions: ['png', 'jpg'], + type: FileType.custom, + allowMultiple: false, + lockParentWindow: true, + dialogTitle: 'Select file with QR code'); + if (result != null && result.files.isNotEmpty) { + final fileWithCode = result.files.first; + final bytes = fileWithCode.bytes; + if (bytes == null || bytes.isEmpty) { + //err return + return; + } + if (bytes.length > 3 * 1024 * 1024) { + // too big file + return; + } + final scanner = ref.read(qrScannerProvider); + if (scanner != null) { + await scanner.scanQr(base64UrlEncode(bytes)); + } + } + }, + key: keys.readFromImage, + child: Text( + l10n.s_read_from_image, + style: const TextStyle(color: Colors.white), + )), + ], + ), ], ), const SizedBox(height: 8) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d041dcff..0d78d8ae 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -516,6 +516,7 @@ "q_want_to_scan": "Would like to scan?", "q_no_qr": "No QR code?", "s_enter_manually": "Enter manually", + "s_read_from_image": "Provide image file", "@_factory_reset": {}, "s_reset": "Reset",