add widget

This commit is contained in:
Adam Velebil 2023-06-08 17:04:17 +02:00
parent 861eae846c
commit ec87865643
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
11 changed files with 392 additions and 35 deletions

View File

@ -16,6 +16,11 @@
package com.yubico.authenticator
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.annotation.SuppressLint
import android.content.*
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
@ -42,16 +47,22 @@ import com.yubico.authenticator.logging.FlutterLog
import com.yubico.authenticator.oath.AppLinkMethodChannel
import com.yubico.authenticator.oath.OathManager
import com.yubico.authenticator.oath.OathViewModel
import com.yubico.authenticator.yubikit.NfcActivityDispatcher
import com.yubico.authenticator.yubikit.NfcActivityListener
import com.yubico.authenticator.yubikit.NfcActivityState
import com.yubico.yubikit.android.YubiKitManager
import com.yubico.yubikit.android.transport.nfc.NfcConfiguration
import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyManager
import com.yubico.yubikit.android.transport.usb.UsbConfiguration
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyManager
import com.yubico.yubikit.core.YubiKeyDevice
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONObject
import org.slf4j.LoggerFactory
@ -74,6 +85,20 @@ class MainActivity : FlutterFragmentActivity() {
private val logger = LoggerFactory.getLogger(MainActivity::class.java)
private val nfcActivityListener = object : NfcActivityListener {
var appMethodChannel : AppMethodChannel? = null
override fun onChange(newState: NfcActivityState) {
appMethodChannel?.let {
logger.debug("setting nfc activity state to ${newState.name}")
it.nfcActivityStateChanged(newState)
} ?: {
logger.warn("cannot set nfc activity state to ${newState.name} - no method channel")
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -85,7 +110,10 @@ class MainActivity : FlutterFragmentActivity() {
allowScreenshots(false)
yubikit = YubiKitManager(this)
yubikit = YubiKitManager(
UsbYubiKeyManager(this),
NfcYubiKeyManager(this, NfcActivityDispatcher(nfcActivityListener))
)
}
/**
@ -263,6 +291,11 @@ class MainActivity : FlutterFragmentActivity() {
lifecycleScope.launch {
try {
it.processYubiKey(device)
if (device is NfcYubiKeyDevice) {
device.remove {
appMethodChannel.nfcActivityStateChanged(NfcActivityState.READY)
}
}
} catch (e: Throwable) {
logger.error("Error processing YubiKey in AppContextManager", e)
}
@ -291,6 +324,8 @@ class MainActivity : FlutterFragmentActivity() {
appMethodChannel = AppMethodChannel(messenger)
appLinkMethodChannel = AppLinkMethodChannel(messenger)
nfcActivityListener.appMethodChannel = appMethodChannel
flutterStreams = listOf(
viewModel.deviceInfo.streamTo(this, messenger, "android.devices.deviceInfo"),
oathViewModel.sessionState.streamTo(this, messenger, "android.oath.sessionState"),
@ -306,7 +341,8 @@ class MainActivity : FlutterFragmentActivity() {
viewModel,
oathViewModel,
dialogManager,
appPreferences
appPreferences,
nfcActivityListener
)
else -> null
}
@ -315,6 +351,7 @@ class MainActivity : FlutterFragmentActivity() {
}
override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
nfcActivityListener.appMethodChannel = null
flutterStreams.forEach { it.close() }
super.cleanUpFlutterEngine(flutterEngine)
}
@ -427,6 +464,15 @@ class MainActivity : FlutterFragmentActivity() {
JSONObject(mapOf("nfcEnabled" to value)).toString()
)
}
fun nfcActivityStateChanged(activityState: NfcActivityState) {
lifecycleScope.launch(Dispatchers.Main) {
methodChannel.invokeMethod(
"nfcActivityChanged",
JSONObject(mapOf("state" to activityState.value)).toString()
)
}
}
}
private fun allowScreenshots(value: Boolean): Boolean {

View File

@ -27,7 +27,6 @@ import com.yubico.authenticator.*
import com.yubico.authenticator.device.Capabilities
import com.yubico.authenticator.device.Info
import com.yubico.authenticator.device.UnknownDevice
import com.yubico.authenticator.logging.Log
import com.yubico.authenticator.oath.data.Code
import com.yubico.authenticator.oath.data.CodeType
import com.yubico.authenticator.oath.data.Credential
@ -43,6 +42,8 @@ import com.yubico.authenticator.oath.keystore.ClearingMemProvider
import com.yubico.authenticator.oath.keystore.KeyProvider
import com.yubico.authenticator.oath.keystore.KeyStoreProvider
import com.yubico.authenticator.oath.keystore.SharedPrefProvider
import com.yubico.authenticator.yubikit.NfcActivityListener
import com.yubico.authenticator.yubikit.NfcActivityState
import com.yubico.authenticator.yubikit.getDeviceInfo
import com.yubico.authenticator.yubikit.withConnection
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
@ -76,6 +77,7 @@ class OathManager(
private val oathViewModel: OathViewModel,
private val dialogManager: DialogManager,
private val appPreferences: AppPreferences,
private val nfcActivityListener: NfcActivityListener
) : AppContextManager {
companion object {
const val NFC_DATA_CLEANUP_DELAY = 30L * 1000 // 30s
@ -330,9 +332,14 @@ class OathManager(
logger.debug(
"Successfully read Oath session info (and credentials if unlocked) from connected key"
)
nfcActivityListener.onChange(NfcActivityState.PROCESSING_FINISHED)
} catch (e: Exception) {
// OATH not enabled/supported, try to get DeviceInfo over other USB interfaces
logger.error("Failed to connect to CCID", e)
nfcActivityListener.onChange(NfcActivityState.PROCESSING_INTERRUPTED)
if (device.transport == Transport.USB || e is ApplicationNotAvailableException) {
val deviceInfo = try {
getDeviceInfo(device)

View File

@ -0,0 +1,77 @@
/*
* Copyright (C) 2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.yubico.authenticator.yubikit
import android.app.Activity
import android.nfc.NfcAdapter
import android.nfc.Tag
import com.yubico.yubikit.android.transport.nfc.NfcConfiguration
import com.yubico.yubikit.android.transport.nfc.NfcDispatcher
import com.yubico.yubikit.android.transport.nfc.NfcReaderDispatcher
import org.slf4j.LoggerFactory
interface NfcActivityListener {
fun onChange(newState: NfcActivityState)
}
class NfcActivityDispatcher(private val listener: NfcActivityListener) : NfcDispatcher {
private lateinit var adapter: NfcAdapter
private lateinit var yubikitNfcDispatcher: NfcReaderDispatcher
private val logger = LoggerFactory.getLogger(NfcActivityDispatcher::class.java)
override fun enable(
activity: Activity,
nfcConfiguration: NfcConfiguration,
handler: NfcDispatcher.OnTagHandler
) {
adapter = NfcAdapter.getDefaultAdapter(activity)
yubikitNfcDispatcher = NfcReaderDispatcher(adapter)
logger.debug("enabling yubikit NFC activity dispatcher")
yubikitNfcDispatcher.enable(
activity,
nfcConfiguration,
TagInterceptor(listener, handler)
)
listener.onChange(NfcActivityState.READY)
}
override fun disable(activity: Activity) {
listener.onChange(NfcActivityState.NOT_ACTIVE)
yubikitNfcDispatcher.disable(activity)
logger.debug("disabling yubikit NFC activity dispatcher")
}
class TagInterceptor(
private val listener: NfcActivityListener,
private val tagHandler: NfcDispatcher.OnTagHandler
) : NfcDispatcher.OnTagHandler {
private val logger = LoggerFactory.getLogger(TagInterceptor::class.java)
override fun onTag(tag: Tag) {
listener.onChange(NfcActivityState.PROCESSING_STARTED)
logger.debug("forwarding tag")
tagHandler.onTag(tag)
}
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (C) 2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.yubico.authenticator.yubikit
enum class NfcActivityState(val value: Int) {
NOT_ACTIVE(0),
READY(1),
PROCESSING_STARTED(2),
PROCESSING_FINISHED(3),
PROCESSING_INTERRUPTED(4)
}

View File

@ -57,6 +57,12 @@ void setupAppMethodsChannel(WidgetRef ref) {
ref.read(androidNfcStateProvider.notifier).setNfcEnabled(nfcEnabled);
break;
}
case 'nfcActivityChanged':
{
var nfcActivityState = args['state'];
ref.read(androidNfcActivityProvider.notifier).setActivityState(nfcActivityState);
break;
}
default:
throw PlatformException(
code: 'NotImplemented',

View File

@ -73,6 +73,32 @@ class NfcStateNotifier extends StateNotifier<bool> {
}
}
enum NfcActivity {
notActive,
ready,
processingStarted,
processingFinished,
processingInterrupted,
}
class NfcActivityNotifier extends StateNotifier<NfcActivity> {
NfcActivityNotifier() : super(NfcActivity.notActive);
void setActivityState(int stateValue) {
var newState = switch (stateValue) {
0 => NfcActivity.notActive,
1 => NfcActivity.ready,
2 => NfcActivity.processingStarted,
3 => NfcActivity.processingFinished,
4 => NfcActivity.processingInterrupted,
_ => NfcActivity.notActive
};
state = newState;
}
}
final androidSdkVersionProvider = Provider<int>((ref) => -1);
final androidNfcSupportProvider = Provider<bool>((ref) => false);
@ -80,6 +106,10 @@ final androidNfcSupportProvider = Provider<bool>((ref) => false);
final androidNfcStateProvider =
StateNotifierProvider<NfcStateNotifier, bool>((ref) => NfcStateNotifier());
final androidNfcActivityProvider = StateNotifierProvider<NfcActivityNotifier, NfcActivity>((ref) =>
NfcActivityNotifier()
);
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.
@ -21,9 +21,10 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'views/nfc/nfc_activity_widget.dart';
import '../app/state.dart';
import '../app/views/user_interaction.dart';
import '../widgets/custom_icons.dart';
const _channel = MethodChannel('com.yubico.authenticator.channel.dialog');
@ -35,6 +36,7 @@ final androidDialogProvider = Provider<_DialogProvider>(
class _DialogProvider {
final WithContext _withContext;
final Widget _icon = const NfcActivityWidget(width: 64, height: 64);
UserInteractionController? _controller;
_DialogProvider(this._withContext) {
@ -65,46 +67,29 @@ class _DialogProvider {
_controller = null;
}
Widget? _getIcon(String? icon) => switch (icon) {
'nfc' => nfcIcon,
'success' => const Icon(Icons.check_circle),
'error' => const Icon(Icons.error),
_ => null,
};
Future<void> _updateDialogState(
String? title, String? description, String? iconName) async {
final icon = _getIcon(iconName);
await _withContext((context) async {
_controller?.updateContent(
title: title,
description: description,
icon: icon != null
? IconTheme(
data: IconTheme.of(context).copyWith(size: 64),
child: icon,
)
: null,
icon: _icon,
);
});
}
Future<void> _showDialog(
String title, String description, String? iconName) async {
final icon = _getIcon(iconName);
_controller = await _withContext((context) async => promptUserInteraction(
context,
title: title,
description: description,
icon: icon != null
? IconTheme(
data: IconTheme.of(context).copyWith(size: 64),
child: icon,
)
: null,
onCancel: () {
_channel.invokeMethod('cancel');
},
));
_controller = await _withContext((context) async {
return promptUserInteraction(
context,
title: title,
description: description,
icon: _icon,
onCancel: () {
_channel.invokeMethod('cancel');
},
);
});
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (C) 2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'package:flutter/material.dart';
import 'package:yubico_authenticator/android/state.dart';
import 'package:yubico_authenticator/android/views/nfc/nfc_activity_widget.dart';
class MainPageNfcActivityWidget extends StatelessWidget {
final Widget widget;
const MainPageNfcActivityWidget(this.widget, {super.key});
@override
Widget build(BuildContext context) {
return NfcActivityWidget(
width: 128.0,
height: 128.0,
iconView: (nfcActivityState) {
return Opacity(
opacity: switch (nfcActivityState) {
NfcActivity.processingStarted => 1.0,
_ => 0.8
},
child: widget,
);
},
);
}
}

View File

@ -0,0 +1,90 @@
/*
* Copyright (C) 2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:yubico_authenticator/android/state.dart';
/// Default icon for [NfcActivityWidget]
class NfcActivityIcon extends StatelessWidget {
final NfcActivity nfcActivity;
const NfcActivityIcon(this.nfcActivity, {super.key});
@override
Widget build(BuildContext context) => switch (nfcActivity) {
NfcActivity.processingStarted => const _NfcIconWithOpacity(1.0),
_ => const _NfcIconWithOpacity(0.8)
};
}
class _NfcIconWithOpacity extends StatelessWidget {
final double opacity;
const _NfcIconWithOpacity(this.opacity);
@override
Widget build(BuildContext context) => Opacity(
opacity: opacity,
child: const _NfcIcon(),
);
}
class _NfcIcon extends StatelessWidget {
const _NfcIcon();
@override
Widget build(BuildContext context) {
final theme = IconTheme.of(context);
return LayoutBuilder(
builder: (BuildContext buildContext, BoxConstraints constraints) =>
CustomPaint(
size: Size.copy(constraints.biggest),
painter: _NfcIconPainter(theme.color ?? Colors.black),
),
);
}
}
class _NfcIconPainter extends CustomPainter {
final Color color;
_NfcIconPainter(this.color);
@override
void paint(Canvas canvas, Size size) {
final step = size.width / 4;
const sweep = pi / 4;
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..strokeWidth = step / 2;
final rect =
Offset(size.width * -1.7, 0) & Size(size.width * 2, size.height);
for (var i = 0; i < 3; i++) {
canvas.drawArc(rect.inflate(i * step), -sweep / 2, sweep, false, paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (C) 2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import '../../../app/logging.dart';
import '../../state.dart';
import 'nfc_activity_icon.dart';
final _logger = Logger('NfcActivityWidget');
class NfcActivityWidget extends ConsumerWidget {
final double width;
final double height;
final Widget Function(NfcActivity)? iconView;
const NfcActivityWidget(
{super.key, this.width = 32.0, this.height = 32.0, this.iconView});
@override
Widget build(BuildContext context, WidgetRef ref) {
final NfcActivity nfcActivityState = ref.watch(androidNfcActivityProvider);
_logger.debug('State for NfcActivityWidget changed to $nfcActivityState');
return IgnorePointer(
child: SizedBox(
width: width,
height: height,
child: iconView?.call(nfcActivityState) ??
NfcActivityIcon(nfcActivityState)),
);
}
}

View File

@ -20,6 +20,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../android/app_methods.dart';
import '../../android/state.dart';
import '../../android/views/nfc/main_page_nfc_activity_widget.dart';
import '../../exception/cancellation_exception.dart';
import '../../core/state.dart';
import '../../fido/views/fido_screen.dart';
@ -82,7 +83,7 @@ class MainPage extends ConsumerWidget {
var hasNfcSupport = ref.watch(androidNfcSupportProvider);
var isNfcEnabled = ref.watch(androidNfcStateProvider);
return MessagePage(
graphic: noKeyImage,
graphic: MainPageNfcActivityWidget(noKeyImage),
message: hasNfcSupport && isNfcEnabled
? l10n.l_insert_or_tap_yk
: l10n.l_insert_yk,