fix(#443): warn + guide user to exempt app from battery optimization

On aggressive OEMs (Samsung One UI) a backgrounded app that is not exempt
from battery optimization is slept on screen-off and drops the radio
connection, with no warning. Add a persistent banner on the channels screen
that appears (Android only) when the app is not exempt, explains the risk,
and deep-links to the battery settings via
openIgnoreBatteryOptimizationSettings(). Re-checks on resume so it clears
once the user applies the change; dismissible for the session.

No restricted permission: reads own status and opens the settings screen
(ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS); the Play-restricted
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS path is deliberately avoided.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pull/460/head
Strycher 2 months ago
parent 8920cf928f
commit b838f4fdba

@ -2667,5 +2667,21 @@
},
"@storageUnavailableBody": {
"description": "Persistent banner body explaining that storage is unavailable and messages are not being saved (#385)."
},
"batteryOptimizationTitle": "Battery optimization is on",
"@batteryOptimizationTitle": {
"description": "Title of the persistent banner warning that Android battery optimization may drop the background radio connection (#443)."
},
"batteryOptimizationBody": "Android may sleep the app when the screen is off and drop the radio connection. Set this app's battery usage to Unrestricted for a reliable background connection.",
"@batteryOptimizationBody": {
"description": "Body of the battery-optimization warning banner (#443)."
},
"batteryOptimizationOpenSettings": "Open battery settings",
"@batteryOptimizationOpenSettings": {
"description": "Button that opens the Android battery-optimization settings screen (#443)."
},
"batteryOptimizationDismiss": "Dismiss",
"@batteryOptimizationDismiss": {
"description": "Tooltip for the button that hides the battery-optimization banner for this session (#443)."
}
}

@ -8055,6 +8055,30 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.'**
String get storageUnavailableBody;
/// Title of the persistent banner warning that Android battery optimization may drop the background radio connection (#443).
///
/// In en, this message translates to:
/// **'Battery optimization is on'**
String get batteryOptimizationTitle;
/// Body of the battery-optimization warning banner (#443).
///
/// In en, this message translates to:
/// **'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.'**
String get batteryOptimizationBody;
/// Button that opens the Android battery-optimization settings screen (#443).
///
/// In en, this message translates to:
/// **'Open battery settings'**
String get batteryOptimizationOpenSettings;
/// Tooltip for the button that hides the battery-optimization banner for this session (#443).
///
/// In en, this message translates to:
/// **'Dismiss'**
String get batteryOptimizationDismiss;
}
class _AppLocalizationsDelegate

@ -4713,4 +4713,17 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4730,4 +4730,17 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4635,4 +4635,17 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4718,4 +4718,17 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4746,4 +4746,17 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4735,4 +4735,17 @@ class AppLocalizationsHu extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4723,4 +4723,17 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4489,4 +4489,17 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4490,4 +4490,17 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4698,4 +4698,17 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4735,4 +4735,17 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4711,4 +4711,17 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4729,4 +4729,17 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4695,4 +4695,17 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4693,4 +4693,17 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4668,4 +4668,17 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4730,4 +4730,17 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -4363,4 +4363,17 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get storageUnavailableBody =>
'Your history is not lost, but the app can\'t open its database, so new messages are NOT being saved. Restart the app after fixing the problem. See the app log for details.';
@override
String get batteryOptimizationTitle => 'Battery optimization is on';
@override
String get batteryOptimizationBody =>
'Android may sleep the app when the screen is off and drop the radio connection. Set this app\'s battery usage to Unrestricted for a reliable background connection.';
@override
String get batteryOptimizationOpenSettings => 'Open battery settings';
@override
String get batteryOptimizationDismiss => 'Dismiss';
}

@ -27,6 +27,7 @@ import '../widgets/channel_drawer_list.dart';
import '../widgets/sync_progress_overlay.dart';
import '../widgets/unread_badge.dart';
import '../widgets/channel_notify_mode.dart';
import '../widgets/battery_optimization_banner.dart';
import '../helpers/snack_bar_builder.dart';
import 'channel_chat_screen.dart';
import 'community_qr_scanner_screen.dart';
@ -138,185 +139,187 @@ class _ChannelsScreenState extends State<ChannelsScreen>
),
],
),
body: RefreshIndicator(
onRefresh: () async {
await context.read<MeshCoreConnector>().getChannels(force: true);
},
child: () {
final channels = connector.channels;
final waitingForFirstChannel =
connector.isLoadingChannels && channels.isEmpty;
// Only block the list while the first channel is actively loading.
// If the initial sync aborts, show cached/partial channels instead
// of trapping the user behind an idle spinner.
if (waitingForFirstChannel) {
return const Center(child: CircularProgressIndicator());
}
body: BatteryOptimizationBanner(
child: RefreshIndicator(
onRefresh: () async {
await context.read<MeshCoreConnector>().getChannels(force: true);
},
child: () {
final channels = connector.channels;
final waitingForFirstChannel =
connector.isLoadingChannels && channels.isEmpty;
// Only block the list while the first channel is actively loading.
// If the initial sync aborts, show cached/partial channels instead
// of trapping the user behind an idle spinner.
if (waitingForFirstChannel) {
return const Center(child: CircularProgressIndicator());
}
if (channels.isEmpty) {
return ListView(
children: [
SizedBox(
height: MediaQuery.of(context).size.height - 200,
child: EmptyState(
icon: Icons.tag,
title: context.l10n.channels_noChannelsConfigured,
action: FilledButton.icon(
onPressed: () => _addPublicChannel(context, connector),
icon: const Icon(Icons.public),
label: Text(context.l10n.channels_addPublicChannel),
if (channels.isEmpty) {
return ListView(
children: [
SizedBox(
height: MediaQuery.of(context).size.height - 200,
child: EmptyState(
icon: Icons.tag,
title: context.l10n.channels_noChannelsConfigured,
action: FilledButton.icon(
onPressed: () => _addPublicChannel(context, connector),
icon: const Icon(Icons.public),
label: Text(context.l10n.channels_addPublicChannel),
),
),
),
),
],
);
}
],
);
}
final filteredChannels = _filterAndSortChannels(
channels,
connector,
viewState,
);
final filteredChannels = _filterAndSortChannels(
channels,
connector,
viewState,
);
return Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: context.l10n.channels_searchChannels,
prefixIcon: const Icon(Icons.search),
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (viewState.channelsSearchText.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchDebounce?.cancel();
_searchDebounce = null;
_searchController.clear();
context
.read<UiViewStateService>()
.setChannelsSearchText('');
},
),
_buildFilterButton(viewState),
],
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
return Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: context.l10n.channels_searchChannels,
prefixIcon: const Icon(Icons.search),
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (viewState.channelsSearchText.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchDebounce?.cancel();
_searchDebounce = null;
_searchController.clear();
context
.read<UiViewStateService>()
.setChannelsSearchText('');
},
),
_buildFilterButton(viewState),
],
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
_searchDebounce?.cancel();
_searchDebounce = Timer(
const Duration(milliseconds: 300),
() {
if (!mounted) return;
context
.read<UiViewStateService>()
.setChannelsSearchText(value);
},
);
},
),
onChanged: (value) {
_searchDebounce?.cancel();
_searchDebounce = Timer(
const Duration(milliseconds: 300),
() {
if (!mounted) return;
context
.read<UiViewStateService>()
.setChannelsSearchText(value);
},
);
},
),
),
Expanded(
child: filteredChannels.isEmpty
? ListView(
children: [
SizedBox(
height: MediaQuery.of(context).size.height - 300,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
context.l10n.channels_noChannelsFound,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
Expanded(
child: filteredChannels.isEmpty
? ListView(
children: [
SizedBox(
height: MediaQuery.of(context).size.height - 300,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
context.l10n.channels_noChannelsFound,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
),
],
],
),
),
),
],
)
: (viewState.channelsSortOption ==
ChannelSortOption.manual &&
viewState.channelsSearchText.isEmpty)
? ReorderableListView.builder(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 8,
bottom: 88,
),
],
)
: (viewState.channelsSortOption ==
ChannelSortOption.manual &&
viewState.channelsSearchText.isEmpty)
? ReorderableListView.builder(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 8,
bottom: 88,
),
buildDefaultDragHandles: false,
itemCount: filteredChannels.length,
onReorderItem: (oldIndex, newIndex) {
// onReorderItem already adjusts newIndex after the
// removed item, unlike the deprecated onReorder.
final reordered = List<Channel>.from(
filteredChannels,
);
final item = reordered.removeAt(oldIndex);
reordered.insert(newIndex, item);
unawaited(
connector.setChannelOrder(
reordered.map((c) => c.index).toList(),
),
);
},
itemBuilder: (context, index) {
final channel = filteredChannels[index];
return _buildChannelTile(
context,
connector,
channelMessageStore,
channel,
showDragHandle: true,
dragIndex: index,
);
},
)
: ListView.builder(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 8,
bottom: 88,
buildDefaultDragHandles: false,
itemCount: filteredChannels.length,
onReorderItem: (oldIndex, newIndex) {
// onReorderItem already adjusts newIndex after the
// removed item, unlike the deprecated onReorder.
final reordered = List<Channel>.from(
filteredChannels,
);
final item = reordered.removeAt(oldIndex);
reordered.insert(newIndex, item);
unawaited(
connector.setChannelOrder(
reordered.map((c) => c.index).toList(),
),
);
},
itemBuilder: (context, index) {
final channel = filteredChannels[index];
return _buildChannelTile(
context,
connector,
channelMessageStore,
channel,
showDragHandle: true,
dragIndex: index,
);
},
)
: ListView.builder(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 8,
bottom: 88,
),
itemCount: filteredChannels.length,
itemBuilder: (context, index) {
final channel = filteredChannels[index];
return _buildChannelTile(
context,
connector,
channelMessageStore,
channel,
);
},
),
itemCount: filteredChannels.length,
itemBuilder: (context, index) {
final channel = filteredChannels[index];
return _buildChannelTile(
context,
connector,
channelMessageStore,
channel,
);
},
),
),
],
);
}(),
),
],
);
}(),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddChannelDialog(context),

@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import '../l10n/l10n.dart';
import '../utils/platform_info.dart';
/// Warns the user when the app is NOT exempt from Android battery optimization
/// and guides them to the settings to fix it (#443).
///
/// On aggressive OEMs (e.g. Samsung One UI) a backgrounded app that is not
/// exempt gets put to sleep on screen-off, dropping the radio connection. The
/// app can't force the exemption without the Play-restricted
/// `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission, so it only reads its own
/// status and deep-links to the settings screen — no restricted permission,
/// no Play-policy impact.
///
/// Mirrors [StorageUnavailableBanner]'s wrap-child shape: when there is nothing
/// to warn about (non-Android, already exempt, or dismissed) this is a
/// transparent pass-through and adds no layout.
class BatteryOptimizationBanner extends StatefulWidget {
final Widget child;
const BatteryOptimizationBanner({super.key, required this.child});
@override
State<BatteryOptimizationBanner> createState() =>
_BatteryOptimizationBannerState();
}
class _BatteryOptimizationBannerState extends State<BatteryOptimizationBanner>
with WidgetsBindingObserver {
// Assume exempt until the async check says otherwise, so the banner never
// flashes on first frame.
bool _ignoring = true;
bool _dismissed = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_check();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// Re-check on resume so the banner clears once the user sets the app to
// Unrestricted in settings.
if (state == AppLifecycleState.resumed) {
_check();
}
}
Future<void> _check() async {
if (!PlatformInfo.isAndroid) return;
final ignoring = await FlutterForegroundTask.isIgnoringBatteryOptimizations;
if (!mounted) return;
setState(() => _ignoring = ignoring);
}
Future<void> _openSettings() async {
await FlutterForegroundTask.openIgnoreBatteryOptimizationSettings();
// The user returns via resume, which triggers _check() and clears the
// banner if they applied the change.
}
@override
Widget build(BuildContext context) {
final show = PlatformInfo.isAndroid && !_ignoring && !_dismissed;
if (!show) return widget.child;
final scheme = Theme.of(context).colorScheme;
return Column(
children: [
Material(
color: scheme.secondaryContainer,
child: SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 8, 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.battery_alert_outlined,
color: scheme.onSecondaryContainer,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.batteryOptimizationTitle,
style: TextStyle(
fontWeight: FontWeight.bold,
color: scheme.onSecondaryContainer,
),
),
const SizedBox(height: 2),
Text(
context.l10n.batteryOptimizationBody,
style: TextStyle(color: scheme.onSecondaryContainer),
),
const SizedBox(height: 4),
Align(
alignment: Alignment.centerLeft,
child: TextButton(
onPressed: _openSettings,
child: Text(
context.l10n.batteryOptimizationOpenSettings,
),
),
),
],
),
),
IconButton(
icon: const Icon(Icons.close),
color: scheme.onSecondaryContainer,
tooltip: context.l10n.batteryOptimizationDismiss,
onPressed: () => setState(() => _dismissed = true),
),
],
),
),
),
),
Expanded(child: widget.child),
],
);
}
}
Loading…
Cancel
Save

Powered by TurnKey Linux.