From 2652a752366a8fa1f7761ad6ab207d747d5cba18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 10:41:07 +0000 Subject: [PATCH 1/9] Initial plan From 5e758c3f9faa5a2b35eda3225c2ad3d9ad792201 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 10:47:00 +0000 Subject: [PATCH 2/9] Add battery level check to reject FOTA updates below 50% Co-authored-by: DennisMoschina <45356478+DennisMoschina@users.noreply.github.com> --- .../lib/widgets/fota/fota_warning_page.dart | 134 ++++++++++++++++-- 1 file changed, 126 insertions(+), 8 deletions(-) diff --git a/open_wearable/lib/widgets/fota/fota_warning_page.dart b/open_wearable/lib/widgets/fota/fota_warning_page.dart index 6827a02..3953dea 100644 --- a/open_wearable/lib/widgets/fota/fota_warning_page.dart +++ b/open_wearable/lib/widgets/fota/fota_warning_page.dart @@ -1,12 +1,69 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.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 { + 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.updateParameters.peripheral; + + 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: () => null, + ); + + 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 +75,33 @@ class FotaWarningPage extends StatelessWidget { } } + void _handleProceed() { + if (_currentBatteryLevel != null && _currentBatteryLevel! < 50) { + // Show error dialog + 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 50% minimum for firmware updates.\n\n' + 'Please charge your OpenEarable to at least 50% before attempting a firmware update to prevent issues during the update process.', + ), + actions: [ + PlatformDialogAction( + cupertino: (_, __) => CupertinoDialogActionData( + isDefaultAction: true, + ), + child: const Text('OK'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ); + } else { + context.push('/fota/update'); + } + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -128,7 +212,7 @@ class FotaWarningPage extends StatelessWidget { number: '3.', text: TextSpan( text: - 'Charge your OpenEarable fully before starting.', + 'Ensure your OpenEarable has at least 50% battery charge before starting. Fully charging is recommended.', ), ), SizedBox(height: 8), @@ -174,15 +258,49 @@ class FotaWarningPage extends StatelessWidget { const SizedBox(height: 24), + // Battery level warning if below 50% + if (_currentBatteryLevel != null && _currentBatteryLevel! < 50) + Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: theme.colorScheme.error.withOpacity(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 50% before updating.', + style: baseTextStyle?.copyWith( + color: theme.colorScheme.error, + 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'), + ), ), ], ), From 72072b15d7982687c93aba40649ca65b0153f6ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 10:49:35 +0000 Subject: [PATCH 3/9] Improve battery check: handle unknown battery levels and remove redundant cast Co-authored-by: DennisMoschina <45356478+DennisMoschina@users.noreply.github.com> --- .../lib/widgets/fota/fota_warning_page.dart | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/open_wearable/lib/widgets/fota/fota_warning_page.dart b/open_wearable/lib/widgets/fota/fota_warning_page.dart index 3953dea..9f2798f 100644 --- a/open_wearable/lib/widgets/fota/fota_warning_page.dart +++ b/open_wearable/lib/widgets/fota/fota_warning_page.dart @@ -34,7 +34,7 @@ class _FotaWarningPageState extends State { if (device != null && device is BatteryLevelStatus) { // Get the current battery level from the stream - final batteryLevel = await (device as BatteryLevelStatus) + final batteryLevel = await device .batteryPercentageStream .first .timeout( @@ -76,7 +76,37 @@ class _FotaWarningPageState extends State { } void _handleProceed() { - if (_currentBatteryLevel != null && _currentBatteryLevel! < 50) { + if (_currentBatteryLevel == null) { + // Battery level could not be determined + showPlatformDialog( + context: context, + builder: (_) => PlatformAlertDialog( + title: const Text('Battery Level Unknown'), + content: const Text( + 'Unable to determine the OpenEarable battery level. ' + 'For safety, please ensure your OpenEarable is charged to at least 50% 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! < 50) { // Show error dialog showPlatformDialog( context: context, @@ -291,6 +321,40 @@ class _FotaWarningPageState extends State { ], ), ), + + // 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.withOpacity(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 50%.', + style: baseTextStyle?.copyWith( + color: Colors.orange.shade900, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), // Proceed button SizedBox( From ee61cdb7dfc3f7aa5dccb7c523ce3a3153b64a1c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 10:52:06 +0000 Subject: [PATCH 4/9] Extract battery threshold to named constant for better maintainability Co-authored-by: DennisMoschina <45356478+DennisMoschina@users.noreply.github.com> --- .../lib/widgets/fota/fota_warning_page.dart | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/open_wearable/lib/widgets/fota/fota_warning_page.dart b/open_wearable/lib/widgets/fota/fota_warning_page.dart index 9f2798f..3e85d31 100644 --- a/open_wearable/lib/widgets/fota/fota_warning_page.dart +++ b/open_wearable/lib/widgets/fota/fota_warning_page.dart @@ -15,6 +15,8 @@ class FotaWarningPage extends StatefulWidget { } class _FotaWarningPageState extends State { + static const int _minimumBatteryThreshold = 50; + int? _currentBatteryLevel; bool _checkingBattery = true; @@ -82,9 +84,9 @@ class _FotaWarningPageState extends State { context: context, builder: (_) => PlatformAlertDialog( title: const Text('Battery Level Unknown'), - content: const Text( + content: Text( 'Unable to determine the OpenEarable battery level. ' - 'For safety, please ensure your OpenEarable is charged to at least 50% before proceeding with the firmware update.\n\n' + '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: [ @@ -106,15 +108,15 @@ class _FotaWarningPageState extends State { ], ), ); - } else if (_currentBatteryLevel! < 50) { + } else if (_currentBatteryLevel! < _minimumBatteryThreshold) { // Show error dialog 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 50% minimum for firmware updates.\n\n' - 'Please charge your OpenEarable to at least 50% before attempting a firmware update to prevent issues during the update process.', + 'Your OpenEarable battery level is $_currentBatteryLevel%, which is below the required $_minimumBatteryThreshold% minimum for firmware updates.\n\n' + 'Please charge your OpenEarable to at least $_minimumBatteryThreshold% before attempting a firmware update to prevent issues during the update process.', ), actions: [ PlatformDialogAction( @@ -242,7 +244,7 @@ class _FotaWarningPageState extends State { number: '3.', text: TextSpan( text: - 'Ensure your OpenEarable has at least 50% battery charge before starting. Fully charging is recommended.', + 'Ensure your OpenEarable has at least $_minimumBatteryThreshold% battery charge before starting. Fully charging is recommended.', ), ), SizedBox(height: 8), @@ -289,7 +291,7 @@ class _FotaWarningPageState extends State { const SizedBox(height: 24), // Battery level warning if below 50% - if (_currentBatteryLevel != null && _currentBatteryLevel! < 50) + if (_currentBatteryLevel != null && _currentBatteryLevel! < _minimumBatteryThreshold) Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(12), @@ -311,7 +313,7 @@ class _FotaWarningPageState extends State { const SizedBox(width: 12), Expanded( child: Text( - 'Battery level is ${_currentBatteryLevel}%. Please charge to at least 50% before updating.', + 'Battery level is $_currentBatteryLevel%. Please charge to at least $_minimumBatteryThreshold% before updating.', style: baseTextStyle?.copyWith( color: theme.colorScheme.error, fontWeight: FontWeight.w600, @@ -345,7 +347,7 @@ class _FotaWarningPageState extends State { const SizedBox(width: 12), Expanded( child: Text( - 'Unable to determine battery level. Please ensure your device is charged to at least 50%.', + '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, From eceb7fe3cfcb3e5140a6361d2444ecffb517a609 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 10:54:56 +0000 Subject: [PATCH 5/9] Fix const keyword issue with interpolated battery threshold Co-authored-by: DennisMoschina <45356478+DennisMoschina@users.noreply.github.com> --- .../lib/widgets/fota/fota_warning_page.dart | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/open_wearable/lib/widgets/fota/fota_warning_page.dart b/open_wearable/lib/widgets/fota/fota_warning_page.dart index 3e85d31..eb42065 100644 --- a/open_wearable/lib/widgets/fota/fota_warning_page.dart +++ b/open_wearable/lib/widgets/fota/fota_warning_page.dart @@ -223,23 +223,23 @@ class _FotaWarningPageState extends State { ), 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( @@ -247,23 +247,23 @@ class _FotaWarningPageState extends State { '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: [ From e8923461f93d10553a7e864666f61a8e77f17a91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:08:26 +0000 Subject: [PATCH 6/9] Add two-step forced update confirmation for low battery scenarios Co-authored-by: DennisMoschina <45356478+DennisMoschina@users.noreply.github.com> --- .../lib/widgets/fota/fota_warning_page.dart | 88 ++++++++++++++----- 1 file changed, 68 insertions(+), 20 deletions(-) diff --git a/open_wearable/lib/widgets/fota/fota_warning_page.dart b/open_wearable/lib/widgets/fota/fota_warning_page.dart index eb42065..2402493 100644 --- a/open_wearable/lib/widgets/fota/fota_warning_page.dart +++ b/open_wearable/lib/widgets/fota/fota_warning_page.dart @@ -109,31 +109,79 @@ class _FotaWarningPageState extends State { ), ); } else if (_currentBatteryLevel! < _minimumBatteryThreshold) { - // Show error dialog - 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' - 'Please charge your OpenEarable to at least $_minimumBatteryThreshold% before attempting a firmware update to prevent issues during the update process.', - ), - actions: [ - PlatformDialogAction( - cupertino: (_, __) => CupertinoDialogActionData( - isDefaultAction: true, - ), - child: const Text('OK'), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - ); + // 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); From 984dda8a0d0edd99873d27f45a1a2c72465426ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:09:23 +0000 Subject: [PATCH 7/9] Remove emoji from dialog title for better accessibility Co-authored-by: DennisMoschina <45356478+DennisMoschina@users.noreply.github.com> --- open_wearable/lib/widgets/fota/fota_warning_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/open_wearable/lib/widgets/fota/fota_warning_page.dart b/open_wearable/lib/widgets/fota/fota_warning_page.dart index 2402493..d66cafa 100644 --- a/open_wearable/lib/widgets/fota/fota_warning_page.dart +++ b/open_wearable/lib/widgets/fota/fota_warning_page.dart @@ -153,7 +153,7 @@ class _FotaWarningPageState extends State { showPlatformDialog( context: context, builder: (_) => PlatformAlertDialog( - title: const Text('⚠️ Critical Warning'), + 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' From 1976e3deed3a144e95b231cde76c96cd472d2cae Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:34:01 +0100 Subject: [PATCH 8/9] open_wearable/lib/widgets/fota/fota_warning_page.dart: fixed issues with flutter analyze --- open_wearable/lib/widgets/fota/fota_warning_page.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/open_wearable/lib/widgets/fota/fota_warning_page.dart b/open_wearable/lib/widgets/fota/fota_warning_page.dart index d66cafa..98f4d53 100644 --- a/open_wearable/lib/widgets/fota/fota_warning_page.dart +++ b/open_wearable/lib/widgets/fota/fota_warning_page.dart @@ -1,6 +1,5 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -36,12 +35,12 @@ class _FotaWarningPageState extends State { if (device != null && device is BatteryLevelStatus) { // Get the current battery level from the stream - final batteryLevel = await device + final batteryLevel = await (device as BatteryLevelStatus) .batteryPercentageStream .first .timeout( const Duration(seconds: 5), - onTimeout: () => null, + onTimeout: () => 0, ); if (mounted) { @@ -344,7 +343,7 @@ class _FotaWarningPageState extends State { margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: theme.colorScheme.error.withOpacity(0.1), + color: theme.colorScheme.error.withValues(alpha: 0.1), border: Border.all( color: theme.colorScheme.error, width: 2, @@ -378,7 +377,7 @@ class _FotaWarningPageState extends State { margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.orange.withOpacity(0.1), + color: Colors.orange.withValues(alpha: 0.1), border: Border.all( color: Colors.orange, width: 2, From 0b3b0e2b9fe406e22377c5ff019fa197eb435459 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Mon, 22 Dec 2025 18:00:37 +0100 Subject: [PATCH 9/9] open_wearable/lib/widgets/fota/fota_warning_page.dart: use wearable to read battery --- open_wearable/lib/widgets/fota/fota_warning_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/open_wearable/lib/widgets/fota/fota_warning_page.dart b/open_wearable/lib/widgets/fota/fota_warning_page.dart index 98f4d53..b447131 100644 --- a/open_wearable/lib/widgets/fota/fota_warning_page.dart +++ b/open_wearable/lib/widgets/fota/fota_warning_page.dart @@ -31,7 +31,7 @@ class _FotaWarningPageState extends State { context, listen: false, ); - final device = updateProvider.updateParameters.peripheral; + final device = updateProvider.selectedWearable; if (device != null && device is BatteryLevelStatus) { // Get the current battery level from the stream