Skip to content
Open
269 changes: 250 additions & 19 deletions open_wearable/lib/widgets/fota/fota_warning_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<FotaWarningPage> createState() => _FotaWarningPageState();
}

class _FotaWarningPageState extends State<FotaWarningPage> {
static const int _minimumBatteryThreshold = 50;

int? _currentBatteryLevel;
bool _checkingBattery = true;

@override
void initState() {
super.initState();
_checkBatteryLevel();
}

Future<void> _checkBatteryLevel() async {
try {
final updateProvider = Provider.of<FirmwareUpdateRequestProvider>(
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<void> _openGitHubLink() async {
final uri = Uri.parse(
'https://github.com/OpenEarable/open-earable-2?tab=readme-ov-file#setup',
Expand All @@ -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: <Widget>[
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: <Widget>[
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: <Widget>[
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);
Expand Down Expand Up @@ -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: [
Expand All @@ -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'),
),
),
],
),
Expand Down