diff --git a/lib/main.dart b/lib/main.dart index ff27e29..f70dcff 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,8 +2,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:http/http.dart'; -import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/pages/home.dart'; import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/logs_provider.dart'; @@ -71,179 +69,6 @@ Future loadTranslations() async { fallbackTranslations: controller.fallbackTranslations); } -moveStrToEnd(List arr, String str, {String? strB}) { - String? temp; - arr.removeWhere((element) { - bool res = element == str || element == strB; - if (res) { - temp = element; - } - return res; - }); - if (temp != null) { - arr = [...arr, temp!]; - } - return arr; -} - -/// Background updater function -/// -/// @param List? toCheck: The appIds to check for updates (default to all apps sorted by last update check time) -/// -/// @param List? toInstall: The appIds to attempt to update (defaults to an empty array) -/// -/// @param int? attemptCount: The number of times the function has failed up to this point (defaults to 0) -/// -/// When toCheck is empty, the function is in "install mode" (else it is in "update mode"). -/// In update mode, all apps in toCheck are checked for updates. -/// If an update is available, the appId is either added to toInstall (if a background update is possible) or the user is notified. -/// If there is an error, the function tries to continue after a few minutes (duration depends on the error), up to a maximum of 5 tries. -/// -/// Once all update checks are complete, the function is called again in install mode. -/// In this mode, all apps in toInstall are downloaded and installed in the background (install result is unknown). -/// If there is an error, the function tries to continue after a few minutes (duration depends on the error), up to a maximum of 5 tries. -/// -/// In either mode, if the function fails after the maximum number of tries, the user is notified. -@pragma('vm:entry-point') -Future bgUpdateCheck(int taskId, Map? params) async { - WidgetsFlutterBinding.ensureInitialized(); - await EasyLocalization.ensureInitialized(); - await AndroidAlarmManager.initialize(); - await loadTranslations(); - - LogsProvider logs = LogsProvider(); - NotificationsProvider notificationsProvider = NotificationsProvider(); - AppsProvider appsProvider = AppsProvider(isBg: true); - await appsProvider.loadApps(); - var settingsProvider = SettingsProvider(); - await settingsProvider.initializeSettings(); - - int maxAttempts = 4; - - params ??= {}; - if (params['toCheck'] == null) { - settingsProvider.lastBGCheckTime = DateTime.now(); - } - int attemptCount = (params['attemptCount'] ?? 0) + 1; - List toCheck = [ - ...(params['toCheck'] ?? appsProvider.getAppsSortedByUpdateCheckTime()) - ]; - List toInstall = [...(params['toInstall'] ?? ([]))]; - - bool installMode = toCheck.isEmpty && toInstall.isNotEmpty; - - logs.add( - 'BG ${installMode ? 'install' : 'update'} task $taskId: Started${attemptCount > 1 ? '. ${attemptCount - 1} consecutive fail(s)' : ''}.'); - - if (!installMode) { - var didCompleteChecking = false; - CheckingUpdatesNotification? notif; - for (int i = 0; i < toCheck.length; i++) { - var appId = toCheck[i]; - AppInMemory? app = appsProvider.apps[appId]; - if (app?.app.installedVersion != null) { - try { - notificationsProvider.notify( - notif = CheckingUpdatesNotification(app?.name ?? appId), - cancelExisting: true); - App? newApp = await appsProvider.checkUpdate(appId); - if (newApp != null) { - if (!(await appsProvider.canInstallSilently( - app!.app, settingsProvider))) { - notificationsProvider.notify( - UpdateNotification([newApp], id: newApp.id.hashCode - 1)); - } else { - toInstall.add(appId); - } - } - if (i == (toCheck.length - 1)) { - didCompleteChecking = true; - } - } catch (e) { - logs.add( - 'BG update task $taskId: Got error on checking for $appId \'${e.toString()}\'.'); - if (attemptCount < maxAttempts) { - var remainingSeconds = e is RateLimitError - ? (e.remainingMinutes * 60) - : e is ClientException - ? (15 * 60) - : (attemptCount ^ 2); - logs.add( - 'BG update task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).'); - var remainingToCheck = moveStrToEnd(toCheck.sublist(i), appId); - AndroidAlarmManager.oneShot( - Duration(seconds: remainingSeconds), taskId + 1, bgUpdateCheck, - params: { - 'toCheck': remainingToCheck, - 'toInstall': toInstall, - 'attemptCount': attemptCount - }); - break; - } else { - notificationsProvider - .notify(ErrorCheckingUpdatesNotification(e.toString())); - } - } finally { - if (notif != null) { - notificationsProvider.cancel(notif.id); - } - } - } - } - if (didCompleteChecking && toInstall.isNotEmpty) { - 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}); - } else if (didCompleteChecking) { - logs.add('BG install task $taskId: Done.'); - } - } else { - var didCompleteInstalling = false; - toInstall = moveStrToEnd(toInstall, obtainiumId); - for (var i = 0; i < toInstall.length; i++) { - String appId = toInstall[i]; - try { - logs.add( - 'BG install task $taskId: Attempting to update $appId in the background.'); - await appsProvider.downloadAndInstallLatestApps( - [appId], null, settingsProvider, - 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) { - logs.add( - 'BG install task $taskId: Got error on updating $appId \'${e.toString()}\'.'); - if (attemptCount < maxAttempts) { - var remainingSeconds = attemptCount; - logs.add( - 'BG install task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).'); - var remainingToInstall = moveStrToEnd(toInstall.sublist(i), appId); - AndroidAlarmManager.oneShot( - Duration(seconds: remainingSeconds), taskId + 1, bgUpdateCheck, - params: { - 'toCheck': toCheck, - 'toInstall': remainingToInstall, - 'attemptCount': attemptCount - }); - break; - } else { - notificationsProvider - .notify(ErrorCheckingUpdatesNotification(e.toString())); - } - } - if (didCompleteInstalling) { - logs.add('BG install task $taskId: Done.'); - } - } - } -} - void main() async { WidgetsFlutterBinding.ensureInitialized(); try { diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 1ea70ce..5b673b6 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +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'; @@ -99,6 +100,38 @@ Set findStandardFormatsForVersion(String version, bool strict) { return results; } +moveStrToEnd(List arr, String str, {String? strB}) { + String? temp; + arr.removeWhere((element) { + bool res = element == str || element == strB; + if (res) { + temp = element; + } + return res; + }); + if (temp != null) { + arr = [...arr, temp!]; + } + return arr; +} + +moveStrToEndMapEntryWithCount( + List> arr, MapEntry str, + {MapEntry? strB}) { + MapEntry? temp; + arr.removeWhere((element) { + bool res = element.key == str.key || element.key == strB?.key; + if (res) { + temp = element; + } + return res; + }); + if (temp != null) { + arr = [...arr, temp!]; + } + return arr; +} + class AppsProvider with ChangeNotifier { // In memory App state (should always be kept in sync with local storage versions) Map apps = {}; @@ -1221,3 +1254,206 @@ class _APKOriginWarningDialogState extends State { ); } } + +/// Background updater function +/// +/// @param List? toCheck: The appIds to check for updates (default to all apps sorted by last update check time) +/// +/// @param List? toInstall: The appIds to attempt to update (defaults to an empty array) +/// +/// @param int? attemptCount: The number of times the function has failed up to this point (defaults to 0) +/// +/// When toCheck is empty, the function is in "install mode" (else it is in "update mode"). +/// In update mode, all apps in toCheck are checked for updates. +/// If an update is available, the appId is either added to toInstall (if a background update is possible) or the user is notified. +/// If there is an error, the function tries to continue after a few minutes (duration depends on the error), up to a maximum of 5 tries. +/// +/// Once all update checks are complete, the function is called again in install mode. +/// In this mode, all apps in toInstall are downloaded and installed in the background (install result is unknown). +/// If there is an error, the function tries to continue after a few minutes (duration depends on the error), up to a maximum of 5 tries. +/// +/// In either mode, if the function fails after the maximum number of tries, the user is notified. +@pragma('vm:entry-point') +Future bgUpdateCheck(int taskId, Map? params) async { + WidgetsFlutterBinding.ensureInitialized(); + await EasyLocalization.ensureInitialized(); + await AndroidAlarmManager.initialize(); + await loadTranslations(); + + LogsProvider logs = LogsProvider(); + NotificationsProvider notificationsProvider = NotificationsProvider(); + AppsProvider appsProvider = AppsProvider(isBg: true); + await appsProvider.loadApps(); + var settingsProvider = SettingsProvider(); + await settingsProvider.initializeSettings(); + + int maxAttempts = 4; + + params ??= {}; + if (params['toCheck'] == null) { + settingsProvider.lastBGCheckTime = DateTime.now(); + } + List> toCheck = >[ + ...(params['toCheck'] + ?.map((entry) => MapEntry( + entry['key'] as String, entry['value'] as int)) + .toList() ?? + appsProvider + .getAppsSortedByUpdateCheckTime() + .map((e) => MapEntry(e, 0))) + ]; + List> toInstall = >[ + ...(params['toInstall'] + ?.map((entry) => MapEntry( + entry['key'] as String, entry['value'] as int)) + .toList() ?? + (>>[])) + ]; + + bool installMode = toCheck.isEmpty && + toInstall.isNotEmpty; // 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... + var didCompleteChecking = false; + CheckingUpdatesNotification? notif; + // Loop through all updates and check each + for (int i = 0; i < toCheck.length; i++) { + var appId = toCheck[i].key; + var retryCount = toCheck[i].value; + AppInMemory? app = appsProvider.apps[appId]; + if (app?.app.installedVersion != null) { + try { + notificationsProvider.notify( + notif = CheckingUpdatesNotification(app?.name ?? appId), + cancelExisting: true); + App? newApp = await appsProvider.checkUpdate(appId); + if (newApp != null) { + if (!(await appsProvider.canInstallSilently( + app!.app, settingsProvider))) { + notificationsProvider.notify( + UpdateNotification([newApp], id: newApp.id.hashCode - 1)); + } else { + toInstall.add(MapEntry(appId, 0)); + } + } + if (i == (toCheck.length - 1)) { + didCompleteChecking = 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 checking shortly + logs.add( + 'BG update task $taskId: Got error on checking for $appId \'${e.toString()}\'.'); + if (retryCount < maxAttempts) { + var remainingSeconds = e is RateLimitError + ? (i == 0 ? (e.remainingMinutes * 60) : (5 * 60)) + : e is ClientException + ? (15 * 60) + : (retryCount ^ 2); + logs.add( + 'BG update task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).'); + var remainingToCheck = moveStrToEndMapEntryWithCount( + toCheck.sublist(i), MapEntry(appId, retryCount + 1)); + AndroidAlarmManager.oneShot( + Duration(seconds: remainingSeconds), taskId + 1, bgUpdateCheck, + params: { + 'toCheck': remainingToCheck + .map((entry) => {'key': entry.key, 'value': entry.value}) + .toList(), + 'toInstall': toInstall + .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) + toCheck.removeAt(i); + i--; + notificationsProvider + .notify(ErrorCheckingUpdatesNotification(e.toString())); + } + } finally { + if (notif != null) { + notificationsProvider.cancel(notif.id); + } + } + } + } + // If you're done checking and found some silently installable updates, schedule another task which will run in install mode + if (didCompleteChecking && toInstall.isNotEmpty) { + 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 if (didCompleteChecking) { + logs.add('BG install task $taskId: Done.'); + } + } else { + // If in install mode... + 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, settingsProvider, + 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 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) { + logs.add('BG install task $taskId: Done.'); + } + } + } +}