From d3247a9ec16e127004bb296e5b663a2978f3c2f0 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sun, 22 Oct 2023 00:59:51 -0400 Subject: [PATCH 1/7] Switched to WorkManager for reliability (#608) --- lib/main.dart | 29 +- lib/pages/settings.dart | 10 +- lib/providers/apps_provider.dart | 480 ++++++++++++++++--------------- pubspec.lock | 18 +- pubspec.yaml | 4 +- 5 files changed, 279 insertions(+), 262 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 1ca54447..69fd16bb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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> supportedLocales = const [ MapEntry(Locale('en'), 'English'), @@ -71,6 +71,17 @@ Future 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 { 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 { 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; diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index c36c9584..38f7db70 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -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 { 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'))) diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index d4085d6a..e2991902 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -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,268 +1356,276 @@ class _APKOriginWarningDialogState extends State { /// 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 bgUpdateCheck(int taskId, Map? params) async { +Future bgUpdateTask(String taskId, Map? params) async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); - await AndroidAlarmManager.initialize(); + await Workmanager().initialize(bgTaskDispatcher); await loadTranslations(); LogsProvider logs = LogsProvider(); - NotificationsProvider notificationsProvider = NotificationsProvider(); - AppsProvider appsProvider = AppsProvider(isBg: true); - await appsProvider.loadApps(); - - int maxAttempts = 4; - - params ??= {}; - if (params['toCheck'] == null) { - appsProvider.settingsProvider.lastBGCheckTime = DateTime.now(); - } - List> toCheck = >[ - ...(params['toCheck'] - ?.map((entry) => MapEntry( - entry['key'] as String, entry['value'] as int)) - .toList() ?? - appsProvider - .getAppsSortedByUpdateCheckTime( - onlyCheckInstalledOrTrackOnlyApps: appsProvider - .settingsProvider.onlyCheckInstalledOrTrackOnlyApps) - .map((e) => MapEntry(e, 0))) - ]; - List> toInstall = >[ - ...(params['toInstall'] - ?.map((entry) => MapEntry( - entry['key'] as String, entry['value'] as int)) - .toList() ?? - (>>[])) - ]; - - var netResult = await (Connectivity().checkConnectivity()); - - if (netResult == ConnectivityResult.none) { - var networkBasedRetryInterval = 15; - var nextRegularCheck = appsProvider.settingsProvider.lastBGCheckTime - .add(Duration(minutes: appsProvider.settingsProvider.updateInterval)); - var potentialNetworkRetryCheck = - 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(), - 'toInstall': toInstall - .map((entry) => {'key': entry.key, 'value': entry.value}) - .toList(), - }); - return; - } - - var networkRestricted = false; - if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) { - networkRestricted = (netResult != ConnectivityResult.wifi) && - (netResult != ConnectivityResult.ethernet); - } + try { + NotificationsProvider notificationsProvider = NotificationsProvider(); + AppsProvider appsProvider = AppsProvider(isBg: true); + await appsProvider.loadApps(); + + int maxAttempts = 4; + + params ??= {}; + if (params['toCheck'] == null) { + appsProvider.settingsProvider.lastBGCheckTime = DateTime.now(); + } + List> toCheck = >[ + ...(params['toCheck']?.map((str) { + var temp = str.split(','); + return MapEntry(temp[0], int.parse(temp[1])); + }).toList() ?? + appsProvider + .getAppsSortedByUpdateCheckTime( + onlyCheckInstalledOrTrackOnlyApps: appsProvider + .settingsProvider.onlyCheckInstalledOrTrackOnlyApps) + .map((e) => MapEntry(e, 0))) + ]; + List> toInstall = >[ + ...(params['toInstall']?.map((str) { + var temp = str.split(','); + return MapEntry(temp[0], int.parse(temp[1])); + }).toList() ?? + (>>[])) + ]; + + var netResult = await (Connectivity().checkConnectivity()); + + if (netResult == ConnectivityResult.none) { + var networkBasedRetryInterval = 15; + var nextRegularCheck = appsProvider.settingsProvider.lastBGCheckTime + .add(Duration(minutes: appsProvider.settingsProvider.updateInterval)); + var potentialNetworkRetryCheck = + DateTime.now().add(Duration(minutes: networkBasedRetryInterval)); + var shouldRetry = potentialNetworkRetryCheck.isBefore(nextRegularCheck); + logs.add( + '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) => '${entry.key},${entry.value}') + .toList(), + }); + } - bool installMode = - toCheck.isEmpty; // Task is either in update mode or install mode - - logs.add( - 'BG ${installMode ? 'install' : 'update'} task $taskId: Started (${installMode ? toInstall.length : toCheck.length}).'); - - if (!installMode) { - // If in update mode, we check for updates. - // We divide the results into 4 groups: - // - toNotify - Apps with updates that the user will be notified about (can't be silently installed) - // - toRetry - Apps with update check errors that will be retried in a while - // - toThrow - Apps with update check errors that the user will be notified about (no retry) - // After grouping the updates, we take care of toNotify and toThrow first - // Then if toRetry is not empty, we schedule another update task to run in a while - // If toRetry is empty, we take care of schedule another task that will run in install mode (toCheck is empty) - - // Init. vars. - List updates = []; // All updates found (silent and non-silent) - List toNotify = - []; // All non-silent updates that the user will be notified about - List> toRetry = - []; // All apps that got errors while checking - var retryAfterXSeconds = - 0; // How long to wait until the next attempt (if there are errors) - MultiAppMultiError? - errors; // All errors including those that will lead to a retry - MultiAppMultiError toThrow = - MultiAppMultiError(); // All errors that will not lead to a retry, just a notification - CheckingUpdatesNotification notif = CheckingUpdatesNotification( - plural('apps', toCheck.length)); // The notif. to show while checking - - // Set a bool for when we're no on wifi/wired and the user doesn't want to download apps in that state var networkRestricted = false; if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) { - var netResult = await (Connectivity().checkConnectivity()); networkRestricted = (netResult != ConnectivityResult.wifi) && (netResult != ConnectivityResult.ethernet); } - try { - // Check for updates - notificationsProvider.notify(notif, cancelExisting: true); - updates = await appsProvider.checkUpdates( - specificIds: toCheck.map((e) => e.key).toList(), - sp: appsProvider.settingsProvider); - } catch (e) { - // If there were errors, group them into toRetry and toThrow based on max retry count per app - if (e is Map) { - updates = e['updates']; - 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; - 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 - var minRetryIntervalForThisApp = err is RateLimitError - ? (err.remainingMinutes * 60) - : e is ClientException - ? (15 * 60) - : pow(toCheckApp.value + 1, 2).toInt(); - if (minRetryIntervalForThisApp > retryAfterXSeconds) { - retryAfterXSeconds = minRetryIntervalForThisApp; - } - } else { - toThrow.add(key, err, appName: errors?.appIdNames[key]); - } - }); - } 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()}'); - rethrow; + bool installMode = + toCheck.isEmpty; // Task is either in update mode or install mode + + logs.add( + 'BG ${installMode ? 'install' : 'update'} task $taskId: Started (${installMode ? toInstall.length : toCheck.length}).'); + + if (!installMode) { + // If in update mode, we check for updates. + // We divide the results into 4 groups: + // - toNotify - Apps with updates that the user will be notified about (can't be silently installed) + // - toRetry - Apps with update check errors that will be retried in a while + // - toThrow - Apps with update check errors that the user will be notified about (no retry) + // After grouping the updates, we take care of toNotify and toThrow first + // Then if toRetry is not empty, we schedule another update task to run in a while + // If toRetry is empty, we take care of schedule another task that will run in install mode (toCheck is empty) + + // Init. vars. + List updates = []; // All updates found (silent and non-silent) + List toNotify = + []; // All non-silent updates that the user will be notified about + List> toRetry = + []; // All apps that got errors while checking + var retryAfterXSeconds = + 0; // How long to wait until the next attempt (if there are errors) + MultiAppMultiError? + errors; // All errors including those that will lead to a retry + MultiAppMultiError toThrow = + MultiAppMultiError(); // All errors that will not lead to a retry, just a notification + CheckingUpdatesNotification notif = CheckingUpdatesNotification( + plural('apps', toCheck.length)); // The notif. to show while checking + + // Set a bool for when we're no on wifi/wired and the user doesn't want to download apps in that state + var networkRestricted = false; + if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) { + var netResult = await (Connectivity().checkConnectivity()); + networkRestricted = (netResult != ConnectivityResult.wifi) && + (netResult != ConnectivityResult.ethernet); } - } finally { - notificationsProvider.cancel(notif.id); - } - // Filter out updates that will be installed silently (the rest go into toNotify) - for (var i = 0; i < updates.length; i++) { - if (networkRestricted || - !(await appsProvider.canInstallSilently(updates[i]))) { - if (updates[i].additionalSettings['skipUpdateNotifications'] != true) { - toNotify.add(updates[i]); + try { + // Check for updates + notificationsProvider.notify(notif, cancelExisting: true); + updates = await appsProvider.checkUpdates( + specificIds: toCheck.map((e) => e.key).toList(), + sp: appsProvider.settingsProvider); + } catch (e) { + // If there were errors, group them into toRetry and toThrow based on max retry count per app + if (e is Map) { + updates = e['updates']; + errors = e['errors']; + errors!.rawErrors.forEach((key, err) { + logs.add( + '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 + var minRetryIntervalForThisApp = err is RateLimitError + ? (err.remainingMinutes * 60) + : e is ClientException + ? (15 * 60) + : pow(toCheckApp.value + 1, 2).toInt(); + if (minRetryIntervalForThisApp > retryAfterXSeconds) { + retryAfterXSeconds = minRetryIntervalForThisApp; + } + } else { + toThrow.add(key, err, appName: errors?.appIdNames[key]); + } + }); + } 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 task: ${e.toString()}'); + rethrow; } + } finally { + notificationsProvider.cancel(notif.id); } - } - // Send the update notification - if (toNotify.isNotEmpty) { - notificationsProvider.notify(UpdateNotification(toNotify)); - } + // Filter out updates that will be installed silently (the rest go into toNotify) + for (var i = 0; i < updates.length; i++) { + if (networkRestricted || + !(await appsProvider.canInstallSilently(updates[i]))) { + if (updates[i].additionalSettings['skipUpdateNotifications'] != + true) { + toNotify.add(updates[i]); + } + } + } - // Send the error notifications (grouped by error string) - if (toThrow.rawErrors.isNotEmpty) { - for (var element in toThrow.idsByErrorString.entries) { - notificationsProvider.notify(ErrorCheckingUpdatesNotification( - errors!.errorsAppsString(element.key, element.value), - id: Random().nextInt(10000))); + // Send the update notification + if (toNotify.isNotEmpty) { + notificationsProvider.notify(UpdateNotification(toNotify)); } - } - // 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: { - 'toCheck': toRetry - .map((entry) => {'key': entry.key, 'value': entry.value}) - .toList(), - 'toInstall': toInstall - .map((entry) => {'key': entry.key, 'value': 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': [], - 'toInstall': toInstall - .map((entry) => {'key': entry.key, 'value': entry.value}) - .toList() - }); - } - } else { - // In install mode... - // If you haven't explicitly been given updates to install (which is the case for new tasks), grab all available silent updates - if (toInstall.isEmpty && !networkRestricted) { - var temp = appsProvider.findExistingUpdates(installedOnly: true); - for (var i = 0; i < temp.length; i++) { - if (await appsProvider - .canInstallSilently(appsProvider.apps[temp[i]]!.app)) { - toInstall.add(MapEntry(temp[i], 0)); + // Send the error notifications (grouped by error string) + if (toThrow.rawErrors.isNotEmpty) { + for (var element in toThrow.idsByErrorString.entries) { + notificationsProvider.notify(ErrorCheckingUpdatesNotification( + errors!.errorsAppsString(element.key, element.value), + id: Random().nextInt(10000))); } } - } - var didCompleteInstalling = false; - var tempObtArr = toInstall.where((element) => element.key == obtainiumId); - if (tempObtArr.isNotEmpty) { - // Move obtainium to the end of the list as it must always install last - var obt = tempObtArr.first; - toInstall = moveStrToEndMapEntryWithCount(toInstall, obt); - } - // Loop through all updates and install each - for (var i = 0; i < toInstall.length; i++) { - var appId = toInstall[i].key; - var retryCount = toInstall[i].value; - try { + + // if there are update checks to retry, schedule a retry task + if (toRetry.isNotEmpty) { + 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) => '${entry.key},${entry.value}') + .toList(), + 'toInstall': toInstall + .map((entry) => '${entry.key},${entry.value}') + .toList(), + }); + } else { + // If there are no more update checks, schedule an install task logs.add( - 'BG install task $taskId: Attempting to update $appId in the background.'); - await appsProvider.downloadAndInstallLatestApps([appId], null, - notificationsProvider: notificationsProvider); - await Future.delayed(const Duration( - seconds: - 5)); // Just in case task ending causes install fail (not clear) - if (i == (toCheck.length - 1)) { - didCompleteInstalling = true; + 'BG task $taskId: Done. Scheduling install task to run immediately.'); + await Workmanager().registerOneOffTask( + "$bgUpdateTaskId+Install", taskId, + existingWorkPolicy: ExistingWorkPolicy.replace, + inputData: { + 'toCheck': [], + 'toInstall': toInstall + .map((entry) => '${entry.key},${entry.value}') + .toList() + }); + } + } else { + // In install mode... + // If you haven't explicitly been given updates to install (which is the case for new tasks), grab all available silent updates + if (toInstall.isEmpty && !networkRestricted) { + var temp = appsProvider.findExistingUpdates(installedOnly: true); + for (var i = 0; i < temp.length; i++) { + if (await appsProvider + .canInstallSilently(appsProvider.apps[temp[i]]!.app)) { + toInstall.add(MapEntry(temp[i], 0)); + } } - } 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()}\'.'); - if (retryCount < maxAttempts) { - var remainingSeconds = retryCount; + } + var didCompleteInstalling = false; + var tempObtArr = toInstall.where((element) => element.key == obtainiumId); + if (tempObtArr.isNotEmpty) { + // Move obtainium to the end of the list as it must always install last + var obt = tempObtArr.first; + toInstall = moveStrToEndMapEntryWithCount(toInstall, obt); + } + // Loop through all updates and install each + for (var i = 0; i < toInstall.length; i++) { + var appId = toInstall[i].key; + var retryCount = toInstall[i].value; + try { logs.add( - 'BG install 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: { - 'toCheck': toCheck - .map((entry) => {'key': entry.key, 'value': entry.value}) - .toList(), - 'toInstall': remainingToInstall - .map((entry) => {'key': entry.key, 'value': entry.value}) - .toList(), - }); - break; - } else { - // If the offender has reached its fail limit, notify the user and remove it from the list (task can continue) - toInstall.removeAt(i); - i--; - notificationsProvider - .notify(ErrorCheckingUpdatesNotification(e.toString())); + 'BG task $taskId: Attempting to update $appId in the background.'); + await appsProvider.downloadAndInstallLatestApps([appId], null, + notificationsProvider: notificationsProvider); + await Future.delayed(const Duration( + seconds: + 5)); // Just in case task ending causes install fail (not clear) + if (i == (toCheck.length - 1)) { + didCompleteInstalling = true; + } + } 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 task $taskId: Got error on updating $appId \'${e.toString()}\'.'); + if (retryCount < maxAttempts) { + var remainingSeconds = retryCount; + logs.add( + '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)); + await Workmanager().registerOneOffTask("$taskId+Retry", taskId, + initialDelay: Duration(seconds: remainingSeconds), + existingWorkPolicy: ExistingWorkPolicy.replace, + inputData: { + 'toCheck': toCheck + .map((entry) => '${entry.key},${entry.value}') + .toList(), + 'toInstall': remainingToInstall + .map((entry) => '${entry.key},${entry.value}') + .toList(), + }); + break; + } else { + // If the offender has reached its fail limit, notify the user and remove it from the list (task can continue) + toInstall.removeAt(i); + i--; + notificationsProvider + .notify(ErrorCheckingUpdatesNotification(e.toString())); + } } } + if (didCompleteInstalling || toInstall.isEmpty) { + logs.add('BG task $taskId: Done.'); + } } - if (didCompleteInstalling || toInstall.isEmpty) { - logs.add('BG install task $taskId: Done.'); - } + return true; + } catch (e) { + logs.add(e.toString()); + return false; } } diff --git a/pubspec.lock b/pubspec.lock index 52d0c323..4cda9851 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index 9f617289..a3bcc91f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: From a34a447164ba09cce69535fa91edc1568567c9b0 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sun, 22 Oct 2023 12:50:54 -0400 Subject: [PATCH 2/7] Revert "Switched to WorkManager for reliability (#608)" This reverts commit d3247a9ec16e127004bb296e5b663a2978f3c2f0. --- lib/main.dart | 29 +- lib/pages/settings.dart | 10 +- lib/providers/apps_provider.dart | 480 +++++++++++++++---------------- pubspec.lock | 18 +- pubspec.yaml | 4 +- 5 files changed, 262 insertions(+), 279 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 69fd16bb..1ca54447 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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:workmanager/workmanager.dart'; +import 'package:android_alarm_manager_plus/android_alarm_manager_plus.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 String bgUpdateTaskId = 'bgUpdate'; +const int bgUpdateCheckAlarmId = 666; List> supportedLocales = const [ MapEntry(Locale('en'), 'English'), @@ -71,17 +71,6 @@ Future 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 { @@ -99,7 +88,7 @@ void main() async { ); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); } - await Workmanager().initialize(bgTaskDispatcher); + await AndroidAlarmManager.initialize(); runApp(MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => AppsProvider()), @@ -169,7 +158,7 @@ class _ObtainiumState extends State { var actualUpdateInterval = settingsProvider.updateInterval; if (existingUpdateInterval != actualUpdateInterval) { if (actualUpdateInterval == 0) { - Workmanager().cancelByUniqueName(bgUpdateTaskId); + AndroidAlarmManager.cancel(bgUpdateCheckAlarmId); } else { var settingChanged = existingUpdateInterval != -1; var lastCheckWasTooLongAgo = actualUpdateInterval != 0 && @@ -179,10 +168,12 @@ class _ObtainiumState extends State { if (settingChanged || lastCheckWasTooLongAgo) { logs.add( 'Update interval was set to ${actualUpdateInterval.toString()} (reason: ${settingChanged ? 'setting changed' : 'last check was ${settingsProvider.lastBGCheckTime.toLocal().toString()}'}).'); - Workmanager().registerPeriodicTask( - bgUpdateTaskId, "BG Update Main Loop", - initialDelay: Duration(minutes: actualUpdateInterval), - existingWorkPolicy: ExistingWorkPolicy.replace); + AndroidAlarmManager.periodic( + Duration(minutes: actualUpdateInterval), + bgUpdateCheckAlarmId, + bgUpdateCheck, + rescheduleOnReboot: true, + wakeup: true); } } existingUpdateInterval = actualUpdateInterval; diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 38f7db70..c36c9584 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -1,4 +1,4 @@ -import 'package:workmanager/workmanager.dart'; +import 'package:android_alarm_manager_plus/android_alarm_manager_plus.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 { height16, TextButton( onPressed: () { - Workmanager().registerOneOffTask( - '$bgUpdateTaskId+Manual', bgUpdateTaskId, - existingWorkPolicy: - ExistingWorkPolicy.replace); + AndroidAlarmManager.oneShot( + const Duration(seconds: 0), + bgUpdateCheckAlarmId + 200, + bgUpdateCheck); showMessage(tr('bgTaskStarted'), context); }, child: Text(tr('runBgCheckNow'))) diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index e2991902..d4085d6a 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -7,7 +7,7 @@ import 'dart:io'; import 'dart:math'; import 'package:http/http.dart' as http; -import 'package:workmanager/workmanager.dart'; +import 'package:android_alarm_manager_plus/android_alarm_manager_plus.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,276 +1356,268 @@ class _APKOriginWarningDialogState extends State { /// 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. /// -Future bgUpdateTask(String taskId, Map? params) async { +@pragma('vm:entry-point') +Future bgUpdateCheck(int taskId, Map? params) async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); - await Workmanager().initialize(bgTaskDispatcher); + await AndroidAlarmManager.initialize(); await loadTranslations(); LogsProvider logs = LogsProvider(); - try { - NotificationsProvider notificationsProvider = NotificationsProvider(); - AppsProvider appsProvider = AppsProvider(isBg: true); - await appsProvider.loadApps(); - - int maxAttempts = 4; - - params ??= {}; - if (params['toCheck'] == null) { - appsProvider.settingsProvider.lastBGCheckTime = DateTime.now(); - } - List> toCheck = >[ - ...(params['toCheck']?.map((str) { - var temp = str.split(','); - return MapEntry(temp[0], int.parse(temp[1])); - }).toList() ?? - appsProvider - .getAppsSortedByUpdateCheckTime( - onlyCheckInstalledOrTrackOnlyApps: appsProvider - .settingsProvider.onlyCheckInstalledOrTrackOnlyApps) - .map((e) => MapEntry(e, 0))) - ]; - List> toInstall = >[ - ...(params['toInstall']?.map((str) { - var temp = str.split(','); - return MapEntry(temp[0], int.parse(temp[1])); - }).toList() ?? - (>>[])) - ]; - - var netResult = await (Connectivity().checkConnectivity()); - - if (netResult == ConnectivityResult.none) { - var networkBasedRetryInterval = 15; - var nextRegularCheck = appsProvider.settingsProvider.lastBGCheckTime - .add(Duration(minutes: appsProvider.settingsProvider.updateInterval)); - var potentialNetworkRetryCheck = - DateTime.now().add(Duration(minutes: networkBasedRetryInterval)); - var shouldRetry = potentialNetworkRetryCheck.isBefore(nextRegularCheck); - logs.add( - '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) => '${entry.key},${entry.value}') - .toList(), - }); - } + NotificationsProvider notificationsProvider = NotificationsProvider(); + AppsProvider appsProvider = AppsProvider(isBg: true); + await appsProvider.loadApps(); + + int maxAttempts = 4; + + params ??= {}; + if (params['toCheck'] == null) { + appsProvider.settingsProvider.lastBGCheckTime = DateTime.now(); + } + List> toCheck = >[ + ...(params['toCheck'] + ?.map((entry) => MapEntry( + entry['key'] as String, entry['value'] as int)) + .toList() ?? + appsProvider + .getAppsSortedByUpdateCheckTime( + onlyCheckInstalledOrTrackOnlyApps: appsProvider + .settingsProvider.onlyCheckInstalledOrTrackOnlyApps) + .map((e) => MapEntry(e, 0))) + ]; + List> toInstall = >[ + ...(params['toInstall'] + ?.map((entry) => MapEntry( + entry['key'] as String, entry['value'] as int)) + .toList() ?? + (>>[])) + ]; + var netResult = await (Connectivity().checkConnectivity()); + + if (netResult == ConnectivityResult.none) { + var networkBasedRetryInterval = 15; + var nextRegularCheck = appsProvider.settingsProvider.lastBGCheckTime + .add(Duration(minutes: appsProvider.settingsProvider.updateInterval)); + var potentialNetworkRetryCheck = + 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(), + 'toInstall': toInstall + .map((entry) => {'key': entry.key, 'value': entry.value}) + .toList(), + }); + return; + } + + var networkRestricted = false; + if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) { + networkRestricted = (netResult != ConnectivityResult.wifi) && + (netResult != ConnectivityResult.ethernet); + } + + bool installMode = + toCheck.isEmpty; // Task is either in update mode or install mode + + logs.add( + 'BG ${installMode ? 'install' : 'update'} task $taskId: Started (${installMode ? toInstall.length : toCheck.length}).'); + + if (!installMode) { + // If in update mode, we check for updates. + // We divide the results into 4 groups: + // - toNotify - Apps with updates that the user will be notified about (can't be silently installed) + // - toRetry - Apps with update check errors that will be retried in a while + // - toThrow - Apps with update check errors that the user will be notified about (no retry) + // After grouping the updates, we take care of toNotify and toThrow first + // Then if toRetry is not empty, we schedule another update task to run in a while + // If toRetry is empty, we take care of schedule another task that will run in install mode (toCheck is empty) + + // Init. vars. + List updates = []; // All updates found (silent and non-silent) + List toNotify = + []; // All non-silent updates that the user will be notified about + List> toRetry = + []; // All apps that got errors while checking + var retryAfterXSeconds = + 0; // How long to wait until the next attempt (if there are errors) + MultiAppMultiError? + errors; // All errors including those that will lead to a retry + MultiAppMultiError toThrow = + MultiAppMultiError(); // All errors that will not lead to a retry, just a notification + CheckingUpdatesNotification notif = CheckingUpdatesNotification( + plural('apps', toCheck.length)); // The notif. to show while checking + + // Set a bool for when we're no on wifi/wired and the user doesn't want to download apps in that state var networkRestricted = false; if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) { + var netResult = await (Connectivity().checkConnectivity()); networkRestricted = (netResult != ConnectivityResult.wifi) && (netResult != ConnectivityResult.ethernet); } - bool installMode = - toCheck.isEmpty; // Task is either in update mode or install mode - - logs.add( - 'BG ${installMode ? 'install' : 'update'} task $taskId: Started (${installMode ? toInstall.length : toCheck.length}).'); - - if (!installMode) { - // If in update mode, we check for updates. - // We divide the results into 4 groups: - // - toNotify - Apps with updates that the user will be notified about (can't be silently installed) - // - toRetry - Apps with update check errors that will be retried in a while - // - toThrow - Apps with update check errors that the user will be notified about (no retry) - // After grouping the updates, we take care of toNotify and toThrow first - // Then if toRetry is not empty, we schedule another update task to run in a while - // If toRetry is empty, we take care of schedule another task that will run in install mode (toCheck is empty) - - // Init. vars. - List updates = []; // All updates found (silent and non-silent) - List toNotify = - []; // All non-silent updates that the user will be notified about - List> toRetry = - []; // All apps that got errors while checking - var retryAfterXSeconds = - 0; // How long to wait until the next attempt (if there are errors) - MultiAppMultiError? - errors; // All errors including those that will lead to a retry - MultiAppMultiError toThrow = - MultiAppMultiError(); // All errors that will not lead to a retry, just a notification - CheckingUpdatesNotification notif = CheckingUpdatesNotification( - plural('apps', toCheck.length)); // The notif. to show while checking - - // Set a bool for when we're no on wifi/wired and the user doesn't want to download apps in that state - var networkRestricted = false; - if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) { - var netResult = await (Connectivity().checkConnectivity()); - networkRestricted = (netResult != ConnectivityResult.wifi) && - (netResult != ConnectivityResult.ethernet); - } - - try { - // Check for updates - notificationsProvider.notify(notif, cancelExisting: true); - updates = await appsProvider.checkUpdates( - specificIds: toCheck.map((e) => e.key).toList(), - sp: appsProvider.settingsProvider); - } catch (e) { - // If there were errors, group them into toRetry and toThrow based on max retry count per app - if (e is Map) { - updates = e['updates']; - errors = e['errors']; - errors!.rawErrors.forEach((key, err) { - logs.add( - '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 - var minRetryIntervalForThisApp = err is RateLimitError - ? (err.remainingMinutes * 60) - : e is ClientException - ? (15 * 60) - : pow(toCheckApp.value + 1, 2).toInt(); - if (minRetryIntervalForThisApp > retryAfterXSeconds) { - retryAfterXSeconds = minRetryIntervalForThisApp; - } - } else { - toThrow.add(key, err, appName: errors?.appIdNames[key]); + try { + // Check for updates + notificationsProvider.notify(notif, cancelExisting: true); + updates = await appsProvider.checkUpdates( + specificIds: toCheck.map((e) => e.key).toList(), + sp: appsProvider.settingsProvider); + } catch (e) { + // If there were errors, group them into toRetry and toThrow based on max retry count per app + if (e is Map) { + updates = e['updates']; + 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; + 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 + var minRetryIntervalForThisApp = err is RateLimitError + ? (err.remainingMinutes * 60) + : e is ClientException + ? (15 * 60) + : pow(toCheckApp.value + 1, 2).toInt(); + if (minRetryIntervalForThisApp > retryAfterXSeconds) { + retryAfterXSeconds = minRetryIntervalForThisApp; } - }); - } 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 task: ${e.toString()}'); - rethrow; - } - } finally { - notificationsProvider.cancel(notif.id); + } else { + toThrow.add(key, err, appName: errors?.appIdNames[key]); + } + }); + } 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()}'); + rethrow; } + } finally { + notificationsProvider.cancel(notif.id); + } - // Filter out updates that will be installed silently (the rest go into toNotify) - for (var i = 0; i < updates.length; i++) { - if (networkRestricted || - !(await appsProvider.canInstallSilently(updates[i]))) { - if (updates[i].additionalSettings['skipUpdateNotifications'] != - true) { - toNotify.add(updates[i]); - } + // Filter out updates that will be installed silently (the rest go into toNotify) + for (var i = 0; i < updates.length; i++) { + if (networkRestricted || + !(await appsProvider.canInstallSilently(updates[i]))) { + if (updates[i].additionalSettings['skipUpdateNotifications'] != true) { + toNotify.add(updates[i]); } } + } - // Send the update notification - if (toNotify.isNotEmpty) { - notificationsProvider.notify(UpdateNotification(toNotify)); - } + // Send the update notification + if (toNotify.isNotEmpty) { + notificationsProvider.notify(UpdateNotification(toNotify)); + } - // Send the error notifications (grouped by error string) - if (toThrow.rawErrors.isNotEmpty) { - for (var element in toThrow.idsByErrorString.entries) { - notificationsProvider.notify(ErrorCheckingUpdatesNotification( - errors!.errorsAppsString(element.key, element.value), - id: Random().nextInt(10000))); - } + // Send the error notifications (grouped by error string) + if (toThrow.rawErrors.isNotEmpty) { + for (var element in toThrow.idsByErrorString.entries) { + notificationsProvider.notify(ErrorCheckingUpdatesNotification( + errors!.errorsAppsString(element.key, element.value), + id: Random().nextInt(10000))); } + } - // if there are update checks to retry, schedule a retry task - if (toRetry.isNotEmpty) { - 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) => '${entry.key},${entry.value}') - .toList(), - 'toInstall': toInstall - .map((entry) => '${entry.key},${entry.value}') - .toList(), - }); - } else { - // If there are no more update checks, schedule an install task - logs.add( - 'BG task $taskId: Done. Scheduling install task to run immediately.'); - await Workmanager().registerOneOffTask( - "$bgUpdateTaskId+Install", taskId, - existingWorkPolicy: ExistingWorkPolicy.replace, - inputData: { - 'toCheck': [], - 'toInstall': toInstall - .map((entry) => '${entry.key},${entry.value}') - .toList() - }); - } + // 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: { + 'toCheck': toRetry + .map((entry) => {'key': entry.key, 'value': entry.value}) + .toList(), + 'toInstall': toInstall + .map((entry) => {'key': entry.key, 'value': entry.value}) + .toList(), + }); } else { - // In install mode... - // If you haven't explicitly been given updates to install (which is the case for new tasks), grab all available silent updates - if (toInstall.isEmpty && !networkRestricted) { - var temp = appsProvider.findExistingUpdates(installedOnly: true); - for (var i = 0; i < temp.length; i++) { - if (await appsProvider - .canInstallSilently(appsProvider.apps[temp[i]]!.app)) { - toInstall.add(MapEntry(temp[i], 0)); - } + // 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': [], + 'toInstall': toInstall + .map((entry) => {'key': entry.key, 'value': entry.value}) + .toList() + }); + } + } else { + // In install mode... + // If you haven't explicitly been given updates to install (which is the case for new tasks), grab all available silent updates + if (toInstall.isEmpty && !networkRestricted) { + var temp = appsProvider.findExistingUpdates(installedOnly: true); + for (var i = 0; i < temp.length; i++) { + if (await appsProvider + .canInstallSilently(appsProvider.apps[temp[i]]!.app)) { + toInstall.add(MapEntry(temp[i], 0)); } } - var didCompleteInstalling = false; - var tempObtArr = toInstall.where((element) => element.key == obtainiumId); - if (tempObtArr.isNotEmpty) { - // Move obtainium to the end of the list as it must always install last - var obt = tempObtArr.first; - toInstall = moveStrToEndMapEntryWithCount(toInstall, obt); - } - // Loop through all updates and install each - for (var i = 0; i < toInstall.length; i++) { - var appId = toInstall[i].key; - var retryCount = toInstall[i].value; - try { - logs.add( - 'BG task $taskId: Attempting to update $appId in the background.'); - await appsProvider.downloadAndInstallLatestApps([appId], null, - notificationsProvider: notificationsProvider); - await Future.delayed(const Duration( - seconds: - 5)); // Just in case task ending causes install fail (not clear) - if (i == (toCheck.length - 1)) { - didCompleteInstalling = true; - } - } 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 + } + var didCompleteInstalling = false; + var tempObtArr = toInstall.where((element) => element.key == obtainiumId); + if (tempObtArr.isNotEmpty) { + // Move obtainium to the end of the list as it must always install last + var obt = tempObtArr.first; + toInstall = moveStrToEndMapEntryWithCount(toInstall, obt); + } + // Loop through all updates and install each + for (var i = 0; i < toInstall.length; i++) { + var appId = toInstall[i].key; + var retryCount = toInstall[i].value; + try { + logs.add( + 'BG install task $taskId: Attempting to update $appId in the background.'); + await appsProvider.downloadAndInstallLatestApps([appId], null, + notificationsProvider: notificationsProvider); + await Future.delayed(const Duration( + seconds: + 5)); // Just in case task ending causes install fail (not clear) + if (i == (toCheck.length - 1)) { + didCompleteInstalling = true; + } + } 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()}\'.'); + if (retryCount < maxAttempts) { + var remainingSeconds = retryCount; logs.add( - 'BG task $taskId: Got error on updating $appId \'${e.toString()}\'.'); - if (retryCount < maxAttempts) { - var remainingSeconds = retryCount; - logs.add( - '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)); - await Workmanager().registerOneOffTask("$taskId+Retry", taskId, - initialDelay: Duration(seconds: remainingSeconds), - existingWorkPolicy: ExistingWorkPolicy.replace, - inputData: { - 'toCheck': toCheck - .map((entry) => '${entry.key},${entry.value}') - .toList(), - 'toInstall': remainingToInstall - .map((entry) => '${entry.key},${entry.value}') - .toList(), - }); - break; - } else { - // If the offender has reached its fail limit, notify the user and remove it from the list (task can continue) - toInstall.removeAt(i); - i--; - notificationsProvider - .notify(ErrorCheckingUpdatesNotification(e.toString())); - } + 'BG install 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: { + 'toCheck': toCheck + .map((entry) => {'key': entry.key, 'value': entry.value}) + .toList(), + 'toInstall': remainingToInstall + .map((entry) => {'key': entry.key, 'value': entry.value}) + .toList(), + }); + break; + } else { + // If the offender has reached its fail limit, notify the user and remove it from the list (task can continue) + toInstall.removeAt(i); + i--; + notificationsProvider + .notify(ErrorCheckingUpdatesNotification(e.toString())); } } - if (didCompleteInstalling || toInstall.isEmpty) { - logs.add('BG task $taskId: Done.'); - } } - return true; - } catch (e) { - logs.add(e.toString()); - return false; + if (didCompleteInstalling || toInstall.isEmpty) { + logs.add('BG install task $taskId: Done.'); + } } } diff --git a/pubspec.lock b/pubspec.lock index 4cda9851..52d0c323 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # 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: @@ -923,14 +931,6 @@ 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.2 <4.0.0" + dart: ">=3.1.0 <4.0.0" flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index a3bcc91f..9f617289 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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+224 # When changing this, update the tag in main() accordingly +version: 0.14.31+223 # When changing this, update the tag in main() accordingly environment: sdk: '>=3.0.0 <4.0.0' @@ -57,6 +57,7 @@ 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 @@ -65,7 +66,6 @@ dependencies: hsluv: ^1.1.3 connectivity_plus: ^5.0.0 shared_storage: ^0.8.0 - workmanager: ^0.5.2 dev_dependencies: flutter_test: From 6d416f45a96077dac8f00f68730179a5e206e088 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sun, 22 Oct 2023 12:51:30 -0400 Subject: [PATCH 3/7] Higher versionCode after revert --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 9f617289..5635a7dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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+225 # When changing this, update the tag in main() accordingly environment: sdk: '>=3.0.0 <4.0.0' From 26971aa109fa93d80d95ba2b3624edda8a43e880 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Wed, 25 Oct 2023 18:22:58 -0400 Subject: [PATCH 4/7] Upgrade packages --- pubspec.lock | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 52d0c323..1ad4fe53 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -328,10 +328,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c + sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.0.17" flutter_test: dependency: "direct dev" description: flutter @@ -498,10 +498,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" path_provider_foundation: dependency: transitive description: @@ -807,58 +807,58 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 + sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" + sha256: "4ac97281cf60e2e8c5cc703b2b28528f9b50c8f7cebc71df6bdf0845f647268a" url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.2.0" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e + sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.1.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.1.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" + sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" + sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2" url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.2.0" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" + sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.1.0" uuid: dependency: transitive description: @@ -911,10 +911,10 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: "30b9af6bdd457b44c08748b9190d23208b5165357cc2eb57914fee1366c42974" + sha256: b4b42295b3aa91ed22ba6d3dd7de56efbb8f3ab3d6e41d8b1615d13537c7801d url: "https://pub.dev" source: hosted - version: "3.9.1" + version: "3.9.2" win32: dependency: transitive description: From 5b142b44016fa14f1aa96b41585060889b13a4dd Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Fri, 3 Nov 2023 19:35:42 -0400 Subject: [PATCH 5/7] Various fixes and improvements (#454, #1026, #1050, #1051, #1052, #1060) --- lib/app_sources/fdroid.dart | 2 +- lib/app_sources/fdroidrepo.dart | 3 ++- lib/app_sources/html.dart | 8 ++++++++ lib/app_sources/uptodown.dart | 8 ++++---- lib/pages/app.dart | 27 +++++++++++++++------------ lib/providers/source_provider.dart | 5 +---- 6 files changed, 31 insertions(+), 22 deletions(-) diff --git a/lib/app_sources/fdroid.dart b/lib/app_sources/fdroid.dart index c7b2ddaa..c071c02f 100644 --- a/lib/app_sources/fdroid.dart +++ b/lib/app_sources/fdroid.dart @@ -181,7 +181,7 @@ APKDetails getAPKUrlsFromFDroidPackagesAPIResponse( List apkUrls = releaseChoices .map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk') .toList(); - return APKDetails(version, getApkUrlsFromUrls(apkUrls), + return APKDetails(version, getApkUrlsFromUrls(apkUrls.toSet().toList()), AppNames(sourceName, Uri.parse(standardUrl).pathSegments.last)); } else { throw getObtainiumHttpError(res); diff --git a/lib/app_sources/fdroidrepo.dart b/lib/app_sources/fdroidrepo.dart index e61d544d..b0d74d80 100644 --- a/lib/app_sources/fdroidrepo.dart +++ b/lib/app_sources/fdroidrepo.dart @@ -108,7 +108,8 @@ class FDroidRepo extends AppSource { if (appIdOrName == null) { throw NoReleasesError(); } - var res = await sourceRequest('$standardUrl/index.xml'); + var res = await sourceRequest( + '$standardUrl${standardUrl.endsWith('/index.xml') ? '' : '/index.xml'}'); if (res.statusCode == 200) { var body = parse(res.body); var foundApps = body.querySelectorAll('application').where((element) { diff --git a/lib/app_sources/html.dart b/lib/app_sources/html.dart index a2dacdbf..13dcdd86 100644 --- a/lib/app_sources/html.dart +++ b/lib/app_sources/html.dart @@ -170,7 +170,15 @@ class HTML extends AppSource { List allLinks = html .querySelectorAll('a') .map((element) => element.attributes['href'] ?? '') + .where((element) => element.isNotEmpty) .toList(); + if (allLinks.isEmpty) { + allLinks = RegExp( + r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?') + .allMatches(res.body) + .map((match) => match.group(0)!) + .toList(); + } List links = []; if ((additionalSettings['intermediateLinkRegex'] as String?) ?.isNotEmpty == diff --git a/lib/app_sources/uptodown.dart b/lib/app_sources/uptodown.dart index c764b7c0..7a0b41df 100644 --- a/lib/app_sources/uptodown.dart +++ b/lib/app_sources/uptodown.dart @@ -89,11 +89,11 @@ class Uptodown extends AppSource { throw getObtainiumHttpError(res); } var html = parse(res.body); - var finalUrl = - (html.querySelector('.post-download')?.attributes['data-url']); - if (finalUrl == null) { + var finalUrlKey = + html.querySelector('.post-download')?.attributes['data-url']; + if (finalUrlKey == null) { throw NoAPKError(); } - return finalUrl; + return 'https://dw.$host/dwn/$finalUrlKey'; } } diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 7622a969..14e33d43 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -155,10 +155,13 @@ class _AppPageState extends State { const SizedBox(height: 20), app?.icon != null ? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - Image.memory( - app!.icon!, - height: 150, - gaplessPlayback: true, + GestureDetector( + child: Image.memory( + app!.icon!, + height: 150, + gaplessPlayback: true, + ), + onTap: () => pm.openApp(app.app.id), ) ]) : Container(), @@ -463,15 +466,15 @@ class _AppPageState extends State { : null)) ], )); - + appScreenAppBar() => AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - Navigator.pop(context); - }, - ), - ); + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); + }, + ), + ); return Scaffold( appBar: settingsProvider.showAppWebpage ? AppBar() : appScreenAppBar(), diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index e5ea114e..57360270 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -283,9 +283,6 @@ preStandardizeUrl(String url) { url.toLowerCase().indexOf('https://') != 0) { url = 'https://$url'; } - if (url.toLowerCase().indexOf('https://www.') == 0) { - url = 'https://${url.substring(12)}'; - } url = url .split('/') .where((e) => e.isNotEmpty) @@ -599,7 +596,7 @@ class SourceProvider { AppSource? source; for (var s in sources.where((element) => element.host != null)) { if (RegExp( - '://${s.allowSubDomains ? '([^\\.]+\\.)*' : ''}${s.host}(/|\\z)?') + '://(${s.allowSubDomains ? '([^\\.]+\\.)*' : ''}|www\\.)${s.host}(/|\\z)?') .hasMatch(url)) { source = s; break; From 168c1cf1ceb8092d6c220d31eeec696182814f3c Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Fri, 3 Nov 2023 19:36:39 -0400 Subject: [PATCH 6/7] Upgrade packages, increment version --- lib/main.dart | 2 +- pubspec.lock | 20 ++++++++++---------- pubspec.yaml | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 1ca54447..0e3d6d61 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,7 +19,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart'; // ignore: implementation_imports import 'package:easy_localization/src/localization.dart'; -const String currentVersion = '0.14.31'; +const String currentVersion = '0.14.32'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES diff --git a/pubspec.lock b/pubspec.lock index 1ad4fe53..7ab0e9fa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -283,10 +283,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: ad76540d21c066228ee3f9d1dad64a9f7e46530e8bb7c85011a88bc1fd874bc5 + sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" flutter_local_notifications: dependency: "direct main" description: @@ -799,10 +799,10 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" + sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba url: "https://pub.dev" source: hosted - version: "6.1.14" + version: "6.2.1" url_launcher_android: dependency: transitive description: @@ -887,18 +887,18 @@ packages: dependency: "direct main" description: name: webview_flutter - sha256: c1ab9b81090705c6069197d9fdc1625e587b52b8d70cdde2339d177ad0dbb98e + sha256: "42393b4492e629aa3a88618530a4a00de8bb46e50e7b3993fedbfdc5352f0dbf" url: "https://pub.dev" source: hosted - version: "4.4.1" + version: "4.4.2" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: b0cd33dd7d3dd8e5f664e11a19e17ba12c352647269921a3b568406b001f1dff + sha256: "8326ee235f87605a2bfc444a4abc897f4abc78d83f054ba7d3d1074ce82b4fbf" url: "https://pub.dev" source: hosted - version: "3.12.0" + version: "3.12.1" webview_flutter_platform_interface: dependency: transitive description: @@ -911,10 +911,10 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: b4b42295b3aa91ed22ba6d3dd7de56efbb8f3ab3d6e41d8b1615d13537c7801d + sha256: af6f5ab05918070b33507b0d453ba9fb7d39338a3256c23cf9433dc68100774a url: "https://pub.dev" source: hosted - version: "3.9.2" + version: "3.9.3" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5635a7dd..6c7cb05a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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+225 # When changing this, update the tag in main() accordingly +version: 0.14.32+226 # When changing this, update the tag in main() accordingly environment: sdk: '>=3.0.0 <4.0.0' From b03675811cb8263d586654f07c3460fee15374b1 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Fri, 3 Nov 2023 19:40:19 -0400 Subject: [PATCH 7/7] Added new language menu entries --- lib/main.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index 0e3d6d61..4d973de7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -40,6 +40,8 @@ List> supportedLocales = const [ MapEntry(Locale('bs'), 'Bosanski'), MapEntry(Locale('pt'), 'Brasileiro'), MapEntry(Locale('cs'), 'Česky'), + MapEntry(Locale('sv'), 'Svenska'), + MapEntry(Locale('nl'), 'Nederlands'), ]; const fallbackLocale = Locale('en'); const localeDir = 'assets/translations';