Switched to WorkManager for reliability (#608)

This commit is contained in:
Imran Remtulla
2023-10-22 00:59:51 -04:00
parent b8c0e18bb4
commit d3247a9ec1
5 changed files with 280 additions and 263 deletions

View File

@@ -12,7 +12,7 @@ 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:android_alarm_manager_plus/android_alarm_manager_plus.dart';
import 'package:workmanager/workmanager.dart';
import 'package:easy_localization/easy_localization.dart';
// ignore: implementation_imports
import 'package:easy_localization/src/easy_localization_controller.dart';
@@ -23,7 +23,7 @@ const String currentVersion = '0.14.31';
const String currentReleaseTag =
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
const int bgUpdateCheckAlarmId = 666;
const String bgUpdateTaskId = 'bgUpdate';
List<MapEntry<Locale, String>> supportedLocales = const [
MapEntry(Locale('en'), 'English'),
@@ -71,6 +71,17 @@ Future<void> loadTranslations() async {
fallbackTranslations: controller.fallbackTranslations);
}
@pragma('vm:entry-point')
void bgTaskDispatcher() {
Workmanager().executeTask((taskId, params) {
if (taskId == bgUpdateTaskId) {
return bgUpdateTask(taskId, params);
} else {
return Future.value(true);
}
});
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
try {
@@ -88,7 +99,7 @@ void main() async {
);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
await AndroidAlarmManager.initialize();
await Workmanager().initialize(bgTaskDispatcher);
runApp(MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => AppsProvider()),
@@ -158,7 +169,7 @@ class _ObtainiumState extends State<Obtainium> {
var actualUpdateInterval = settingsProvider.updateInterval;
if (existingUpdateInterval != actualUpdateInterval) {
if (actualUpdateInterval == 0) {
AndroidAlarmManager.cancel(bgUpdateCheckAlarmId);
Workmanager().cancelByUniqueName(bgUpdateTaskId);
} else {
var settingChanged = existingUpdateInterval != -1;
var lastCheckWasTooLongAgo = actualUpdateInterval != 0 &&
@@ -168,12 +179,10 @@ class _ObtainiumState extends State<Obtainium> {
if (settingChanged || lastCheckWasTooLongAgo) {
logs.add(
'Update interval was set to ${actualUpdateInterval.toString()} (reason: ${settingChanged ? 'setting changed' : 'last check was ${settingsProvider.lastBGCheckTime.toLocal().toString()}'}).');
AndroidAlarmManager.periodic(
Duration(minutes: actualUpdateInterval),
bgUpdateCheckAlarmId,
bgUpdateCheck,
rescheduleOnReboot: true,
wakeup: true);
Workmanager().registerPeriodicTask(
bgUpdateTaskId, "BG Update Main Loop",
initialDelay: Duration(minutes: actualUpdateInterval),
existingWorkPolicy: ExistingWorkPolicy.replace);
}
}
existingUpdateInterval = actualUpdateInterval;

View File

@@ -1,4 +1,4 @@
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
import 'package:workmanager/workmanager.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -591,10 +591,10 @@ class _SettingsPageState extends State<SettingsPage> {
height16,
TextButton(
onPressed: () {
AndroidAlarmManager.oneShot(
const Duration(seconds: 0),
bgUpdateCheckAlarmId + 200,
bgUpdateCheck);
Workmanager().registerOneOffTask(
'$bgUpdateTaskId+Manual', bgUpdateTaskId,
existingWorkPolicy:
ExistingWorkPolicy.replace);
showMessage(tr('bgTaskStarted'), context);
},
child: Text(tr('runBgCheckNow')))

View File

@@ -7,7 +7,7 @@ import 'dart:io';
import 'dart:math';
import 'package:http/http.dart' as http;
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
import 'package:workmanager/workmanager.dart';
import 'package:android_intent_plus/flag.dart';
import 'package:android_package_installer/android_package_installer.dart';
import 'package:android_package_manager/android_package_manager.dart';
@@ -1356,14 +1356,14 @@ class _APKOriginWarningDialogState extends State<APKOriginWarningDialog> {
/// If there is an error, the offending app is moved to the back of the line of remaining apps, and the task is retried.
/// If an app repeatedly fails to install up to its retry limit, the user is notified.
///
@pragma('vm:entry-point')
Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
Future<bool> bgUpdateTask(String taskId, Map<String, dynamic>? params) async {
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
await AndroidAlarmManager.initialize();
await Workmanager().initialize(bgTaskDispatcher);
await loadTranslations();
LogsProvider logs = LogsProvider();
try {
NotificationsProvider notificationsProvider = NotificationsProvider();
AppsProvider appsProvider = AppsProvider(isBg: true);
await appsProvider.loadApps();
@@ -1375,10 +1375,10 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
appsProvider.settingsProvider.lastBGCheckTime = DateTime.now();
}
List<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[
...(params['toCheck']
?.map((entry) => MapEntry<String, int>(
entry['key'] as String, entry['value'] as int))
.toList() ??
...(params['toCheck']?.map((str) {
var temp = str.split(',');
return MapEntry<String, int>(temp[0], int.parse(temp[1]));
}).toList() ??
appsProvider
.getAppsSortedByUpdateCheckTime(
onlyCheckInstalledOrTrackOnlyApps: appsProvider
@@ -1386,10 +1386,10 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
.map((e) => MapEntry(e, 0)))
];
List<MapEntry<String, int>> toInstall = <MapEntry<String, int>>[
...(params['toInstall']
?.map((entry) => MapEntry<String, int>(
entry['key'] as String, entry['value'] as int))
.toList() ??
...(params['toInstall']?.map((str) {
var temp = str.split(',');
return MapEntry<String, int>(temp[0], int.parse(temp[1]));
}).toList() ??
(<List<MapEntry<String, int>>>[]))
];
@@ -1403,18 +1403,17 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
DateTime.now().add(Duration(minutes: networkBasedRetryInterval));
var shouldRetry = potentialNetworkRetryCheck.isBefore(nextRegularCheck);
logs.add(
'BG update task $taskId: No network. Will ${shouldRetry ? 'retry in $networkBasedRetryInterval minutes' : 'not retry'}.');
AndroidAlarmManager.oneShot(
const Duration(minutes: 15), taskId + 1, bgUpdateCheck,
params: {
'toCheck': toCheck
.map((entry) => {'key': entry.key, 'value': entry.value})
.toList(),
'BG task $taskId: No network. Will ${shouldRetry ? 'retry in $networkBasedRetryInterval minutes' : 'not retry'}.');
await Workmanager().registerOneOffTask("$taskId+Retry", taskId,
initialDelay: const Duration(minutes: 15),
existingWorkPolicy: ExistingWorkPolicy.replace,
inputData: {
'toCheck':
toCheck.map((entry) => '${entry.key},${entry.value}').toList(),
'toInstall': toInstall
.map((entry) => {'key': entry.key, 'value': entry.value})
.map((entry) => '${entry.key},${entry.value}')
.toList(),
});
return;
}
var networkRestricted = false;
@@ -1475,8 +1474,9 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
errors = e['errors'];
errors!.rawErrors.forEach((key, err) {
logs.add(
'BG update task $taskId: Got error on checking for $key \'${err.toString()}\'.');
var toCheckApp = toCheck.where((element) => element.key == key).first;
'BG task $taskId: Got error on checking for $key \'${err.toString()}\'.');
var toCheckApp =
toCheck.where((element) => element.key == key).first;
if (toCheckApp.value < maxAttempts) {
toRetry.add(MapEntry(toCheckApp.key, toCheckApp.value + 1));
// Next task interval is based on the error with the longest retry time
@@ -1494,7 +1494,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
});
} else {
// We don't expect to ever get here in any situation so no need to catch (but log it in case)
logs.add('Fatal error in BG update task: ${e.toString()}');
logs.add('Fatal error in BG task: ${e.toString()}');
rethrow;
}
} finally {
@@ -1505,7 +1505,8 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
for (var i = 0; i < updates.length; i++) {
if (networkRestricted ||
!(await appsProvider.canInstallSilently(updates[i]))) {
if (updates[i].additionalSettings['skipUpdateNotifications'] != true) {
if (updates[i].additionalSettings['skipUpdateNotifications'] !=
true) {
toNotify.add(updates[i]);
}
}
@@ -1527,28 +1528,29 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
// if there are update checks to retry, schedule a retry task
if (toRetry.isNotEmpty) {
logs.add(
'BG update task $taskId: Will retry in $retryAfterXSeconds seconds.');
AndroidAlarmManager.oneShot(
Duration(seconds: retryAfterXSeconds), taskId + 1, bgUpdateCheck,
params: {
logs.add('BG task $taskId: Will retry in $retryAfterXSeconds seconds.');
await Workmanager().registerOneOffTask("$taskId+Retry", taskId,
initialDelay: Duration(seconds: retryAfterXSeconds),
existingWorkPolicy: ExistingWorkPolicy.replace,
inputData: {
'toCheck': toRetry
.map((entry) => {'key': entry.key, 'value': entry.value})
.map((entry) => '${entry.key},${entry.value}')
.toList(),
'toInstall': toInstall
.map((entry) => {'key': entry.key, 'value': entry.value})
.map((entry) => '${entry.key},${entry.value}')
.toList(),
});
} else {
// If there are no more update checks, schedule an install task
logs.add(
'BG update task $taskId: Done. Scheduling install task to run immediately.');
AndroidAlarmManager.oneShot(
const Duration(minutes: 0), taskId + 1, bgUpdateCheck,
params: {
'toCheck': [],
'BG task $taskId: Done. Scheduling install task to run immediately.');
await Workmanager().registerOneOffTask(
"$bgUpdateTaskId+Install", taskId,
existingWorkPolicy: ExistingWorkPolicy.replace,
inputData: {
'toCheck': <String>[],
'toInstall': toInstall
.map((entry) => {'key': entry.key, 'value': entry.value})
.map((entry) => '${entry.key},${entry.value}')
.toList()
});
}
@@ -1577,7 +1579,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
var retryCount = toInstall[i].value;
try {
logs.add(
'BG install task $taskId: Attempting to update $appId in the background.');
'BG task $taskId: Attempting to update $appId in the background.');
await appsProvider.downloadAndInstallLatestApps([appId], null,
notificationsProvider: notificationsProvider);
await Future.delayed(const Duration(
@@ -1589,21 +1591,22 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
} catch (e) {
// If you got an error, move the offender to the back of the line (increment their fail count) and schedule another task to continue installing shortly
logs.add(
'BG install task $taskId: Got error on updating $appId \'${e.toString()}\'.');
'BG task $taskId: Got error on updating $appId \'${e.toString()}\'.');
if (retryCount < maxAttempts) {
var remainingSeconds = retryCount;
logs.add(
'BG install task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).');
'BG task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).');
var remainingToInstall = moveStrToEndMapEntryWithCount(
toInstall.sublist(i), MapEntry(appId, retryCount + 1));
AndroidAlarmManager.oneShot(
Duration(seconds: remainingSeconds), taskId + 1, bgUpdateCheck,
params: {
await Workmanager().registerOneOffTask("$taskId+Retry", taskId,
initialDelay: Duration(seconds: remainingSeconds),
existingWorkPolicy: ExistingWorkPolicy.replace,
inputData: {
'toCheck': toCheck
.map((entry) => {'key': entry.key, 'value': entry.value})
.map((entry) => '${entry.key},${entry.value}')
.toList(),
'toInstall': remainingToInstall
.map((entry) => {'key': entry.key, 'value': entry.value})
.map((entry) => '${entry.key},${entry.value}')
.toList(),
});
break;
@@ -1617,7 +1620,12 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
}
}
if (didCompleteInstalling || toInstall.isEmpty) {
logs.add('BG install task $taskId: Done.');
logs.add('BG task $taskId: Done.');
}
}
return true;
} catch (e) {
logs.add(e.toString());
return false;
}
}

View File

@@ -1,14 +1,6 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
android_alarm_manager_plus:
dependency: "direct main"
description:
name: android_alarm_manager_plus
sha256: "82fb28c867c4b3dd7e9157728e46426b8916362f977dbba46b949210f00099f4"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
android_intent_plus:
dependency: "direct main"
description:
@@ -931,6 +923,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
workmanager:
dependency: "direct main"
description:
name: workmanager
sha256: ed13530cccd28c5c9959ad42d657cd0666274ca74c56dea0ca183ddd527d3a00
url: "https://pub.dev"
source: hosted
version: "0.5.2"
xdg_directories:
dependency: transitive
description:
@@ -956,5 +956,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.1.0 <4.0.0"
dart: ">=3.1.2 <4.0.0"
flutter: ">=3.13.0"

View File

@@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.14.31+223 # When changing this, update the tag in main() accordingly
version: 0.14.31+224 # When changing this, update the tag in main() accordingly
environment:
sdk: '>=3.0.0 <4.0.0'
@@ -57,7 +57,6 @@ dependencies:
ref: main
android_package_manager: ^0.6.0
share_plus: ^7.0.0
android_alarm_manager_plus: ^3.0.0
sqflite: ^2.2.0+3
easy_localization: ^3.0.1
android_intent_plus: ^4.0.0
@@ -66,6 +65,7 @@ dependencies:
hsluv: ^1.1.3
connectivity_plus: ^5.0.0
shared_storage: ^0.8.0
workmanager: ^0.5.2
dev_dependencies:
flutter_test: