Try using foreground tasks for reliability

This commit is contained in:
Imran Remtulla
2025-07-01 15:05:26 -04:00
parent 6055ae6a69
commit 7dbf3ac102
10 changed files with 186 additions and 108 deletions

View File

@@ -9,16 +9,15 @@ import 'package:obtainium/providers/native_provider.dart';
import 'package:obtainium/providers/notifications_provider.dart';
import 'package:obtainium/providers/settings_provider.dart';
import 'package:obtainium/providers/source_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:background_fetch/background_fetch.dart';
import 'package:easy_localization/easy_localization.dart';
// ignore: implementation_imports
import 'package:easy_localization/src/easy_localization_controller.dart';
// ignore: implementation_imports
import 'package:easy_localization/src/localization.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
List<MapEntry<Locale, String>> supportedLocales = const [
MapEntry(Locale('en'), 'English'),
@@ -86,16 +85,35 @@ Future<void> loadTranslations() async {
}
@pragma('vm:entry-point')
void backgroundFetchHeadlessTask(HeadlessTask task) async {
String taskId = task.taskId;
bool isTimeout = task.timeout;
if (isTimeout) {
print('BG update task timed out.');
BackgroundFetch.finish(taskId);
return;
void startCallback() {
FlutterForegroundTask.setTaskHandler(MyTaskHandler());
}
class MyTaskHandler extends TaskHandler {
static const String incrementCountCommand = 'incrementCount';
// Called when the task is started.
@override
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {
print('onStart(starter: ${starter.name})');
bgUpdateCheck('bg_check', null);
}
await bgUpdateCheck(taskId, null);
BackgroundFetch.finish(taskId);
// Called based on the eventAction set in ForegroundTaskOptions.
@override
void onRepeatEvent(DateTime timestamp) {
bgUpdateCheck('Foreground service bg_check', null);
}
// Called when the task is destroyed.
@override
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {
print('Foreground service onDestroy(isTimeout: $isTimeout)');
}
// Called when data is sent using `FlutterForegroundTask.sendDataToTask`.
@override
void onReceiveData(Object data) {}
}
void main() async {
@@ -119,6 +137,7 @@ void main() async {
}
final np = NotificationsProvider();
await np.initialize();
FlutterForegroundTask.initCommunicationPort();
runApp(
MultiProvider(
providers: [
@@ -136,7 +155,6 @@ void main() async {
),
),
);
BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
}
class Obtainium extends StatefulWidget {
@@ -152,32 +170,72 @@ class _ObtainiumState extends State<Obtainium> {
@override
void initState() {
super.initState();
initPlatformState();
// FlutterForegroundTask.addTaskDataCallback(onReceiveForegroundServiceData);
WidgetsBinding.instance.addPostFrameCallback((_) {
requestNonOptionalPermissions();
initForegroundService();
});
}
Future<void> initPlatformState() async {
await BackgroundFetch.configure(
BackgroundFetchConfig(
minimumFetchInterval: 15,
stopOnTerminate: false,
startOnBoot: true,
enableHeadless: true,
requiresBatteryNotLow: false,
requiresCharging: false,
requiresStorageNotLow: false,
requiresDeviceIdle: false,
requiredNetworkType: NetworkType.ANY,
Future<void> requestNonOptionalPermissions() async {
final NotificationPermission notificationPermission =
await FlutterForegroundTask.checkNotificationPermission();
if (notificationPermission != NotificationPermission.granted) {
await FlutterForegroundTask.requestNotificationPermission();
}
if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) {
await FlutterForegroundTask.requestIgnoreBatteryOptimization();
}
}
void initForegroundService() {
FlutterForegroundTask.init(
androidNotificationOptions: AndroidNotificationOptions(
channelId: 'bg_update',
channelName: tr('placeholder'),
channelDescription: tr('placeholder'),
onlyAlertOnce: true,
),
iosNotificationOptions: const IOSNotificationOptions(
showNotification: false,
playSound: false,
),
foregroundTaskOptions: ForegroundTaskOptions(
eventAction: ForegroundTaskEventAction.repeat(900000),
autoRunOnBoot: true,
autoRunOnMyPackageReplaced: true,
allowWakeLock: true,
allowWifiLock: true,
),
(String taskId) async {
await bgUpdateCheck(taskId, null);
BackgroundFetch.finish(taskId);
},
(String taskId) async {
context.read<LogsProvider>().add('BG update task timed out.');
BackgroundFetch.finish(taskId);
},
);
if (!mounted) return;
}
Future<ServiceRequestResult> startForegroundService() async {
if (await FlutterForegroundTask.isRunningService) {
return FlutterForegroundTask.restartService();
} else {
return FlutterForegroundTask.startService(
serviceTypes: [ForegroundServiceTypes.specialUse],
serviceId: 666,
notificationTitle: tr('placeholder'),
notificationText: tr('placeholder'),
notificationIcon: NotificationIcon(
metaDataName: 'dev.imranr.obtainium.service.NOTIFICATION_ICON',
),
callback: startCallback,
);
}
}
// void onReceiveForegroundServiceData(Object data) {
// print('onReceiveTaskData: $data');
// }
@override
void dispose() {
// Remove a callback to receive data sent from the TaskHandler.
// FlutterForegroundTask.removeTaskDataCallback(onReceiveForegroundServiceData);
super.dispose();
}
@override
@@ -186,15 +244,14 @@ class _ObtainiumState extends State<Obtainium> {
AppsProvider appsProvider = context.read<AppsProvider>();
LogsProvider logs = context.read<LogsProvider>();
NotificationsProvider notifs = context.read<NotificationsProvider>();
startForegroundService();
if (settingsProvider.prefs == null) {
settingsProvider.initializeSettings();
} else {
bool isFirstRun = settingsProvider.checkAndFlipFirstRun();
if (isFirstRun) {
logs.add('This is the first ever run of Obtainium.');
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
Permission.notification.request();
// If this is the first run, add Obtainium to the Apps list
if (!fdroid) {
getInstalledInfo(obtainiumId)
.then((value) {
@@ -236,68 +293,71 @@ class _ObtainiumState extends State<Obtainium> {
notifs.checkLaunchByNotif();
});
return DynamicColorBuilder(
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
// Decide on a colour/brightness scheme based on OS and user settings
ColorScheme lightColorScheme;
ColorScheme darkColorScheme;
if (lightDynamic != null &&
darkDynamic != null &&
settingsProvider.useMaterialYou) {
lightColorScheme = lightDynamic.harmonized();
darkColorScheme = darkDynamic.harmonized();
} else {
lightColorScheme = ColorScheme.fromSeed(
seedColor: settingsProvider.themeColor,
return WithForegroundTask(
child: DynamicColorBuilder(
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
// Decide on a colour/brightness scheme based on OS and user settings
ColorScheme lightColorScheme;
ColorScheme darkColorScheme;
if (lightDynamic != null &&
darkDynamic != null &&
settingsProvider.useMaterialYou) {
lightColorScheme = lightDynamic.harmonized();
darkColorScheme = darkDynamic.harmonized();
} else {
lightColorScheme = ColorScheme.fromSeed(
seedColor: settingsProvider.themeColor,
);
darkColorScheme = ColorScheme.fromSeed(
seedColor: settingsProvider.themeColor,
brightness: Brightness.dark,
);
}
// set the background and surface colors to pure black in the amoled theme
if (settingsProvider.useBlackTheme) {
darkColorScheme = darkColorScheme
.copyWith(surface: Colors.black)
.harmonized();
}
if (settingsProvider.useSystemFont) NativeFeatures.loadSystemFont();
return MaterialApp(
title: 'Obtainium',
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
navigatorKey: globalNavigatorKey,
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: settingsProvider.theme == ThemeSettings.dark
? darkColorScheme
: lightColorScheme,
fontFamily: settingsProvider.useSystemFont
? 'SystemFont'
: 'Montserrat',
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: settingsProvider.theme == ThemeSettings.light
? lightColorScheme
: darkColorScheme,
fontFamily: settingsProvider.useSystemFont
? 'SystemFont'
: 'Montserrat',
),
home: Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.select):
const ActivateIntent(),
},
child: const HomePage(),
),
);
darkColorScheme = ColorScheme.fromSeed(
seedColor: settingsProvider.themeColor,
brightness: Brightness.dark,
);
}
// set the background and surface colors to pure black in the amoled theme
if (settingsProvider.useBlackTheme) {
darkColorScheme = darkColorScheme
.copyWith(surface: Colors.black)
.harmonized();
}
if (settingsProvider.useSystemFont) NativeFeatures.loadSystemFont();
return MaterialApp(
title: 'Obtainium',
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
navigatorKey: globalNavigatorKey,
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: settingsProvider.theme == ThemeSettings.dark
? darkColorScheme
: lightColorScheme,
fontFamily: settingsProvider.useSystemFont
? 'SystemFont'
: 'Montserrat',
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: settingsProvider.theme == ThemeSettings.light
? lightColorScheme
: darkColorScheme,
fontFamily: settingsProvider.useSystemFont
? 'SystemFont'
: 'Montserrat',
),
home: Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
},
child: const HomePage(),
),
);
},
},
),
);
}
}