yubioath-flutter/lib/management/views/management_screen.dart

332 lines
9.9 KiB
Dart
Raw Normal View History

2022-10-04 13:12:54 +03:00
/*
* Copyright (C) 2022 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.
*/
2022-10-03 16:34:06 +03:00
import 'package:collection/collection.dart';
2022-03-04 15:42:10 +03:00
import 'package:flutter/material.dart';
2022-09-07 14:59:44 +03:00
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2022-03-04 15:42:10 +03:00
import 'package:flutter_riverpod/flutter_riverpod.dart';
2022-03-25 17:43:32 +03:00
import '../../app/message.dart';
2022-03-04 15:42:10 +03:00
import '../../app/models.dart';
2022-03-16 14:43:56 +03:00
import '../../core/models.dart';
2022-06-05 14:49:16 +03:00
import '../../widgets/custom_icons.dart';
import '../../widgets/delayed_visibility.dart';
import '../../widgets/responsive_dialog.dart';
2022-03-04 15:42:10 +03:00
import '../models.dart';
import '../state.dart';
2022-10-03 16:39:57 +03:00
import 'keys.dart' as management_keys;
2022-03-04 15:42:10 +03:00
final _mapEquals = const DeepCollectionEquality().equals;
const _usbCcid = 0x04;
2022-03-04 15:42:10 +03:00
enum _CapabilityType { usb, nfc }
2022-10-03 16:34:06 +03:00
2022-03-04 15:42:10 +03:00
class _CapabilityForm extends StatelessWidget {
2022-10-03 16:34:06 +03:00
final _CapabilityType type;
2022-03-04 15:42:10 +03:00
final int capabilities;
final int enabled;
final Function(int) onChanged;
2022-05-12 10:56:55 +03:00
const _CapabilityForm({
2022-10-03 16:34:06 +03:00
required this.type,
2022-05-12 10:56:55 +03:00
required this.capabilities,
required this.enabled,
required this.onChanged,
});
2022-03-04 15:42:10 +03:00
@override
Widget build(BuildContext context) {
2022-10-03 16:34:06 +03:00
final keyPrefix = (type == _CapabilityType.usb)
2022-10-03 16:39:57 +03:00
? management_keys.usbCapabilityKeyPrefix
: management_keys.nfcCapabilityKeyPrefix;
2022-03-04 15:42:10 +03:00
return Wrap(
2022-05-20 15:38:23 +03:00
spacing: 8,
runSpacing: 16,
2022-03-04 15:42:10 +03:00
children: Capability.values
.where((c) => capabilities & c.value != 0)
.map((c) => FilterChip(
label: Text(c.name),
2022-10-03 16:34:06 +03:00
key: Key('$keyPrefix.${c.name}'),
2022-09-01 11:14:59 +03:00
selected: enabled & c.value != 0,
2022-03-04 15:42:10 +03:00
onSelected: (_) {
onChanged(enabled ^ c.value);
},
))
.toList(),
);
}
}
2022-05-05 13:40:56 +03:00
class _ModeForm extends StatelessWidget {
final int interfaces;
final Function(int) onChanged;
2022-05-12 10:56:55 +03:00
const _ModeForm(this.interfaces, {required this.onChanged});
@override
Widget build(BuildContext context) {
return Column(children: [
...UsbInterface.values.map(
(iface) => CheckboxListTile(
title: Text(iface.name.toUpperCase()),
2022-05-05 13:40:56 +03:00
value: iface.value & interfaces != 0,
onChanged: (_) {
2022-05-05 13:40:56 +03:00
onChanged(interfaces ^ iface.value);
},
),
),
2022-09-07 14:59:44 +03:00
Text(interfaces == 0
? AppLocalizations.of(context)!.mgmt_min_one_interface
: ''),
]);
}
}
2022-03-15 20:04:26 +03:00
class _CapabilitiesForm extends StatelessWidget {
final Map<Transport, int> supported;
final Map<Transport, int> enabled;
final Function(Map<Transport, int> enabled) onChanged;
2022-03-04 15:42:10 +03:00
2022-03-15 20:04:26 +03:00
const _CapabilitiesForm({
required this.onChanged,
required this.supported,
required this.enabled,
2022-05-12 10:56:55 +03:00
});
2022-03-04 15:42:10 +03:00
@override
Widget build(BuildContext context) {
2022-03-15 20:04:26 +03:00
final usbCapabilities = supported[Transport.usb] ?? 0;
final nfcCapabilities = supported[Transport.nfc] ?? 0;
2022-03-04 15:42:10 +03:00
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
2022-05-20 15:38:23 +03:00
if (usbCapabilities != 0) ...[
2023-02-28 13:34:29 +03:00
ListTile(
leading: const Icon(Icons.usb),
title: Text(AppLocalizations.of(context)!.general_usb),
contentPadding: const EdgeInsets.only(bottom: 8),
2022-05-20 15:38:23 +03:00
horizontalTitleGap: 0,
2022-03-04 15:42:10 +03:00
),
2022-05-20 15:38:23 +03:00
_CapabilityForm(
2022-10-03 16:34:06 +03:00
type: _CapabilityType.usb,
2022-05-20 15:38:23 +03:00
capabilities: usbCapabilities,
enabled: enabled[Transport.usb] ?? 0,
onChanged: (value) {
onChanged({...enabled, Transport.usb: value});
},
),
],
if (nfcCapabilities != 0) ...[
if (usbCapabilities != 0)
const Padding(
padding: EdgeInsets.only(top: 24, bottom: 12),
child: Divider(),
),
2022-06-05 14:49:16 +03:00
ListTile(
leading: nfcIcon,
2023-02-28 13:34:29 +03:00
title: Text(AppLocalizations.of(context)!.general_nfc),
2022-06-05 14:49:16 +03:00
contentPadding: const EdgeInsets.only(bottom: 8),
2022-05-20 15:38:23 +03:00
horizontalTitleGap: 0,
2022-03-04 15:42:10 +03:00
),
2022-05-20 15:38:23 +03:00
_CapabilityForm(
2022-10-03 16:34:06 +03:00
type: _CapabilityType.nfc,
2022-05-20 15:38:23 +03:00
capabilities: nfcCapabilities,
enabled: enabled[Transport.nfc] ?? 0,
onChanged: (value) {
onChanged({...enabled, Transport.nfc: value});
},
),
]
2022-03-04 15:42:10 +03:00
],
);
}
}
2022-03-15 20:04:26 +03:00
class ManagementScreen extends ConsumerStatefulWidget {
2022-03-04 15:42:10 +03:00
final YubiKeyData deviceData;
2022-10-03 16:39:57 +03:00
const ManagementScreen(this.deviceData)
: super(key: management_keys.screenKey);
2022-03-04 15:42:10 +03:00
2022-03-15 20:04:26 +03:00
@override
ConsumerState<ConsumerStatefulWidget> createState() =>
_ManagementScreenState();
}
class _ManagementScreenState extends ConsumerState<ManagementScreen> {
late Map<Transport, int> _enabled;
2022-05-05 13:40:56 +03:00
late int _interfaces;
2022-03-15 20:04:26 +03:00
@override
void initState() {
super.initState();
_enabled = widget.deviceData.info.config.enabledCapabilities;
2022-10-19 16:30:55 +03:00
_interfaces = UsbInterface.forCapabilities(
2022-05-05 13:40:56 +03:00
widget.deviceData.info.config.enabledCapabilities[Transport.usb] ?? 0);
2022-03-15 20:04:26 +03:00
}
Widget _buildCapabilitiesForm(
2022-03-15 20:04:26 +03:00
BuildContext context, WidgetRef ref, DeviceInfo info) {
return _CapabilitiesForm(
supported: widget.deviceData.info.supportedCapabilities,
enabled: _enabled,
onChanged: (enabled) {
setState(() {
_enabled = enabled;
});
},
);
}
void _submitCapabilitiesForm() async {
final bool reboot;
if (widget.deviceData.node is UsbYubiKeyNode) {
// Reboot if USB device descriptor is changed.
2022-10-19 16:30:55 +03:00
final oldInterfaces = UsbInterface.forCapabilities(
2022-03-15 20:04:26 +03:00
widget.deviceData.info.config.enabledCapabilities[Transport.usb] ??
0);
final newInterfaces =
2022-10-19 16:30:55 +03:00
UsbInterface.forCapabilities(_enabled[Transport.usb] ?? 0);
2022-03-15 20:04:26 +03:00
reboot = oldInterfaces != newInterfaces;
} else {
reboot = false;
}
Function()? close;
try {
if (reboot) {
// This will take longer, show a message
2022-03-25 17:43:32 +03:00
close = showMessage(
context,
2022-09-07 14:59:44 +03:00
AppLocalizations.of(context)!.mgmt_reconfiguring_yubikey,
2022-03-25 17:43:32 +03:00
duration: const Duration(seconds: 8),
);
2022-03-15 20:04:26 +03:00
}
await ref
.read(managementStateProvider(widget.deviceData.node.path).notifier)
.writeConfig(
widget.deviceData.info.config
.copyWith(enabledCapabilities: _enabled),
reboot: reboot,
);
2022-05-12 09:34:51 +03:00
if (!mounted) return;
2022-04-04 20:59:49 +03:00
if (!reboot) Navigator.pop(context);
2022-09-07 14:59:44 +03:00
showMessage(
context, AppLocalizations.of(context)!.mgmt_configuration_updated);
2022-03-15 20:04:26 +03:00
} finally {
close?.call();
}
}
Widget _buildModeForm(BuildContext context, WidgetRef ref, DeviceInfo info) =>
_ModeForm(
2022-05-05 13:40:56 +03:00
_interfaces,
onChanged: (interfaces) {
setState(() {
_interfaces = interfaces;
});
},
);
void _submitModeForm() async {
await ref
.read(managementStateProvider(widget.deviceData.node.path).notifier)
.setMode(interfaces: _interfaces);
2022-05-12 09:34:51 +03:00
if (!mounted) return;
2022-05-05 13:40:56 +03:00
showMessage(
2022-05-06 09:34:16 +03:00
context,
widget.deviceData.node.maybeMap(
2022-09-07 14:59:44 +03:00
nfcReader: (_) =>
AppLocalizations.of(context)!.mgmt_configuration_updated,
orElse: () => AppLocalizations.of(context)!
.mgmt_configuration_updated_remove_reinsert));
2022-05-06 09:34:16 +03:00
Navigator.pop(context);
2022-05-05 13:40:56 +03:00
}
void _submitForm() {
if (widget.deviceData.info.version.major > 4) {
_submitCapabilitiesForm();
} else {
_submitModeForm();
}
}
2022-03-04 15:42:10 +03:00
@override
2022-03-15 20:04:26 +03:00
Widget build(BuildContext context) {
var canSave = false;
2022-09-07 11:36:12 +03:00
final child = ref
.watch(managementStateProvider(widget.deviceData.node.path))
.when(
loading: () => const Center(
child: DelayedVisibility(
delay: Duration(milliseconds: 200),
child: CircularProgressIndicator(),
)),
2022-09-07 11:36:12 +03:00
error: (error, _) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
error.toString(),
textAlign: TextAlign.center,
2022-05-20 18:19:39 +03:00
),
2022-09-07 11:36:12 +03:00
],
),
),
data: (info) {
bool hasConfig = info.version.major > 4;
int usbEnabled = _enabled[Transport.usb] ?? 0;
2022-09-07 11:36:12 +03:00
if (hasConfig) {
// Ignore the _usbCcid bit:
canSave = (usbEnabled & ~_usbCcid) != 0 &&
2022-09-07 11:36:12 +03:00
!_mapEquals(
_enabled,
info.config.enabledCapabilities,
);
} else {
canSave = _interfaces != 0 &&
_interfaces !=
2022-10-19 16:30:55 +03:00
UsbInterface.forCapabilities(widget.deviceData.info.config
2022-09-07 11:36:12 +03:00
.enabledCapabilities[Transport.usb] ??
0);
}
return Column(
children: [
hasConfig
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: _buildCapabilitiesForm(context, ref, info),
)
: _buildModeForm(context, ref, info),
],
);
2022-09-07 11:36:12 +03:00
},
);
2022-03-15 20:04:26 +03:00
return ResponsiveDialog(
2022-09-07 14:59:44 +03:00
title: Text(AppLocalizations.of(context)!.mgmt_toggle_applications),
2022-05-12 09:34:51 +03:00
actions: [
TextButton(
onPressed: canSave ? _submitForm : null,
2022-10-03 16:39:57 +03:00
key: management_keys.saveButtonKey,
2022-09-07 14:59:44 +03:00
child: Text(AppLocalizations.of(context)!.mgmt_save),
2022-05-12 09:34:51 +03:00
),
],
2022-09-07 11:36:12 +03:00
child: child,
2022-03-15 20:04:26 +03:00
);
}
2022-03-04 15:42:10 +03:00
}