Merge branch 'main' into re7gog

This commit is contained in:
Григорий Величко
2024-01-01 18:50:24 +03:00
committed by GitHub
46 changed files with 873 additions and 638 deletions

View File

@@ -8,7 +8,6 @@ import 'dart:math';
import 'package:http/http.dart' as http;
import 'package:crypto/crypto.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';
@@ -621,7 +620,8 @@ class AppsProvider with ChangeNotifier {
// Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
Future<List<String>> downloadAndInstallLatestApps(
List<String> appIds, BuildContext? context,
{NotificationsProvider? notificationsProvider}) async {
{NotificationsProvider? notificationsProvider,
bool forceParallelDownloads = false}) async {
notificationsProvider =
notificationsProvider ?? context?.read<NotificationsProvider>();
List<String> appsToInstall = [];
@@ -742,7 +742,7 @@ class AppsProvider with ChangeNotifier {
}
}
if (!settingsProvider.parallelDownloads) {
if (forceParallelDownloads || !settingsProvider.parallelDownloads) {
for (var id in appsToInstall) {
await updateFn(id);
}
@@ -1448,19 +1448,17 @@ class _APKOriginWarningDialogState extends State<APKOriginWarningDialog> {
/// 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 (in parallel).
/// If an update is available and it cannot be installed silently, the user is notified of the available update.
/// If there are any errors, the task is run again for the remaining apps after a few minutes (based on the error with the longest retry interval).
/// Any app that has reached it's retry limit, the user is notified that it could not be checked.
/// If there are any errors, we recursively call the same function with retry count for the relevant apps decremented (if zero, the user is notified).
///
/// Once all update checks are complete, the task is run again in install mode.
/// In this mode, all pending silent updates are downloaded and installed in the background (serially - one at a time).
/// 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.
/// In this mode, all pending silent updates are downloaded (in parallel) and installed in the background.
/// If there is an error, the user is notified.
///
@pragma('vm:entry-point')
Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
Future<void> bgUpdateCheck(String taskId, Map<String, dynamic>? params) async {
// ignore: avoid_print
print('Started $taskId: ${params.toString()}');
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
await AndroidAlarmManager.initialize();
await loadTranslations();
LogsProvider logs = LogsProvider();
@@ -1469,11 +1467,20 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
await appsProvider.loadApps();
int maxAttempts = 4;
int maxRetryWaitSeconds = 5;
var netResult = await (Connectivity().checkConnectivity());
if (netResult == ConnectivityResult.none) {
logs.add('BG update task: No network.');
return;
}
params ??= {};
if (params['toCheck'] == null) {
appsProvider.settingsProvider.lastBGCheckTime = DateTime.now();
}
bool firstEverUpdateTask = DateTime.fromMillisecondsSinceEpoch(0)
.compareTo(appsProvider.settingsProvider.lastCompletedBGCheckTime) ==
0;
List<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[
...(params['toCheck']
?.map((entry) => MapEntry<String, int>(
@@ -1481,6 +1488,11 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
.toList() ??
appsProvider
.getAppsSortedByUpdateCheckTime(
ignoreAppsCheckedAfter: params['toCheck'] == null
? firstEverUpdateTask
? null
: appsProvider.settingsProvider.lastCompletedBGCheckTime
: null,
onlyCheckInstalledOrTrackOnlyApps: appsProvider
.settingsProvider.onlyCheckInstalledOrTrackOnlyApps)
.map((e) => MapEntry(e, 0)))
@@ -1493,51 +1505,34 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
(<List<MapEntry<String, int>>>[]))
];
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 (toCheck.isNotEmpty) {
// Task is either in update mode or install mode
// 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)
// Then we run the function again in install mode (toCheck is empty)
var enoughTimePassed = appsProvider.settingsProvider.updateInterval != 0 &&
appsProvider.settingsProvider.lastCompletedBGCheckTime
.add(
Duration(minutes: appsProvider.settingsProvider.updateInterval))
.isBefore(DateTime.now());
if (!enoughTimePassed) {
// ignore: avoid_print
print(
'BG update task: Too early for another check (last check was ${appsProvider.settingsProvider.lastCompletedBGCheckTime.toIso8601String()}, interval is ${appsProvider.settingsProvider.updateInterval}).');
return;
}
logs.add('BG update task: Started (${toCheck.length}).');
// Init. vars.
List<App> updates = []; // All updates found (silent and non-silent)
@@ -1545,8 +1540,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
[]; // All non-silent updates that the user will be notified about
List<MapEntry<String, int>> toRetry =
[]; // All apps that got errors while checking
var retryAfterXSeconds =
0; // How long to wait until the next attempt (if there are errors)
var retryAfterXSeconds = 0;
MultiAppMultiError?
errors; // All errors including those that will lead to a retry
MultiAppMultiError toThrow =
@@ -1569,27 +1563,32 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
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()}\'.');
'BG update task: 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
int minRetryIntervalForThisApp = err is RateLimitError
? (err.remainingMinutes * 60)
: e is ClientException
? (15 * 60)
: pow(toCheckApp.value + 1, 2).toInt();
: (toCheckApp.value + 1);
if (minRetryIntervalForThisApp > maxRetryWaitSeconds) {
minRetryIntervalForThisApp = maxRetryWaitSeconds;
}
if (minRetryIntervalForThisApp > retryAfterXSeconds) {
retryAfterXSeconds = minRetryIntervalForThisApp;
}
} else {
toThrow.add(key, err, appName: errors?.appIdNames[key]);
if (err is! RateLimitError) {
toThrow.add(key, err, appName: errors?.appIdNames[key]);
}
}
});
} else {
@@ -1624,37 +1623,32 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
id: Random().nextInt(10000)));
}
}
// if there are update checks to retry, schedule a retry task
logs.add('BG update task: Done checking for updates.');
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(),
});
return await bgUpdateCheck(taskId, {
'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()
});
// If there are no more update checks, call the function in install mode
logs.add('BG update task: Done checking for updates.');
return await bgUpdateCheck(taskId, {
'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 you haven't explicitly been given updates to install, grab all available silent updates
if (toInstall.isEmpty && !networkRestricted) {
var temp = appsProvider.findExistingUpdates(installedOnly: true);
for (var i = 0; i < temp.length; i++) {
@@ -1664,60 +1658,34 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
}
}
}
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;
if (toInstall.isNotEmpty) {
logs.add('BG install task: Started (${toInstall.length}).');
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
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;
}
await appsProvider.downloadAndInstallLatestApps(
toInstall.map((e) => e.key).toList(), null,
notificationsProvider: notificationsProvider,
forceParallelDownloads: 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;
if (e is MultiAppMultiError) {
e.idsByErrorString.forEach((key, value) {
notificationsProvider.notify(ErrorCheckingUpdatesNotification(
e.errorsAppsString(key, value)));
});
} 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()));
// 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 install task: ${e.toString()}');
rethrow;
}
}
}
if (didCompleteInstalling || toInstall.isEmpty) {
logs.add('BG install task $taskId: Done.');
logs.add('BG install task: Done installing updates.');
}
}
appsProvider.settingsProvider.lastCompletedBGCheckTime = DateTime.now();
}