diff --git a/open_wearable/lib/widgets/fota/fota_warning_page.dart b/open_wearable/lib/widgets/fota/fota_warning_page.dart index 6827a02..b447131 100644 --- a/open_wearable/lib/widgets/fota/fota_warning_page.dart +++ b/open_wearable/lib/widgets/fota/fota_warning_page.dart @@ -3,10 +3,68 @@ import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:open_earable_flutter/open_earable_flutter.dart'; +import 'package:provider/provider.dart'; -class FotaWarningPage extends StatelessWidget { +class FotaWarningPage extends StatefulWidget { const FotaWarningPage({super.key}); + @override + State createState() => _FotaWarningPageState(); +} + +class _FotaWarningPageState extends State { + static const int _minimumBatteryThreshold = 50; + + int? _currentBatteryLevel; + bool _checkingBattery = true; + + @override + void initState() { + super.initState(); + _checkBatteryLevel(); + } + + Future _checkBatteryLevel() async { + try { + final updateProvider = Provider.of( + context, + listen: false, + ); + final device = updateProvider.selectedWearable; + + if (device != null && device is BatteryLevelStatus) { + // Get the current battery level from the stream + final batteryLevel = await (device as BatteryLevelStatus) + .batteryPercentageStream + .first + .timeout( + const Duration(seconds: 5), + onTimeout: () => 0, + ); + + if (mounted) { + setState(() { + _currentBatteryLevel = batteryLevel; + _checkingBattery = false; + }); + } + } else { + if (mounted) { + setState(() { + _checkingBattery = false; + }); + } + } + } catch (e) { + if (mounted) { + setState(() { + _checkingBattery = false; + }); + } + } + } + Future _openGitHubLink() async { final uri = Uri.parse( 'https://github.com/OpenEarable/open-earable-2?tab=readme-ov-file#setup', @@ -18,6 +76,111 @@ class FotaWarningPage extends StatelessWidget { } } + void _handleProceed() { + if (_currentBatteryLevel == null) { + // Battery level could not be determined + showPlatformDialog( + context: context, + builder: (_) => PlatformAlertDialog( + title: const Text('Battery Level Unknown'), + content: Text( + 'Unable to determine the OpenEarable battery level. ' + 'For safety, please ensure your OpenEarable is charged to at least $_minimumBatteryThreshold% before proceeding with the firmware update.\n\n' + 'Do you want to proceed anyway?', + ), + actions: [ + PlatformDialogAction( + cupertino: (_, __) => CupertinoDialogActionData(), + child: const Text('Cancel'), + onPressed: () => Navigator.of(context).pop(), + ), + PlatformDialogAction( + cupertino: (_, __) => CupertinoDialogActionData( + isDestructiveAction: true, + ), + child: const Text('Proceed Anyway'), + onPressed: () { + Navigator.of(context).pop(); + context.push('/fota/update'); + }, + ), + ], + ), + ); + } else if (_currentBatteryLevel! < _minimumBatteryThreshold) { + // Show first warning dialog with option to force update + _showLowBatteryWarning(); + } else { + context.push('/fota/update'); + } + } + + void _showLowBatteryWarning() { + showPlatformDialog( + context: context, + builder: (_) => PlatformAlertDialog( + title: const Text('Battery Level Too Low'), + content: Text( + 'Your OpenEarable battery level is $_currentBatteryLevel%, which is below the required $_minimumBatteryThreshold% minimum for firmware updates.\n\n' + 'Updating with low battery can cause the update to fail and may result in a bricked device.\n\n' + 'It is strongly recommended to charge your device before proceeding.', + ), + actions: [ + PlatformDialogAction( + cupertino: (_, __) => CupertinoDialogActionData( + isDefaultAction: true, + ), + child: const Text('Cancel'), + onPressed: () => Navigator.of(context).pop(), + ), + PlatformDialogAction( + cupertino: (_, __) => CupertinoDialogActionData( + isDestructiveAction: true, + ), + child: const Text('Force Update Anyway'), + onPressed: () { + Navigator.of(context).pop(); + _showFinalBrickingWarning(); + }, + ), + ], + ), + ); + } + + void _showFinalBrickingWarning() { + showPlatformDialog( + context: context, + builder: (_) => PlatformAlertDialog( + title: const Text('Critical Warning'), + content: Text( + 'FINAL WARNING: Proceeding with a firmware update at $_currentBatteryLevel% battery may permanently brick your OpenEarable device.\n\n' + 'You will not be able to recover the device if the update fails due to low battery.\n\n' + 'Are you absolutely sure you want to continue?', + ), + actions: [ + PlatformDialogAction( + cupertino: (_, __) => CupertinoDialogActionData( + isDefaultAction: true, + ), + child: const Text('Cancel'), + onPressed: () => Navigator.of(context).pop(), + ), + PlatformDialogAction( + cupertino: (_, __) => CupertinoDialogActionData( + isDestructiveAction: true, + ), + child: const Text('I Understand, Proceed'), + onPressed: () { + Navigator.of(context).pop(); + context.push('/fota/update'); + }, + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -107,47 +270,47 @@ class FotaWarningPage extends StatelessWidget { ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - _NumberedStep( + children: [ + const _NumberedStep( number: '1.', text: TextSpan( text: 'Power cycle your OpenEarable once before you update.', ), ), - SizedBox(height: 8), - _NumberedStep( + const SizedBox(height: 8), + const _NumberedStep( number: '2.', text: TextSpan( text: 'Keep the app open in the foreground and make sure your phone doesn’t enter power-saving mode.', ), ), - SizedBox(height: 8), + const SizedBox(height: 8), _NumberedStep( number: '3.', text: TextSpan( text: - 'Charge your OpenEarable fully before starting.', + 'Ensure your OpenEarable has at least $_minimumBatteryThreshold% battery charge before starting. Fully charging is recommended.', ), ), - SizedBox(height: 8), - _NumberedStep( + const SizedBox(height: 8), + const _NumberedStep( number: '4.', text: TextSpan( text: "Keep OpenEarable disconnected from charger during the update.", ), ), - SizedBox(height: 8), - _NumberedStep( + const SizedBox(height: 8), + const _NumberedStep( number: '5.', text: TextSpan( text: 'If you have two devices, power off the one that’s not being updated.', ), ), - SizedBox(height: 8), - _NumberedStep( + const SizedBox(height: 8), + const _NumberedStep( number: '6.', text: TextSpan( children: [ @@ -174,15 +337,83 @@ class FotaWarningPage extends StatelessWidget { const SizedBox(height: 24), + // Battery level warning if below 50% + if (_currentBatteryLevel != null && _currentBatteryLevel! < _minimumBatteryThreshold) + Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: theme.colorScheme.error.withValues(alpha: 0.1), + border: Border.all( + color: theme.colorScheme.error, + width: 2, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + Icons.battery_alert, + color: theme.colorScheme.error, + size: 24, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Battery level is $_currentBatteryLevel%. Please charge to at least $_minimumBatteryThreshold% before updating.', + style: baseTextStyle?.copyWith( + color: theme.colorScheme.error, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + + // Battery level warning if unknown + if (!_checkingBattery && _currentBatteryLevel == null) + Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.1), + border: Border.all( + color: Colors.orange, + width: 2, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + Icons.battery_unknown, + color: Colors.orange, + size: 24, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Unable to determine battery level. Please ensure your device is charged to at least $_minimumBatteryThreshold%.', + style: baseTextStyle?.copyWith( + color: Colors.orange.shade900, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + // Proceed button SizedBox( width: double.infinity, - child: PlatformElevatedButton( - onPressed: () { - context.push('/fota/update'); - }, - child: const Text('Acknowledge and Proceed'), - ), + child: _checkingBattery + ? const Center(child: CircularProgressIndicator()) + : PlatformElevatedButton( + onPressed: _handleProceed, + child: const Text('Acknowledge and Proceed'), + ), ), ], ),