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");
* you may not use this file except in compliance with the License.
@ -28,14 +28,14 @@ class MethodChannelQRScannerZxing extends QRScannerZxingPlatform {
@override
Future<String?> getPlatformVersion() async {
final version =
await methodChannel.invokeMethod<String>('getPlatformVersion');
await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
@override
Future<String?> scanBitmap(Uint8List bytes) async {
final version = await methodChannel
final result = await methodChannel
.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");
* you may not use this file except in compliance with the License.
@ -16,16 +16,25 @@
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:qrscanner_zxing/qrscanner_zxing_method_channel.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 '../../app/message.dart';
import '../../oath/views/add_account_page.dart';
import '../../oath/views/utils.dart';
import 'qr_scanner_view.dart';
class AndroidQrScanner implements QrScanner {
static const String kQrScannerRequestManualEntry =
'__QR_SCANNER_ENTER_MANUALLY__';
static const String kQrScannerRequestReadFromFile =
'__QR_SCANNER_SCAN_FROM_FILE__';
final WithContext _withContext;
AndroidQrScanner(this._withContext);
@ -33,8 +42,7 @@ class AndroidQrScanner implements QrScanner {
@override
Future<String?> scanQr([String? imageData]) async {
if (imageData == null) {
var scannedCode = await _withContext(
(context) async =>
var scannedCode = await _withContext((context) async =>
await Navigator.of(context).push(PageRouteBuilder(
pageBuilder: (_, __, ___) =>
Theme(data: AppTheme.darkTheme, child: const QrScannerView()),
@ -55,9 +63,62 @@ class AndroidQrScanner implements QrScanner {
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) {
return (ref) =>
hasCamera ? AndroidQrScanner(ref.watch(withContextProvider)) : null;
hasCamera ? AndroidQrScanner(ref.watch(withContextProvider)) : null;
}

View File

@ -14,13 +14,10 @@
* 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 'package:yubico_authenticator/android/qr_scanner/qr_scanner_provider.dart';
import '../keys.dart' as keys;
import 'qr_scanner_scan_status.dart';
@ -79,53 +76,34 @@ class QRScannerUI extends ConsumerWidget {
style: const TextStyle(color: Colors.white),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisAlignment: MainAxisAlignment.center,
children: [
OutlinedButton(
onPressed: () {
Navigator.of(context).pop('');
Navigator.of(context).pop(
AndroidQrScanner.kQrScannerRequestManualEntry);
},
key: keys.manualEntryButton,
child: Text(
l10n.s_enter_manually,
style: const TextStyle(color: Colors.white),
)),
const SizedBox(width: 16),
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));
}
}
onPressed: () {
Navigator.of(context).pop(
AndroidQrScanner.kQrScannerRequestReadFromFile);
},
key: keys.readFromImage,
child: Text(
l10n.s_read_from_image,
l10n.s_read_from_file,
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_riverpod/flutter_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../android/app_methods.dart';
import '../../android/qr_scanner/qr_scanner_provider.dart';
import '../../android/state.dart';
import '../../exception/cancellation_exception.dart';
import '../../core/state.dart';
import '../../exception/cancellation_exception.dart';
import '../../fido/views/fido_screen.dart';
import '../../oath/views/add_account_page.dart';
import '../../oath/views/oath_screen.dart';
import '../../oath/views/utils.dart';
import '../../piv/views/piv_screen.dart';
import '../../widgets/custom_icons.dart';
import '../message.dart';
import '../models.dart';
import '../state.dart';
import 'device_error_screen.dart';
@ -99,33 +98,16 @@ class MainPage extends ConsumerWidget {
icon: const Icon(Icons.person_add_alt_1),
tooltip: l10n.s_add_account,
onPressed: () async {
final withContext = ref.read(withContextProvider);
final scanner = ref.read(qrScannerProvider);
if (scanner != null) {
try {
final qrData = await scanner.scanQr();
if (qrData != null) {
await withContext((context) =>
handleUri(context, null, qrData, null, null, l10n));
return;
}
await AndroidQrScanner.handleScannedData(qrData, ref, l10n);
} on CancellationException catch (_) {
// ignored - user cancelled
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_no_qr": "No QR code?",
"s_enter_manually": "Enter manually",
"s_read_from_image": "Provide image file",
"s_read_from_file": "Read from file",
"@_factory_reset": {},
"s_reset": "Reset",

View File

@ -30,10 +30,10 @@ import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../app/views/user_interaction.dart';
import '../../exception/apdu_exception.dart';
import '../../exception/cancellation_exception.dart';
import '../../core/state.dart';
import '../../desktop/models.dart';
import '../../exception/apdu_exception.dart';
import '../../exception/cancellation_exception.dart';
import '../../management/models.dart';
import '../../widgets/choice_filter_chip.dart';
import '../../widgets/file_drop_target.dart';
@ -56,6 +56,7 @@ class OathAddAccountPage extends ConsumerStatefulWidget {
final OathState? state;
final List<OathCredential>? credentials;
final CredentialData? credentialData;
const OathAddAccountPage(
this.devicePath,
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/views/add_account_dialog.dart';
import '../../android/qr_scanner/qr_scanner_provider.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/state.dart';
@ -28,11 +29,8 @@ import '../../app/views/action_list.dart';
import '../../core/state.dart';
import '../models.dart';
import '../keys.dart' as keys;
import '../state.dart';
import 'add_account_page.dart';
import 'manage_password_dialog.dart';
import 'reset_dialog.dart';
import 'utils.dart';
Widget oathBuildActions(
BuildContext context,
@ -59,38 +57,14 @@ Widget oathBuildActions(
icon: const Icon(Icons.person_add_alt_1_outlined),
onTap: used != null && (capacity == null || capacity > used)
? (context) async {
final credentials = ref.read(credentialsProvider);
final withContext = ref.read(withContextProvider);
Navigator.of(context).pop();
if (isAndroid) {
final qrScanner = ref.read(qrScannerProvider);
if (qrScanner != null) {
final qrData = await qrScanner.scanQr();
if (qrData != null) {
await withContext((context) => handleUri(
context,
credentials,
qrData,
devicePath,
oathState,
l10n,
));
return;
}
await AndroidQrScanner.handleScannedData(
qrData, ref, l10n);
}
await withContext((context) => showBlurDialog(
context: context,
routeSettings:
const RouteSettings(name: 'oath_add_account'),
builder: (context) {
return OathAddAccountPage(
devicePath,
oathState,
credentials: credentials,
credentialData: null,
);
},
));
} else {
await showBlurDialog(
context: context,