This commit is contained in:
Adam Velebil 2023-02-10 08:43:15 +01:00
commit e71b619c67
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
6 changed files with 134 additions and 8 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.
@ -23,9 +23,12 @@ import android.hardware.camera2.CameraManager
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.nfc.NfcAdapter
import android.nfc.NfcAdapter.STATE_ON
import android.nfc.NfcAdapter.STATE_TURNING_OFF
import android.nfc.Tag
import android.os.Build
import android.os.Bundle
import android.provider.Settings.ACTION_NFC_SETTINGS
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.core.view.WindowCompat
@ -47,6 +50,7 @@ import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.launch
import org.json.JSONObject
import java.io.Closeable
import java.util.concurrent.Executors
@ -62,6 +66,7 @@ class MainActivity : FlutterFragmentActivity() {
// receives broadcasts when QR Scanner camera is closed
private val qrScannerCameraClosedBR = QRScannerCameraClosedBR()
private val nfcAdapterStateChangeBR = NfcAdapterStateChangedBR()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -155,11 +160,13 @@ class MainActivity : FlutterFragmentActivity() {
override fun onStart() {
super.onStart()
registerReceiver(qrScannerCameraClosedBR, QRScannerCameraClosedBR.intentFilter)
registerReceiver(nfcAdapterStateChangeBR, NfcAdapterStateChangedBR.intentFilter)
}
override fun onStop() {
super.onStop()
unregisterReceiver(qrScannerCameraClosedBR)
unregisterReceiver(nfcAdapterStateChangeBR)
}
override fun onPause() {
@ -322,6 +329,23 @@ class MainActivity : FlutterFragmentActivity() {
}
}
class NfcAdapterStateChangedBR : BroadcastReceiver() {
companion object {
val intentFilter = IntentFilter("android.nfc.action.ADAPTER_STATE_CHANGED")
}
override fun onReceive(context: Context?, intent: Intent?) {
intent?.let {
val state = it.getIntExtra("android.nfc.extra.ADAPTER_STATE", 0)
Log.d(TAG, "NfcAdapter state changed to $state")
if (state == STATE_ON || state == STATE_TURNING_OFF) {
(context as? MainActivity)?.appMethodChannel?.nfcAdapterStateChanged(state == STATE_ON)
}
}
}
}
inner class AppMethodChannel(messenger: BinaryMessenger) {
private val methodChannel = MethodChannel(messenger, "app.methods")
@ -356,10 +380,31 @@ class MainActivity : FlutterFragmentActivity() {
cameraService.cameraIdList.isNotEmpty()
)
}
"hasNfc" -> result.success(
packageManager.hasSystemFeature(PackageManager.FEATURE_NFC)
)
"isNfcEnabled" -> {
val nfcAdapter = NfcAdapter.getDefaultAdapter(this@MainActivity)
result.success(
nfcAdapter != null && nfcAdapter.isEnabled
)
}
"openNfcSettings" -> {
startActivity(Intent(ACTION_NFC_SETTINGS))
result.success(true)
}
else -> Log.w(TAG, "Unknown app method: ${methodCall.method}")
}
}
}
fun nfcAdapterStateChanged(value: Boolean) {
methodChannel.invokeMethod(
"nfcAdapterStateChanged",
JSONObject(mapOf("nfcEnabled" to value)).toString()
)
}
}
private fun allowScreenshots(value: Boolean): Boolean {

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.
@ -14,7 +14,11 @@
* limitations under the License.
*/
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:yubico_authenticator/android/state.dart';
const appMethodsChannel = MethodChannel('app.methods');
@ -22,6 +26,18 @@ Future<bool> getHasCamera() async {
return await appMethodsChannel.invokeMethod('hasCamera');
}
Future<bool> getHasNfc() async {
return await appMethodsChannel.invokeMethod('hasNfc');
}
Future<bool> isNfcEnabled() async {
return await appMethodsChannel.invokeMethod('isNfcEnabled');
}
Future<void> openNfcSettings() async {
await appMethodsChannel.invokeMethod('openNfcSettings');
}
Future<int> getAndroidSdkVersion() async {
return await appMethodsChannel.invokeMethod('getAndroidSdkVersion');
}
@ -30,3 +46,22 @@ Future<void> setPrimaryClip(String toClipboard, bool isSensitive) async {
await appMethodsChannel.invokeMethod('setPrimaryClip',
{'toClipboard': toClipboard, 'isSensitive': isSensitive});
}
void setupAppMethodsChannel(WidgetRef ref) {
appMethodsChannel.setMethodCallHandler((call) async {
final args = jsonDecode(call.arguments);
switch (call.method) {
case 'nfcAdapterStateChanged':
{
var nfcEnabled = args['nfcEnabled'];
ref.read(androidNfcStateProvider.notifier).setNfcEnabled(nfcEnabled);
break;
}
default:
throw PlatformException(
code: 'NotImplemented',
message: 'Method ${call.method} is not implemented',
);
}
});
}

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.
@ -82,6 +82,7 @@ Future<Widget> initialize() async {
(ref) => ref.watch(androidClipboardProvider),
),
androidSdkVersionProvider.overrideWithValue(await getAndroidSdkVersion()),
androidNfcSupportProvider.overrideWithValue(await getHasNfc()),
supportedThemesProvider
.overrideWith(
(ref) => ref.watch(androidSupportedThemesProvider),
@ -99,6 +100,8 @@ Future<Widget> initialize() async {
// set context which will handle otpauth links
setupOtpAuthLinkHandler(context);
setupAppMethodsChannel(ref);
return const MainPage();
},
)),

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.
@ -62,8 +62,22 @@ class _AndroidClipboard extends AppClipboard {
}
}
class NfcStateNotifier extends StateNotifier<bool> {
NfcStateNotifier(): super(false);
void setNfcEnabled(bool value) {
state = value;
}
}
final androidSdkVersionProvider = Provider<int>((ref) => -1);
final androidNfcSupportProvider = Provider<bool>((ref) => false);
final androidNfcStateProvider = StateNotifierProvider<NfcStateNotifier, bool>((ref) =>
NfcStateNotifier()
);
final androidSupportedThemesProvider = StateProvider<List<ThemeMode>>((ref) {
if (ref.read(androidSdkVersionProvider) < 29) {
// the user can select from light or dark theme of the app

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.
@ -17,15 +17,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:yubico_authenticator/android/state.dart';
import 'package:yubico_authenticator/app/logging.dart';
import '../app/models.dart';
import 'app_methods.dart';
final _log = Logger('android.window_state_provider');
final _windowStateProvider =
StateNotifierProvider<_WindowStateNotifier, WindowState>(
(ref) => _WindowStateNotifier());
(ref) => _WindowStateNotifier(ref));
final androidWindowStateProvider = Provider<WindowState>(
(ref) => ref.watch(_windowStateProvider),
@ -33,7 +35,8 @@ final androidWindowStateProvider = Provider<WindowState>(
class _WindowStateNotifier extends StateNotifier<WindowState>
with WidgetsBindingObserver {
_WindowStateNotifier()
final StateNotifierProviderRef<_WindowStateNotifier, WindowState> _ref;
_WindowStateNotifier(this._ref)
: super(WindowState(focused: true, visible: true, active: true)) {
_init();
}
@ -52,6 +55,11 @@ class _WindowStateNotifier extends StateNotifier<WindowState>
visible: requestedState,
active: requestedState);
_log.debug('Updated windowState to $state');
if (lifeCycleState == AppLifecycleState.resumed) {
_log.debug('Reading nfc enabled value');
isNfcEnabled().then((value) =>
_ref.read(androidNfcStateProvider.notifier).setNfcEnabled(value));
}
} else {
_log.debug('Ignoring appLifecycleStateChange');
}

View File

@ -16,6 +16,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:yubico_authenticator/android/app_methods.dart';
import 'package:yubico_authenticator/android/state.dart';
import 'package:yubico_authenticator/widgets/custom_icons.dart';
import '../../exception/cancellation_exception.dart';
import '../../core/state.dart';
@ -41,6 +44,10 @@ class MainPage extends ConsumerWidget {
},
);
if (isAndroid) {
isNfcEnabled().then((value) => ref.read(androidNfcStateProvider.notifier).setNfcEnabled(value));
}
// If the current device changes, we need to pop any open dialogs.
ref.listen<AsyncValue<YubiKeyData>>(currentDeviceDataProvider, (_, __) {
Navigator.of(context).popUntil((route) {
@ -67,9 +74,23 @@ class MainPage extends ConsumerWidget {
);
if (deviceNode == null) {
if (isAndroid) {
var hasNfcSupport = ref.watch(androidNfcSupportProvider);
var isNfcEnabled = ref.watch(androidNfcStateProvider);
return MessagePage(
graphic: noKeyImage,
message: 'Insert or tap your YubiKey',
message: hasNfcSupport && isNfcEnabled
? 'Tap or insert your YubiKey'
: 'Insert your YubiKey',
actions: [
if (hasNfcSupport && !isNfcEnabled)
ElevatedButton.icon(
label: const Text('Enable NFC'),
icon: nfcIcon,
onPressed: () async {
await openNfcSettings();
})
],
actionButtonBuilder: (context) => IconButton(
icon: const Icon(Icons.person_add_alt_1),
tooltip: 'Add account',