diff --git a/NEWS b/NEWS index 83c21e43..34c1c5c4 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,9 @@ +* Version 7.0.1 (released 2024-05-30) Android only release + ** Fix: Opening the app by NFC tap needs another tap to reveal accounts. + ** Fix: NFC devices attached to mobile phone prevent usage of USB YubiKeys. + ** Fix: Invalid colors shown in customization views for Android Dynamic color. + ** Fix: Fingerprints are shown in random order. + * Version 7.0.0 (released 2024-05-06) ** UI: Add home screen with device information, customization options, and factory reset. ** UI: Add search filtering to Passkeys and display more information. diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt index 36a7eed1..1d26c67c 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt @@ -108,9 +108,13 @@ class MainActivity : FlutterFragmentActivity() { logger.debug("Starting nfc discovery") yubikit.startNfcDiscovery( nfcConfiguration.disableNfcDiscoverySound(appPreferences.silenceNfcSounds), - this, - ::processYubiKey - ) + this + ) { nfcYubiKeyDevice -> + if (!deviceManager.isUsbKeyConnected()) { + launchProcessYubiKey(nfcYubiKeyDevice) + } + } + hasNfc = true } catch (e: NfcNotAvailable) { hasNfc = false @@ -131,7 +135,7 @@ class MainActivity : FlutterFragmentActivity() { logger.debug("YubiKey was disconnected, stopping usb discovery") stopUsbDiscovery() } - processYubiKey(device) + launchProcessYubiKey(device) } } @@ -214,7 +218,7 @@ class MainActivity : FlutterFragmentActivity() { val device = NfcYubiKeyDevice(tag, nfcConfiguration.timeout, executor) lifecycleScope.launch { try { - contextManager?.processYubiKey(device) + processYubiKey(device) device.remove { executor.shutdown() startNfcDiscovery() @@ -269,38 +273,42 @@ class MainActivity : FlutterFragmentActivity() { } } - private fun processYubiKey(device: YubiKeyDevice) { + private suspend fun processYubiKey(device: YubiKeyDevice) { + val deviceInfo = getDeviceInfo(device) + deviceManager.setDeviceInfo(deviceInfo) + + if (deviceInfo == null) { + return + } + + val supportedContexts = DeviceManager.getSupportedContexts(deviceInfo) + logger.debug("Connected key supports: {}", supportedContexts) + if (!supportedContexts.contains(viewModel.appContext.value)) { + val preferredContext = DeviceManager.getPreferredContext(supportedContexts) + logger.debug( + "Current context ({}) is not supported by the key. Using preferred context {}", + viewModel.appContext.value, + preferredContext + ) + switchContext(preferredContext) + } + + if (contextManager == null) { + switchContext(DeviceManager.getPreferredContext(supportedContexts)) + } + + contextManager?.let { + try { + it.processYubiKey(device) + } catch (e: Throwable) { + logger.error("Error processing YubiKey in AppContextManager", e) + } + } + } + + private fun launchProcessYubiKey(device: YubiKeyDevice) { lifecycleScope.launch { - val deviceInfo = getDeviceInfo(device) - deviceManager.setDeviceInfo(deviceInfo) - - if (deviceInfo == null) { - return@launch - } - - val supportedContexts = DeviceManager.getSupportedContexts(deviceInfo) - logger.debug("Connected key supports: {}", supportedContexts) - if (!supportedContexts.contains(viewModel.appContext.value)) { - val preferredContext = DeviceManager.getPreferredContext(supportedContexts) - logger.debug( - "Current context ({}) is not supported by the key. Using preferred context {}", - viewModel.appContext.value, - preferredContext - ) - switchContext(preferredContext) - } - - if (contextManager == null) { - switchContext(DeviceManager.getPreferredContext(supportedContexts)) - } - - contextManager?.let { - try { - it.processYubiKey(device) - } catch (e: Throwable) { - logger.error("Error processing YubiKey in AppContextManager", e) - } - } + processYubiKey(device) } } @@ -342,7 +350,7 @@ class MainActivity : FlutterFragmentActivity() { viewModel.appContext.observe(this) { switchContext(it) - viewModel.connectedYubiKey.value?.let(::processYubiKey) + viewModel.connectedYubiKey.value?.let(::launchProcessYubiKey) } } diff --git a/helper/version_info.txt b/helper/version_info.txt index 0538c5df..c4c014f7 100755 --- a/helper/version_info.txt +++ b/helper/version_info.txt @@ -6,8 +6,8 @@ VSVersionInfo( ffi=FixedFileInfo( # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) # Set not needed items to zero 0. - filevers=(7, 0, 1, 0), - prodvers=(7, 0, 1, 0), + filevers=(7, 0, 2, 0), + prodvers=(7, 0, 2, 0), # Contains a bitmask that specifies the valid bits 'flags'r mask=0x3f, # Contains a bitmask that specifies the Boolean attributes of the file. @@ -31,11 +31,11 @@ VSVersionInfo( '040904b0', [StringStruct('CompanyName', 'Yubico'), StringStruct('FileDescription', 'Yubico Authenticator Helper'), - StringStruct('FileVersion', '7.0.1-dev.0'), + StringStruct('FileVersion', '7.0.2-dev.0'), StringStruct('LegalCopyright', 'Copyright (c) Yubico'), StringStruct('OriginalFilename', 'authenticator-helper.exe'), StringStruct('ProductName', 'Yubico Authenticator'), - StringStruct('ProductVersion', '7.0.1-dev.0')]) + StringStruct('ProductVersion', '7.0.2-dev.0')]) ]), VarFileInfo([VarStruct('Translation', [1033, 1200])]) ] diff --git a/lib/android/fido/state.dart b/lib/android/fido/state.dart index 8cd1e41b..3c6182b0 100644 --- a/lib/android/fido/state.dart +++ b/lib/android/fido/state.dart @@ -17,6 +17,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; @@ -177,8 +178,10 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier { if (json == null) { state = const AsyncValue.loading(); } else { - List newState = List.from( - (json as List).map((e) => Fingerprint.fromJson(e)).toList()); + List newState = List.from((json as List) + .map((e) => Fingerprint.fromJson(e)) + .sortedBy((f) => f.label.toLowerCase()) + .toList()); state = AsyncValue.data(newState); } }, onError: (err, stackTrace) { diff --git a/lib/android/oath/state.dart b/lib/android/oath/state.dart index 5e868da4..03b0bdcf 100755 --- a/lib/android/oath/state.dart +++ b/lib/android/oath/state.dart @@ -207,10 +207,8 @@ final addCredentialsToAnyProvider = Provider( final androidCredentialListProvider = StateNotifierProvider.autoDispose .family?, DevicePath>( (ref, devicePath) { - var notifier = _AndroidCredentialListNotifier( - ref.watch(withContextProvider), - ref.watch(currentDeviceProvider)?.transport == Transport.usb, - ); + var notifier = + _AndroidCredentialListNotifier(ref.watch(withContextProvider), ref); return notifier; }, ); @@ -218,22 +216,15 @@ final androidCredentialListProvider = StateNotifierProvider.autoDispose class _AndroidCredentialListNotifier extends OathCredentialListNotifier { final _events = const EventChannel('android.oath.credentials'); final WithContext _withContext; - final bool _isUsbAttached; + final Ref _ref; late StreamSubscription _sub; - _AndroidCredentialListNotifier(this._withContext, this._isUsbAttached) - : super() { + _AndroidCredentialListNotifier(this._withContext, this._ref) : super() { _sub = _events.receiveBroadcastStream().listen((event) { final json = jsonDecode(event); List? newState = json != null ? List.from((json as List).map((e) => OathPair.fromJson(e)).toList()) : null; - if (state != null && newState == null) { - // If we go from non-null to null this means we should stop listening to - // avoid receiving a message for a different notifier as there is only - // one channel. - _sub.cancel(); - } state = newState; }); } @@ -249,7 +240,7 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier { // Prompt for touch if needed UserInteractionController? controller; Timer? touchTimer; - if (_isUsbAttached) { + if (_ref.read(currentDeviceProvider)?.transport == Transport.usb) { void triggerTouchPrompt() async { controller = await _withContext( (context) async { diff --git a/lib/home/views/home_screen.dart b/lib/home/views/home_screen.dart index c92ee40f..a2526eac 100644 --- a/lib/home/views/home_screen.dart +++ b/lib/home/views/home_screen.dart @@ -54,7 +54,7 @@ class _HomeScreenState extends ConsumerState { final enabledCapabilities = widget.deviceData.info.config .enabledCapabilities[widget.deviceData.node.transport] ?? 0; - final primaryColor = ref.watch(defaultColorProvider); + final primaryColor = ref.watch(primaryColorProvider); // We need this to avoid unwanted app switch animation if (hide) { @@ -219,19 +219,14 @@ class _DeviceColor extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context)!; - final theme = Theme.of(context); - final primaryColor = ref.watch(defaultColorProvider); - final defaultColor = - (isAndroid && ref.read(androidSdkVersionProvider) >= 31) - ? theme.colorScheme.onSurface - : primaryColor; + final defaultColor = ref.watch(defaultColorProvider); final customColor = initialCustomization.color; return ChoiceFilterChip( disableHover: true, value: customColor, items: const [null], - selected: customColor != null && customColor != defaultColor, + selected: customColor != null && customColor.value != defaultColor.value, itemBuilder: (e) => Wrap( alignment: WrapAlignment.center, runSpacing: 8, @@ -250,32 +245,31 @@ class _DeviceColor extends ConsumerWidget { Colors.lightGreen ].map((e) => _ColorButton( color: e, - isSelected: customColor == e, + isSelected: customColor?.value == e.value, onPressed: () { _updateColor(e, ref); Navigator.of(context).pop(); }, )), - // remove color button + // "use default color" button RawMaterialButton( onPressed: () { _updateColor(null, ref); Navigator.of(context).pop(); }, constraints: const BoxConstraints(minWidth: 26.0, minHeight: 26.0), - fillColor: (isAndroid && ref.read(androidSdkVersionProvider) >= 31) - ? theme.colorScheme.onSurface - : primaryColor, + fillColor: defaultColor, hoverColor: Colors.black12, shape: const CircleBorder(), - child: Icon( - Symbols.cancel, - size: 16, - color: customColor == null - ? theme.colorScheme.onSurface - : theme.colorScheme.surface.withOpacity(0.2), - ), + child: Icon(customColor == null ? Symbols.circle : Symbols.clear, + fill: 1, + size: 16, + weight: 700, + opticalSize: 20, + color: defaultColor.computeLuminance() > 0.7 + ? Colors.grey // for bright colors + : Colors.white), ), ], ), diff --git a/lib/version.dart b/lib/version.dart index 7c2e39c1..936a7305 100755 --- a/lib/version.dart +++ b/lib/version.dart @@ -1,5 +1,5 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // This file is generated by running ./set-version.py -const String version = '7.0.1-dev.0'; -const int build = 70001; +const String version = '7.0.2-dev.0'; +const int build = 70002; diff --git a/pubspec.yaml b/pubspec.yaml index 45f2ecc6..4955ce8b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # This field is updated by running ./set-version.py # DO NOT MANUALLY EDIT THIS! -version: 7.0.1-dev.0+70001 +version: 7.0.2-dev.0+70002 environment: sdk: '>=3.0.0 <4.0.0' @@ -70,7 +70,7 @@ dependencies: io: ^1.0.4 base32: ^2.1.3 convert: ^3.1.1 - material_symbols_icons: ^4.2741.0 + material_symbols_icons: ^4.2719.3 dev_dependencies: integration_test: diff --git a/resources/win/release-win.ps1 b/resources/win/release-win.ps1 index 4193b722..f9bf9bc9 100644 --- a/resources/win/release-win.ps1 +++ b/resources/win/release-win.ps1 @@ -1,4 +1,4 @@ -$version="7.0.1-dev.0" +$version="7.0.2-dev.0" echo "Clean-up of old files" rm *.msi diff --git a/resources/win/yubioath-desktop.wxs b/resources/win/yubioath-desktop.wxs index 79e084fe..fc7895cc 100644 --- a/resources/win/yubioath-desktop.wxs +++ b/resources/win/yubioath-desktop.wxs @@ -1,7 +1,7 @@ - +