Read QR code from file on Android

This commit is contained in:
Adam Velebil 2023-10-18 15:34:31 +02:00
parent 8a9d465bb1
commit 184e7a7f2c
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
7 changed files with 94 additions and 98 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2022 Yubico. * Copyright (C) 2022-2023 Yubico.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,14 +28,14 @@ class MethodChannelQRScannerZxing extends QRScannerZxingPlatform {
@override @override
Future<String?> getPlatformVersion() async { Future<String?> getPlatformVersion() async {
final version = final version =
await methodChannel.invokeMethod<String>('getPlatformVersion'); await methodChannel.invokeMethod<String>('getPlatformVersion');
return version; return version;
} }
@override @override
Future<String?> scanBitmap(Uint8List bytes) async { Future<String?> scanBitmap(Uint8List bytes) async {
final version = await methodChannel final result = await methodChannel
.invokeMethod<String>('scanBitmap', {'bytes': bytes}); .invokeMethod<String>('scanBitmap', {'bytes': bytes});
return version; return result;
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2022 Yubico. * Copyright (C) 2022-2023 Yubico.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,16 +16,25 @@
import 'dart:convert'; import 'dart:convert';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qrscanner_zxing/qrscanner_zxing_method_channel.dart';
import 'package:yubico_authenticator/app/state.dart'; import 'package:yubico_authenticator/app/state.dart';
import 'package:yubico_authenticator/exception/cancellation_exception.dart'; import 'package:yubico_authenticator/exception/cancellation_exception.dart';
import 'package:yubico_authenticator/theme.dart'; import 'package:yubico_authenticator/theme.dart';
import 'package:qrscanner_zxing/qrscanner_zxing_method_channel.dart'; import '../../app/message.dart';
import '../../oath/views/add_account_page.dart';
import '../../oath/views/utils.dart';
import 'qr_scanner_view.dart'; import 'qr_scanner_view.dart';
class AndroidQrScanner implements QrScanner { class AndroidQrScanner implements QrScanner {
static const String kQrScannerRequestManualEntry =
'__QR_SCANNER_ENTER_MANUALLY__';
static const String kQrScannerRequestReadFromFile =
'__QR_SCANNER_SCAN_FROM_FILE__';
final WithContext _withContext; final WithContext _withContext;
AndroidQrScanner(this._withContext); AndroidQrScanner(this._withContext);
@ -33,8 +42,7 @@ class AndroidQrScanner implements QrScanner {
@override @override
Future<String?> scanQr([String? imageData]) async { Future<String?> scanQr([String? imageData]) async {
if (imageData == null) { if (imageData == null) {
var scannedCode = await _withContext( var scannedCode = await _withContext((context) async =>
(context) async =>
await Navigator.of(context).push(PageRouteBuilder( await Navigator.of(context).push(PageRouteBuilder(
pageBuilder: (_, __, ___) => pageBuilder: (_, __, ___) =>
Theme(data: AppTheme.darkTheme, child: const QrScannerView()), Theme(data: AppTheme.darkTheme, child: const QrScannerView()),
@ -55,9 +63,62 @@ class AndroidQrScanner implements QrScanner {
return await zxingChannel.scanBitmap(base64Decode(imageData)); return await zxingChannel.scanBitmap(base64Decode(imageData));
} }
} }
static Future<void> handleScannedData(
String? qrData, WidgetRef ref, AppLocalizations l10n) async {
final withContext = ref.read(withContextProvider);
switch (qrData) {
case null:
break;
case kQrScannerRequestManualEntry:
await withContext((context) => showBlurDialog(
context: context,
routeSettings: const RouteSettings(name: 'oath_add_account'),
builder: (context) {
return const OathAddAccountPage(
null,
null,
credentials: null,
);
},
));
case kQrScannerRequestReadFromFile:
final result = await FilePicker.platform.pickFiles(
allowedExtensions: ['png', 'jpg', 'gif', 'webp'],
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;
final scanner = ref.read(qrScannerProvider);
if (bytes != null && scanner != null) {
final b64bytes = base64Encode(bytes);
final qrData = await scanner.scanQr(b64bytes);
if (qrData != null) {
await withContext((context) =>
handleUri(context, null, qrData, null, null, l10n));
return;
}
}
// no QR code found
await withContext(
(context) async => showMessage(context, l10n.l_qr_not_found));
default:
await withContext(
(context) => handleUri(context, null, qrData, null, null, l10n));
}
}
} }
QrScanner? Function(dynamic) androidQrScannerProvider(hasCamera) { QrScanner? Function(dynamic) androidQrScannerProvider(hasCamera) {
return (ref) => return (ref) =>
hasCamera ? AndroidQrScanner(ref.watch(withContextProvider)) : null; hasCamera ? AndroidQrScanner(ref.watch(withContextProvider)) : null;
} }

View File

@ -14,13 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
import 'dart:convert';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:yubico_authenticator/app/state.dart'; import 'package:yubico_authenticator/android/qr_scanner/qr_scanner_provider.dart';
import '../keys.dart' as keys; import '../keys.dart' as keys;
import 'qr_scanner_scan_status.dart'; import 'qr_scanner_scan_status.dart';
@ -79,53 +76,34 @@ class QRScannerUI extends ConsumerWidget {
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
OutlinedButton( OutlinedButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(''); Navigator.of(context).pop(
AndroidQrScanner.kQrScannerRequestManualEntry);
}, },
key: keys.manualEntryButton, key: keys.manualEntryButton,
child: Text( child: Text(
l10n.s_enter_manually, l10n.s_enter_manually,
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
)), )),
const SizedBox(width: 16),
OutlinedButton( OutlinedButton(
onPressed: () async { onPressed: () {
Navigator.of(context).pop(''); Navigator.of(context).pop(
final result = await FilePicker.platform.pickFiles( AndroidQrScanner.kQrScannerRequestReadFromFile);
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, key: keys.readFromImage,
child: Text( child: Text(
l10n.s_read_from_image, l10n.s_read_from_file,
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
)), ))
], ],
), ),
], ],
), ),
const SizedBox(height: 8) const SizedBox(height: 16)
], ],
), ),
) )

View File

@ -15,19 +15,18 @@
*/ */
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../android/app_methods.dart'; import '../../android/app_methods.dart';
import '../../android/qr_scanner/qr_scanner_provider.dart';
import '../../android/state.dart'; import '../../android/state.dart';
import '../../exception/cancellation_exception.dart';
import '../../core/state.dart'; import '../../core/state.dart';
import '../../exception/cancellation_exception.dart';
import '../../fido/views/fido_screen.dart'; import '../../fido/views/fido_screen.dart';
import '../../oath/views/add_account_page.dart';
import '../../oath/views/oath_screen.dart'; import '../../oath/views/oath_screen.dart';
import '../../oath/views/utils.dart';
import '../../piv/views/piv_screen.dart'; import '../../piv/views/piv_screen.dart';
import '../../widgets/custom_icons.dart'; import '../../widgets/custom_icons.dart';
import '../message.dart';
import '../models.dart'; import '../models.dart';
import '../state.dart'; import '../state.dart';
import 'device_error_screen.dart'; import 'device_error_screen.dart';
@ -99,33 +98,16 @@ class MainPage extends ConsumerWidget {
icon: const Icon(Icons.person_add_alt_1), icon: const Icon(Icons.person_add_alt_1),
tooltip: l10n.s_add_account, tooltip: l10n.s_add_account,
onPressed: () async { onPressed: () async {
final withContext = ref.read(withContextProvider);
final scanner = ref.read(qrScannerProvider); final scanner = ref.read(qrScannerProvider);
if (scanner != null) { if (scanner != null) {
try { try {
final qrData = await scanner.scanQr(); final qrData = await scanner.scanQr();
if (qrData != null) { await AndroidQrScanner.handleScannedData(qrData, ref, l10n);
await withContext((context) =>
handleUri(context, null, qrData, null, null, l10n));
return;
}
} on CancellationException catch (_) { } on CancellationException catch (_) {
// ignored - user cancelled // ignored - user cancelled
return; return;
} }
} }
await withContext((context) => showBlurDialog(
context: context,
routeSettings:
const RouteSettings(name: 'oath_add_account'),
builder: (context) {
return const OathAddAccountPage(
null,
null,
credentials: null,
);
},
));
}, },
), ),
); );

View File

@ -516,7 +516,7 @@
"q_want_to_scan": "Would like to scan?", "q_want_to_scan": "Would like to scan?",
"q_no_qr": "No QR code?", "q_no_qr": "No QR code?",
"s_enter_manually": "Enter manually", "s_enter_manually": "Enter manually",
"s_read_from_image": "Provide image file", "s_read_from_file": "Read from file",
"@_factory_reset": {}, "@_factory_reset": {},
"s_reset": "Reset", "s_reset": "Reset",

View File

@ -30,10 +30,10 @@ import '../../app/message.dart';
import '../../app/models.dart'; import '../../app/models.dart';
import '../../app/state.dart'; import '../../app/state.dart';
import '../../app/views/user_interaction.dart'; import '../../app/views/user_interaction.dart';
import '../../exception/apdu_exception.dart';
import '../../exception/cancellation_exception.dart';
import '../../core/state.dart'; import '../../core/state.dart';
import '../../desktop/models.dart'; import '../../desktop/models.dart';
import '../../exception/apdu_exception.dart';
import '../../exception/cancellation_exception.dart';
import '../../management/models.dart'; import '../../management/models.dart';
import '../../widgets/choice_filter_chip.dart'; import '../../widgets/choice_filter_chip.dart';
import '../../widgets/file_drop_target.dart'; import '../../widgets/file_drop_target.dart';
@ -56,6 +56,7 @@ class OathAddAccountPage extends ConsumerStatefulWidget {
final OathState? state; final OathState? state;
final List<OathCredential>? credentials; final List<OathCredential>? credentials;
final CredentialData? credentialData; final CredentialData? credentialData;
const OathAddAccountPage( const OathAddAccountPage(
this.devicePath, this.devicePath,
this.state, { this.state, {

View File

@ -20,6 +20,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:yubico_authenticator/oath/icon_provider/icon_pack_dialog.dart'; import 'package:yubico_authenticator/oath/icon_provider/icon_pack_dialog.dart';
import 'package:yubico_authenticator/oath/views/add_account_dialog.dart'; import 'package:yubico_authenticator/oath/views/add_account_dialog.dart';
import '../../android/qr_scanner/qr_scanner_provider.dart';
import '../../app/message.dart'; import '../../app/message.dart';
import '../../app/models.dart'; import '../../app/models.dart';
import '../../app/state.dart'; import '../../app/state.dart';
@ -28,11 +29,8 @@ import '../../app/views/action_list.dart';
import '../../core/state.dart'; import '../../core/state.dart';
import '../models.dart'; import '../models.dart';
import '../keys.dart' as keys; import '../keys.dart' as keys;
import '../state.dart';
import 'add_account_page.dart';
import 'manage_password_dialog.dart'; import 'manage_password_dialog.dart';
import 'reset_dialog.dart'; import 'reset_dialog.dart';
import 'utils.dart';
Widget oathBuildActions( Widget oathBuildActions(
BuildContext context, BuildContext context,
@ -59,38 +57,14 @@ Widget oathBuildActions(
icon: const Icon(Icons.person_add_alt_1_outlined), icon: const Icon(Icons.person_add_alt_1_outlined),
onTap: used != null && (capacity == null || capacity > used) onTap: used != null && (capacity == null || capacity > used)
? (context) async { ? (context) async {
final credentials = ref.read(credentialsProvider);
final withContext = ref.read(withContextProvider);
Navigator.of(context).pop(); Navigator.of(context).pop();
if (isAndroid) { if (isAndroid) {
final qrScanner = ref.read(qrScannerProvider); final qrScanner = ref.read(qrScannerProvider);
if (qrScanner != null) { if (qrScanner != null) {
final qrData = await qrScanner.scanQr(); final qrData = await qrScanner.scanQr();
if (qrData != null) { await AndroidQrScanner.handleScannedData(
await withContext((context) => handleUri( qrData, ref, l10n);
context,
credentials,
qrData,
devicePath,
oathState,
l10n,
));
return;
}
} }
await withContext((context) => showBlurDialog(
context: context,
routeSettings:
const RouteSettings(name: 'oath_add_account'),
builder: (context) {
return OathAddAccountPage(
devicePath,
oathState,
credentials: credentials,
credentialData: null,
);
},
));
} else { } else {
await showBlurDialog( await showBlurDialog(
context: context, context: context,