yubioath-flutter/lib/app/state.dart

400 lines
11 KiB
Dart
Raw Normal View History

2021-11-19 17:05:57 +03:00
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
2021-11-19 17:05:57 +03:00
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
2021-11-23 16:51:36 +03:00
import 'package:shared_preferences/shared_preferences.dart';
import 'package:window_manager/window_manager.dart';
2022-01-12 14:49:04 +03:00
import 'package:yubico_authenticator/management/models.dart';
2021-11-19 17:05:57 +03:00
import '../core/models.dart';
import '../core/state.dart';
import '../core/rpc.dart';
import '../oath/menu_actions.dart';
2021-11-19 17:05:57 +03:00
import 'models.dart';
2022-01-18 17:46:42 +03:00
const _usbPollDelay = Duration(milliseconds: 500);
const _nfcPollDelay = Duration(milliseconds: 2500);
const _nfcAttachPollDelay = Duration(seconds: 1);
const _nfcDetachPollDelay = Duration(seconds: 5);
final log = Logger('app.state');
final windowStateProvider =
StateNotifierProvider<WindowStateNotifier, WindowState>(
(ref) => WindowStateNotifier());
class WindowStateNotifier extends StateNotifier<WindowState>
with WindowListener {
Timer? _idleTimer;
WindowStateNotifier()
: super(WindowState(focused: true, visible: true, active: true)) {
_init();
}
void _init() async {
windowManager.addListener(this);
if (!await windowManager.isVisible() && mounted) {
state = WindowState(focused: false, visible: false, active: true);
_idleTimer = Timer(const Duration(seconds: 5), () {
if (mounted) {
state = state.copyWith(active: false);
}
});
}
}
@override
void dispose() {
windowManager.removeListener(this);
super.dispose();
}
@override
set state(WindowState value) {
log.config('Window state changed: $value');
super.state = value;
}
@override
void onWindowEvent(String eventName) {
if (mounted) {
switch (eventName) {
case 'blur':
state = state.copyWith(focused: false);
_idleTimer?.cancel();
_idleTimer = Timer(const Duration(seconds: 5), () {
if (mounted) {
state = state.copyWith(active: false);
}
});
break;
case 'focus':
state = state.copyWith(focused: true, active: true);
_idleTimer?.cancel();
break;
case 'minimize':
state = state.copyWith(visible: false, active: false);
_idleTimer?.cancel();
break;
case 'restore':
state = state.copyWith(visible: true, active: true);
break;
default:
log.fine('Window event ignored: $eventName');
}
}
}
}
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
(ref) => ThemeModeNotifier(ref.watch(prefProvider)));
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
static const String _key = 'APP_STATE_THEME';
final SharedPreferences _prefs;
ThemeModeNotifier(this._prefs) : super(_fromName(_prefs.getString(_key)));
void setThemeMode(ThemeMode mode) {
state = mode;
_prefs.setString(_key, mode.name);
}
static ThemeMode _fromName(String? name) {
switch (name) {
case 'light':
return ThemeMode.light;
case 'dark':
return ThemeMode.dark;
default:
return ThemeMode.system;
}
}
}
final searchProvider =
StateNotifierProvider<SearchNotifier, String>((ref) => SearchNotifier());
class SearchNotifier extends StateNotifier<String> {
SearchNotifier() : super('');
setFilter(String value) {
state = value;
}
}
2022-01-12 14:49:04 +03:00
final _usbDevicesProvider =
StateNotifierProvider<UsbDeviceNotifier, List<UsbYubiKeyNode>>((ref) {
final notifier = UsbDeviceNotifier(ref.watch(rpcProvider));
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
notifier._notifyWindowState(windowState);
}, fireImmediately: true);
return notifier;
});
2021-11-19 17:05:57 +03:00
2022-01-12 14:49:04 +03:00
class UsbDeviceNotifier extends StateNotifier<List<UsbYubiKeyNode>> {
2021-11-19 17:05:57 +03:00
final RpcSession _rpc;
Timer? _pollTimer;
2021-11-19 17:05:57 +03:00
int _usbState = -1;
2022-01-12 14:49:04 +03:00
UsbDeviceNotifier(this._rpc) : super([]);
void _notifyWindowState(WindowState windowState) {
if (windowState.active) {
2022-01-12 14:49:04 +03:00
_pollDevices();
} else {
_pollTimer?.cancel();
// Release any held device
_rpc.command('get', ['usb']);
}
2021-11-19 17:05:57 +03:00
}
@override
void dispose() {
_pollTimer?.cancel();
2021-11-19 17:05:57 +03:00
super.dispose();
}
2022-01-12 14:49:04 +03:00
void _pollDevices() async {
_pollTimer?.cancel();
2022-01-12 14:49:04 +03:00
try {
var scan = await _rpc.command('scan', ['usb']);
if (_usbState != scan['state'] || state.length != scan['pids'].length) {
var usbResult = await _rpc.command('get', ['usb']);
log.info('USB state change', jsonEncode(usbResult));
2022-01-12 14:49:04 +03:00
_usbState = usbResult['data']['state'];
List<UsbYubiKeyNode> usbDevices = [];
for (String id in (usbResult['children'] as Map).keys) {
var path = ['usb', id];
var deviceResult = await _rpc.command('get', path);
2022-01-12 14:49:04 +03:00
var deviceData = deviceResult['data'];
usbDevices.add(DeviceNode.usbYubiKey(
path,
deviceData['name'],
deviceData['pid'],
DeviceInfo.fromJson(deviceData['info']),
) as UsbYubiKeyNode);
}
2022-01-12 14:49:04 +03:00
log.info('USB state updated');
if (mounted) {
2022-01-12 14:49:04 +03:00
state = usbDevices;
}
2021-11-19 17:05:57 +03:00
}
} on RpcError catch (e) {
log.severe('Error polling USB', jsonEncode(e));
2021-11-19 17:05:57 +03:00
}
2022-01-12 14:49:04 +03:00
if (mounted) {
2022-01-18 17:46:42 +03:00
_pollTimer = Timer(_usbPollDelay, _pollDevices);
2022-01-12 14:49:04 +03:00
}
}
}
final _nfcDevicesProvider =
StateNotifierProvider<NfcDeviceNotifier, List<NfcReaderNode>>((ref) {
final notifier = NfcDeviceNotifier(ref.watch(rpcProvider));
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
notifier._notifyWindowState(windowState);
}, fireImmediately: true);
return notifier;
});
class NfcDeviceNotifier extends StateNotifier<List<NfcReaderNode>> {
final RpcSession _rpc;
Timer? _pollTimer;
String _nfcState = '';
NfcDeviceNotifier(this._rpc) : super([]);
void _notifyWindowState(WindowState windowState) {
if (windowState.active) {
_pollReaders();
} else {
_pollTimer?.cancel();
// Release any held device
_rpc.command('get', ['nfc']);
}
}
@override
void dispose() {
_pollTimer?.cancel();
super.dispose();
}
void _pollReaders() async {
_pollTimer?.cancel();
try {
var children = await _rpc.command('scan', ['nfc']);
var newState = children.keys.join(':');
if (mounted && newState != _nfcState) {
log.info('NFC state change', jsonEncode(children));
_nfcState = newState;
state = children.entries
.map((e) =>
DeviceNode.nfcReader(['nfc', e.key], e.value['name'] as String)
as NfcReaderNode)
.toList();
}
} on RpcError catch (e) {
log.severe('Error polling NFC', jsonEncode(e));
}
2021-11-19 17:05:57 +03:00
if (mounted) {
2022-01-18 17:46:42 +03:00
_pollTimer = Timer(_nfcPollDelay, _pollReaders);
2021-11-19 17:05:57 +03:00
}
}
}
2022-01-12 14:49:04 +03:00
final attachedDevicesProvider = Provider<List<DeviceNode>>((ref) {
final usbDevices = ref.watch(_usbDevicesProvider).toList();
final nfcDevices = ref.watch(_nfcDevicesProvider).toList();
usbDevices.sort((a, b) => a.name.compareTo(b.name));
nfcDevices.sort((a, b) => a.name.compareTo(b.name));
return [...usbDevices, ...nfcDevices];
});
2021-11-19 17:05:57 +03:00
final currentDeviceProvider =
StateNotifierProvider<CurrentDeviceNotifier, DeviceNode?>((ref) {
2021-11-23 16:51:36 +03:00
final provider = CurrentDeviceNotifier(ref.watch(prefProvider));
2021-11-19 17:05:57 +03:00
ref.listen(attachedDevicesProvider, provider._updateAttachedDevices);
return provider;
});
class CurrentDeviceNotifier extends StateNotifier<DeviceNode?> {
2022-01-12 14:49:04 +03:00
static const String _lastDevice = 'APP_STATE_LAST_DEVICE';
2021-11-23 16:51:36 +03:00
final SharedPreferences _prefs;
CurrentDeviceNotifier(this._prefs) : super(null);
2021-11-19 17:05:57 +03:00
_updateAttachedDevices(List<DeviceNode>? previous, List<DeviceNode> devices) {
2022-01-12 14:49:04 +03:00
if (!devices.contains(state)) {
final lastDevice = _prefs.getString(_lastDevice) ?? '';
try {
state = devices.firstWhere(
(dev) => dev.when(
usbYubiKey: (path, name, pid, info) =>
lastDevice == 'serial:${info.serial}',
nfcReader: (path, name) => lastDevice == 'name:$name',
),
orElse: () => devices.whereType<UsbYubiKeyNode>().first);
} on StateError {
state = null;
}
2021-11-19 17:05:57 +03:00
}
}
setCurrentDevice(DeviceNode device) {
state = device;
2022-01-12 14:49:04 +03:00
device.when(
usbYubiKey: (path, name, pid, info) {
final serial = info.serial;
if (serial != null) {
_prefs.setString(_lastDevice, 'serial:$serial');
}
},
nfcReader: (path, name) {
_prefs.setString(_lastDevice, 'name:$name');
},
);
2021-11-19 17:05:57 +03:00
}
}
2022-01-12 14:49:04 +03:00
final currentDeviceDataProvider =
StateNotifierProvider<CurrentDeviceDataNotifier, YubiKeyData?>((ref) {
final notifier = CurrentDeviceDataNotifier(
ref.watch(rpcProvider),
ref.watch(currentDeviceProvider),
);
if (notifier._deviceNode is NfcReaderNode) {
// If this is an NFC reader, listen on WindowState.
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
notifier._notifyWindowState(windowState);
}, fireImmediately: true);
}
2022-01-12 14:49:04 +03:00
return notifier;
});
2022-01-12 14:49:04 +03:00
class CurrentDeviceDataNotifier extends StateNotifier<YubiKeyData?> {
final RpcSession _rpc;
final DeviceNode? _deviceNode;
Timer? _pollTimer;
CurrentDeviceDataNotifier(this._rpc, this._deviceNode) : super(null) {
final dev = _deviceNode;
if (dev is UsbYubiKeyNode) {
state = YubiKeyData(dev, dev.name, dev.info);
}
}
void _notifyWindowState(WindowState windowState) {
if (windowState.active) {
_pollReader();
} else {
_pollTimer?.cancel();
// TODO: Should we clear the key here?
/*if (mounted) {
state = null;
}*/
}
}
@override
void dispose() {
_pollTimer?.cancel();
super.dispose();
}
void _pollReader() async {
_pollTimer?.cancel();
final node = _deviceNode!;
try {
var result = await _rpc.command('get', node.path);
if (mounted) {
if (result['data']['present']) {
state = YubiKeyData(node, result['data']['name'],
DeviceInfo.fromJson(result['data']['info']));
} else {
state = null;
}
}
} on RpcError catch (e) {
log.severe('Error polling NFC', jsonEncode(e));
}
if (mounted) {
2022-01-18 17:46:42 +03:00
_pollTimer = Timer(
state == null ? _nfcAttachPollDelay : _nfcDetachPollDelay,
_pollReader);
2022-01-12 14:49:04 +03:00
}
}
}
2021-11-19 17:05:57 +03:00
final subPageProvider = StateNotifierProvider<SubPageNotifier, SubPage>(
(ref) => SubPageNotifier(SubPage.authenticator));
class SubPageNotifier extends StateNotifier<SubPage> {
SubPageNotifier(SubPage state) : super(state);
void setSubPage(SubPage page) {
state = page;
}
}
typedef BuildActions = List<MenuAction> Function(BuildContext);
final menuActionsProvider = Provider.autoDispose<BuildActions>((ref) {
switch (ref.watch(subPageProvider)) {
case SubPage.authenticator:
return (context) => buildOathMenuActions(context, ref);
case SubPage.yubikey:
// TODO: Handle this case.
break;
}
return (_) => [];
});