From 03447c33be0a3cdaafcc61c65903242fd589cdcc Mon Sep 17 00:00:00 2001 From: DwainZwerg <97027379+DwainZwerg@users.noreply.github.com> Date: Fri, 13 Oct 2023 07:30:23 +0000 Subject: [PATCH 1/7] Update de.json --- assets/translations/de.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/assets/translations/de.json b/assets/translations/de.json index 90fbdc6..bc0a558 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -170,7 +170,7 @@ "lastUpdateCheckX": "Letzte Aktualisierungsprüfung: {}", "remove": "Entfernen", "yesMarkUpdated": "Ja, als aktualisiert markieren", - "fdroid": "F-Droid Official", + "fdroid": "offizielles F-Droid-Repo", "appIdOrName": "App ID oder Name", "appId": "App ID", "appWithIdOrNameNotFound": "Es wurde keine App mit dieser ID oder diesem Namen gefunden", @@ -246,26 +246,26 @@ "backgroundUpdateReqsExplanation": "Die Hintergrundaktualisierung ist möglicherweise nicht für alle Apps möglich.", "backgroundUpdateLimitsExplanation": "Der Erfolg einer Hintergrundinstallation kann nur festgestellt werden, wenn Obtainium geöffnet wird.", "verifyLatestTag": "Überprüfe das „latest“ Tag", - "intermediateLinkRegex": "Filter für einen \"Zwischen\"-Link, der zuerst besucht werden soll", - "intermediateLinkNotFound": "Zwischenlink nicht gefunden", + "intermediateLinkRegex": "Filter für einen \„Zwischen\“-Link, der zuerst besucht werden soll", + "intermediateLinkNotFound": "\„Zwischen\“link nicht gefunden", "exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)", "bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist", "autoSelectHighestVersionCode": "Automatisch höchste APK-Code-Version auswählen", - "versionExtractionRegEx": "Versions-Extraktion RegEx", + "versionExtractionRegEx": "Versions-Extraktion per RegEx", "matchGroupToUse": "Zu verwendende Gruppe abgleichen", "highlightTouchTargets": "Weniger offensichtliche Ziele hervorheben", "pickExportDir": "Export-Verzeichnis wählen", "autoExportOnChanges": "Automatischer Export bei Änderung", "filterVersionsByRegEx": "Versionen nach regulären Ausdrücken filtern", "trySelectingSuggestedVersionCode": "Versuchen, die vorgeschlagene APK-Code-Version auszuwählen", - "dontSortReleasesList": "Retain release order from API", + "dontSortReleasesList": "Freigaberelease von der API ordern", "reverseSort": "Umgekehrtes Sortieren", - "debugMenu": "Debug Menü", + "debugMenu": "Debug-Menü", "bgTaskStarted": "Hintergrundaufgabe gestartet – Logs prüfen.", "runBgCheckNow": "Hintergrundaktualisierungsprüfung jetzt durchführen", - "versionExtractWholePage": "Apply Version Extraction Regex to Entire Page", - "installing": "Installing", - "skipUpdateNotifications": "Skip update notifications", + "versionExtractWholePage": "Versions-Extraktion per RegEx auf die gesamte Seite anwenden", + "installing": "Installiere", + "skipUpdateNotifications": "Keine Benachrichtigung zu App-Updates geben", "updatesAvailableNotifChannel": "Aktualisierungen verfügbar", "appsUpdatedNotifChannel": "Apps aktualisiert", "appsPossiblyUpdatedNotifChannel": "App Aktualisierungen wurden versucht", @@ -326,4 +326,4 @@ "one": "{} und 1 weitere Anwendung wurden möglicherweise aktualisiert.", "other": "{} und {} weitere Anwendungen wurden möglicherweise aktualisiert." } -} \ No newline at end of file +} From 1eefeae060a423db53ce352bf982f0422414bfea Mon Sep 17 00:00:00 2001 From: Daviteusz Date: Fri, 13 Oct 2023 13:00:16 +0200 Subject: [PATCH 2/7] locale(pl): Update Polish translations --- assets/translations/pl.json | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/assets/translations/pl.json b/assets/translations/pl.json index 184255a..9aa9f31 100644 --- a/assets/translations/pl.json +++ b/assets/translations/pl.json @@ -142,20 +142,20 @@ "warning": "Uwaga", "sourceIsXButPackageFromYPrompt": "Źródłem aplikacji jest '{}', ale pakiet wydania pochodzi z '{}'. Kontynuować?", "updatesAvailable": "Dostępne aktualizacje", - "updatesAvailableNotifDescription": "Powiadamia użytkownika o dostępności aktualizacji dla jednej lub więcej aplikacji obserwowanych przez Obtainium", + "updatesAvailableNotifDescription": "Informuje o dostępności aktualizacji dla jednej lub więcej aplikacji obserwowanych przez Obtainium", "noNewUpdates": "Brak nowych aktualizacji.", "xHasAnUpdate": "{} ma aktualizację.", - "appsUpdated": "Zaktualizowane aplikacje", - "appsUpdatedNotifDescription": "Powiadamia użytkownika, gdy jedna lub więcej aplikacji zostało zaktualizowanych w tle", + "appsUpdated": "Zaktualizowano aplikacje", + "appsUpdatedNotifDescription": "Informuje, gdy co najmniej jedna aplikacja została zaktualizowana w tle", "xWasUpdatedToY": "{} zaktualizowano do {}.", - "errorCheckingUpdates": "Sprawdzanie błędów aktualizacji", - "errorCheckingUpdatesNotifDescription": "Powiadomienie wyświetlane, gdy sprawdzanie aktualizacji w tle nie powiedzie się", + "errorCheckingUpdates": "Błąd sprawdzania aktualizacji", + "errorCheckingUpdatesNotifDescription": "Jest wyświetlane, gdy sprawdzanie aktualizacji w tle nie powiedzie się", "appsRemoved": "Usunięte aplikacje", - "appsRemovedNotifDescription": "Powiadamia użytkownika, gdy jedna lub więcej aplikacji zostało usuniętych z powodu błędów wczytywania", + "appsRemovedNotifDescription": "Informuje, gdy co najmniej jedna aplikacja została usunięta z powodu błędów podczas wczytywania", "xWasRemovedDueToErrorY": "Usunięto {} z powodu błędu: {}", "completeAppInstallation": "Ukończenie instalacji aplikacji", "obtainiumMustBeOpenToInstallApps": "Aby zainstalować aplikacje, Obtainium musi być otwarte", - "completeAppInstallationNotifDescription": "Prosi użytkownika o powrót do Obtainium w celu dokończenia instalacji aplikacji", + "completeAppInstallationNotifDescription": "Informuje o możliwości powrotu do Obtainium w celu dokończenia instalacji aplikacji", "checkingForUpdates": "Sprawdzanie aktualizacji", "checkingForUpdatesNotifDescription": "Tymczasowe powiadomienie pojawiające się podczas sprawdzania aktualizacji", "pleaseAllowInstallPerm": "Pozwól Obtainium instalować aplikacje", @@ -187,7 +187,7 @@ "disableVersionDetection": "Wyłącz wykrywanie wersji", "noVersionDetectionExplanation": "Opcja ta powinna być używana tylko w przypadku aplikacji, w których wykrywanie wersji nie działa poprawnie.", "downloadingX": "Pobieranie {}", - "downloadNotifDescription": "Powiadamia użytkownika o postępach w pobieraniu aplikacji", + "downloadNotifDescription": "Informuje o postępach w pobieraniu aplikacji", "noAPKFound": "Nie znaleziono pakietu APK", "noVersionDetection": "Bez wykrywania wersji", "categorize": "Kategoryzuj", @@ -240,7 +240,7 @@ "filterReleaseNotesByRegEx": "Filtruj informacje o wersji według wyrażenia regularnego", "customLinkFilterRegex": "Filtruj linki APK według wyrażenia regularnego (domyślnie \".apk$\")", "appsPossiblyUpdated": "Próbowano zaktualizować aplikację", - "appsPossiblyUpdatedNotifDescription": "Powiadamiaj o potencjalnym zastosowaniu w tle aktualizacji jednej lub większej ilości aplikacji", + "appsPossiblyUpdatedNotifDescription": "Powiadamia, gdy co najmniej jedna aktualizacja aplikacji została potencjalnie zastosowana w tle", "xWasPossiblyUpdatedToY": "{} być może zaktualizowano do {}.", "enableBackgroundUpdates": "Włącz aktualizacje w tle", "backgroundUpdateReqsExplanation": "Aktualizacje w tle mogą nie być możliwe dla wszystkich aplikacji.", @@ -252,7 +252,7 @@ "bgUpdatesOnWiFiOnly": "Wyłącz aktualizacje w tle, gdy nie ma połączenia z Wi-Fi", "autoSelectHighestVersionCode": "Automatycznie wybierz najwyższy kod wersji APK", "versionExtractionRegEx": "Wyrażenie regularne wyodrębniające wersję", - "matchGroupToUse": "Dopasuj grupę do użycia", + "matchGroupToUse": "Dopasuj grupę do użycia dla wyrażenia regularnego wyodrębniania wersji", "highlightTouchTargets": "Wyróżnij mniej oczywiste elementy dotykowe", "pickExportDir": "Wybierz katalog eksportu", "autoExportOnChanges": "Automatyczny eksport po wprowadzeniu zmian", @@ -263,17 +263,17 @@ "debugMenu": "Menu debugowania", "bgTaskStarted": "Uruchomiono zadanie w tle - sprawdź logi.", "runBgCheckNow": "Wymuś sprawdzenie aktualizacji w tle", - "versionExtractWholePage": "Apply Version Extraction Regex to Entire Page", - "installing": "Installing", - "skipUpdateNotifications": "Skip update notifications", - "updatesAvailableNotifChannel": "Dostępne aktualizacje", + "versionExtractWholePage": "Zastosuj wyrażenie regularne wyodrębniania wersji dla całej strony", + "installing": "Instalacja", + "skipUpdateNotifications": "Pomiń powiadomienia o aktualizacjach", + "updatesAvailableNotifChannel": "Dostępne aktualizacje aplikacji", "appsUpdatedNotifChannel": "Zaktualizowane aplikacje", "appsPossiblyUpdatedNotifChannel": "Informuj o próbach aktualizacji", - "errorCheckingUpdatesNotifChannel": "Sprawdzanie błędów aktualizacji", + "errorCheckingUpdatesNotifChannel": "Błędy sprawdzania aktualizacji", "appsRemovedNotifChannel": "Usunięte aplikacje", - "downloadingXNotifChannel": "Pobieranie {}", + "downloadingXNotifChannel": "Pobieranie aplikacji", "completeAppInstallationNotifChannel": "Ukończenie instalacji aplikacji", - "checkingForUpdatesNotifChannel": "Sprawdzanie aktualizacji", + "checkingForUpdatesNotifChannel": "Sprawdzanie dostępności aktualizacji", "removeAppQuestion": { "one": "Usunąć aplikację?", "few": "Usunąć aplikacje?", From 7f3e87767c25457d61dccc9c84129d1f7c2db955 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Fri, 13 Oct 2023 23:39:24 -0400 Subject: [PATCH 3/7] Switch back to parallelized BG task (#963) + better logging --- lib/custom_errors.dart | 12 +- lib/pages/app.dart | 4 +- lib/pages/apps.dart | 2 +- lib/pages/import_export.dart | 10 +- lib/pages/settings.dart | 4 +- lib/providers/apps_provider.dart | 218 ++++++++++++++++++------------- 6 files changed, 143 insertions(+), 107 deletions(-) diff --git a/lib/custom_errors.dart b/lib/custom_errors.dart index 657a82a..eed8484 100644 --- a/lib/custom_errors.dart +++ b/lib/custom_errors.dart @@ -101,9 +101,9 @@ class MultiAppMultiError extends ObtainiumError { .join('\n\n'); } -showError(dynamic e, BuildContext context) { +showMessage(dynamic e, BuildContext context, {bool isError = false}) { Provider.of(context, listen: false) - .add(e.toString(), level: LogLevels.error); + .add(e.toString(), level: isError ? LogLevels.error : LogLevels.info); if (e is String || (e is ObtainiumError && !e.unexpected)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString())), @@ -115,8 +115,8 @@ showError(dynamic e, BuildContext context) { return AlertDialog( scrollable: true, title: Text(e is MultiAppMultiError - ? tr('someErrors') - : tr('unexpectedError')), + ? tr(isError ? 'someErrors' : 'updates') + : tr(isError ? 'unexpectedError' : 'unknown')), content: GestureDetector( onLongPress: () { Clipboard.setData(ClipboardData(text: e.toString())); @@ -137,6 +137,10 @@ showError(dynamic e, BuildContext context) { } } +showError(dynamic e, BuildContext context) { + showMessage(e, context, isError: true); +} + String list2FriendlyString(List list) { return list.length == 2 ? '${list[0]} ${tr('and')} ${list[1]}' diff --git a/lib/pages/app.dart b/lib/pages/app.dart index b49deb4..7622a96 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -292,7 +292,7 @@ class _AppPageState extends State { if (source?.enforceTrackOnly == true) { app.app.additionalSettings['trackOnly'] = true; // ignore: use_build_context_synchronously - showError(tr('appsFromSourceAreTrackOnly'), context); + showMessage(tr('appsFromSourceAreTrackOnly'), context); } if (app.app.additionalSettings['versionDetection'] == 'releaseDateAsVersion') { @@ -343,7 +343,7 @@ class _AppPageState extends State { ); if (app?.app.installedVersion != null && !trackOnly) { // ignore: use_build_context_synchronously - showError(tr('appsUpdated'), context); + showMessage(tr('appsUpdated'), context); } if (res.isNotEmpty && mounted) { Navigator.of(context).pop(); diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 49feff6..0d3c2c8 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -705,7 +705,7 @@ class AppsPageState extends State { return []; }).then((value) { if (shouldInstallUpdates) { - showError(tr('appsUpdated'), context); + showMessage(tr('appsUpdated'), context); } }); } diff --git a/lib/pages/import_export.dart b/lib/pages/import_export.dart index 68de7f5..200ff7a 100644 --- a/lib/pages/import_export.dart +++ b/lib/pages/import_export.dart @@ -81,7 +81,7 @@ class _ImportExportPageState extends State { }); appsProvider.addAppsByURL(urls).then((errors) { if (errors.isEmpty) { - showError(tr('importedX', args: [plural('apps', urls.length)]), + showMessage(tr('importedX', args: [plural('apps', urls.length)]), context); } else { showDialog( @@ -111,7 +111,7 @@ class _ImportExportPageState extends State { sp: settingsProvider) .then((String? result) { if (result != null) { - showError(tr('exportedTo', args: [result]), context); + showMessage(tr('exportedTo', args: [result]), context); } }).catchError((e) { showError(e, context); @@ -141,7 +141,7 @@ class _ImportExportPageState extends State { } }); appsProvider.addMissingCategories(settingsProvider); - showError(tr('importedX', args: [plural('apps', value)]), context); + showMessage(tr('importedX', args: [plural('apps', value)]), context); }); } else { // User canceled the picker @@ -216,7 +216,7 @@ class _ImportExportPageState extends State { var errors = await appsProvider.addAppsByURL(selectedUrls); if (errors.isEmpty) { // ignore: use_build_context_synchronously - showError( + showMessage( tr('importedX', args: [plural('apps', selectedUrls.length)]), context); @@ -274,7 +274,7 @@ class _ImportExportPageState extends State { var errors = await appsProvider.addAppsByURL(selectedUrls); if (errors.isEmpty) { // ignore: use_build_context_synchronously - showError( + showMessage( tr('importedX', args: [plural('apps', selectedUrls.length)]), context); } else { diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 02237de..265be4e 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -535,7 +535,7 @@ class _SettingsPageState extends State { onPressed: () { context.read().get().then((logs) { if (logs.isEmpty) { - showError(ObtainiumError(tr('noLogs')), context); + showMessage(ObtainiumError(tr('noLogs')), context); } else { showDialog( context: context, @@ -577,7 +577,7 @@ class _SettingsPageState extends State { const Duration(seconds: 0), bgUpdateCheckAlarmId + 200, bgUpdateCheck); - showError(tr('bgTaskStarted'), context); + showMessage(tr('bgTaskStarted'), context); }, child: Text(tr('runBgCheckNow'))) ], diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index db1087b..b84306d 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -1325,18 +1325,19 @@ class _APKOriginWarningDialogState extends State { /// /// @param List>? toCheck: The appIds to check for updates (with the number of previous attempts made per appid) (defaults to all apps) /// -/// @param List? toInstall: The appIds to attempt to update (defaults to an empty array) +/// @param List? toInstall: The appIds to attempt to update (if empty - which is the default - all pending updates are taken) /// /// 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 are errors, the task is run again for the remaining apps after a few minutes (duration depends on the errors), up to a maximum of 5 tries for any app. +/// 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. /// /// Once all update checks are complete, the task is run 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 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 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(); @@ -1405,97 +1406,120 @@ Future bgUpdateCheck(int taskId, Map? params) async { bool installMode = toCheck.isEmpty; // Task is either in update mode or install mode - // In install mode, grab all available silent updates unless explicitly told otherwise - if (installMode && 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)); - } - } - } - 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 - List toNotify = []; + // 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 { - for (int i = 0; i < toCheck.length; i++) { - var appId = toCheck[i].key; - var attemptCount = toCheck[i].value + 1; - 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 (networkRestricted || - !(await appsProvider.canInstallSilently(app!.app))) { - if (newApp.additionalSettings['skipUpdateNotifications'] != - true) { - toNotify.add(newApp); - } - } - } - 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 (attemptCount < maxAttempts) { - var remainingSeconds = e is RateLimitError - ? (i == 0 ? (e.remainingMinutes * 60) : (5 * 60)) - : e is ClientException - ? (15 * 60) - : pow(attemptCount, 2).toInt(); - 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, attemptCount)); - 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); + // Check for updates + notificationsProvider.notify(notif, cancelExisting: true); + updates = await appsProvider.checkUpdates( + specificIds: toCheck.map((e) => e.key).toList()); + } 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; } } finally { - if (toNotify.isNotEmpty) { - notificationsProvider.notify(UpdateNotification(toNotify)); + 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]); + } } } - // If you're done checking and found some silently installable updates, schedule another task which will run in install mode - if (didCompleteChecking) { + + // 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))); + } + } + + // 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( @@ -1506,11 +1530,19 @@ Future bgUpdateCheck(int taskId, Map? params) async { .map((entry) => {'key': entry.key, 'value': entry.value}) .toList() }); - } else if (didCompleteChecking) { - logs.add('BG update task $taskId: Done.'); } } else { - // If in install mode... + // 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) { @@ -1562,9 +1594,9 @@ Future bgUpdateCheck(int taskId, Map? params) async { .notify(ErrorCheckingUpdatesNotification(e.toString())); } } - if (didCompleteInstalling) { - logs.add('BG install task $taskId: Done.'); - } + } + if (didCompleteInstalling || toInstall.isEmpty) { + logs.add('BG install task $taskId: Done.'); } } } From aa8d45e63659a380ea70e36e9d63a82c7d7bc4a7 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 14 Oct 2023 01:29:02 -0400 Subject: [PATCH 4/7] Make Third Party F-Droid Repos Searchable (#995) --- lib/app_sources/fdroidrepo.dart | 83 ++++++++++++++++++++++++++++-- lib/pages/add_app.dart | 2 +- lib/providers/apps_provider.dart | 8 +-- lib/providers/source_provider.dart | 17 +++--- 4 files changed, 95 insertions(+), 15 deletions(-) diff --git a/lib/app_sources/fdroidrepo.dart b/lib/app_sources/fdroidrepo.dart index 31f8435..22b8e49 100644 --- a/lib/app_sources/fdroidrepo.dart +++ b/lib/app_sources/fdroidrepo.dart @@ -7,6 +7,7 @@ import 'package:obtainium/providers/source_provider.dart'; class FDroidRepo extends AppSource { FDroidRepo() { name = tr('fdroidThirdPartyRepo'); + canSearch = true; additionalSourceAppSpecificSettingFormItems = [ [ @@ -22,12 +23,85 @@ class FDroidRepo extends AppSource { ]; } + String removeQueryParamsFromUrl(String url, {List keep = const []}) { + var uri = Uri.parse(url); + Map resultParams = {}; + uri.queryParameters.forEach((key, value) { + if (keep.contains(key)) { + resultParams[key] = value; + } + }); + url = uri.replace(queryParameters: resultParams).toString(); + if (url.endsWith('?')) { + url = url.substring(0, url.length - 1); + } + return url; + } + + @override + String sourceSpecificStandardizeURL(String url) { + var standardUri = Uri.parse(url); + var pathSegments = standardUri.pathSegments; + if (pathSegments.last == 'index.xml') { + pathSegments.removeLast(); + standardUri = standardUri.replace(path: pathSegments.join('/')); + } + return removeQueryParamsFromUrl(standardUri.toString(), keep: ['appId']); + } + + @override + Future>> search(String query, + {Map querySettings = const {}}) async { + query = removeQueryParamsFromUrl(standardizeUrl(query)); + var res = await sourceRequest('$query/index.xml'); + if (res.statusCode == 200) { + var body = parse(res.body); + Map> results = {}; + body.querySelectorAll('application').toList().forEach((app) { + String appId = app.attributes['id']!; + results['$query?appId=$appId'] = [ + app.querySelector('name')?.innerHtml ?? appId, + app.querySelector('desc')?.innerHtml ?? '' + ]; + }); + return results; + } else { + throw getObtainiumHttpError(res); + } + } + + @override + App endOfGetAppChanges(App app) { + var uri = Uri.parse(app.url); + String? appId; + if (!isTempId(app)) { + appId = app.id; + } else if (uri.queryParameters['appId'] != null) { + appId = uri.queryParameters['appId']; + } + if (appId != null) { + app.url = uri + .replace( + queryParameters: Map.fromEntries( + [...uri.queryParameters.entries, MapEntry('appId', appId)])) + .toString(); + app.additionalSettings['appIdOrName'] = appId; + app.id = appId; + } + return app; + } + @override Future getLatestAPKDetails( String standardUrl, Map additionalSettings, ) async { String? appIdOrName = additionalSettings['appIdOrName']; + var standardUri = Uri.parse(standardUrl); + if (standardUri.queryParameters['appId'] != null) { + appIdOrName = standardUri.queryParameters['appId']; + } + standardUrl = removeQueryParamsFromUrl(standardUrl); bool pickHighestVersionCode = additionalSettings['pickHighestVersionCode']; if (appIdOrName == null) { throw NoReleasesError(); @@ -41,7 +115,7 @@ class FDroidRepo extends AppSource { if (foundApps.isEmpty) { foundApps = body.querySelectorAll('application').where((element) { return element.querySelector('name')?.innerHtml.toLowerCase() == - appIdOrName.toLowerCase(); + appIdOrName!.toLowerCase(); }).toList(); } if (foundApps.isEmpty) { @@ -50,7 +124,7 @@ class FDroidRepo extends AppSource { .querySelector('name') ?.innerHtml .toLowerCase() - .contains(appIdOrName.toLowerCase()) ?? + .contains(appIdOrName!.toLowerCase()) ?? false; }).toList(); } @@ -58,8 +132,9 @@ class FDroidRepo extends AppSource { throw ObtainiumError(tr('appWithIdOrNameNotFound')); } var authorName = body.querySelector('repo')?.attributes['name'] ?? name; - var appName = - foundApps[0].querySelector('name')?.innerHtml ?? appIdOrName; + String appId = foundApps[0].attributes['id']!; + foundApps[0].querySelector('name')?.innerHtml ?? appId; + var appName = foundApps[0].querySelector('name')?.innerHtml ?? appId; var releases = foundApps[0].querySelectorAll('package'); String? latestVersion = releases[0].querySelector('version')?.innerHtml; String? added = releases[0].querySelector('added')?.innerHtml; diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index c792a2a..bbe82ad 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -153,7 +153,7 @@ class _AddAppPageState extends State { overrideSource: pickedSourceOverride, inferAppIdIfOptional: inferAppIdIfOptional); // Only download the APK here if you need to for the package ID - if (sourceProvider.isTempId(app) && + if (isTempId(app) && app.additionalSettings['trackOnly'] != true) { // ignore: use_build_context_synchronously var apkUrl = await appsProvider.confirmApkUrl(app, context); diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index b84306d..3ef8e73 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -267,10 +267,10 @@ class AppsProvider with ChangeNotifier { File downloadedFile, String downloadUrl) async { // If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed // The former case should be handled (give the App its real ID), the latter is a security issue - var isTempId = SourceProvider().isTempId(app); + var isTempIdBool = isTempId(app); if (newInfo != null) { if (app.id != newInfo.packageName) { - if (apps[app.id] != null && !isTempId && !app.allowIdChange) { + if (apps[app.id] != null && !isTempIdBool && !app.allowIdChange) { throw IDChangedError(newInfo.packageName!); } var idChangeWasAllowed = app.allowIdChange; @@ -281,10 +281,10 @@ class AppsProvider with ChangeNotifier { '${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.${downloadedFile.path.split('.').last}'); if (apps[originalAppId] != null) { await removeApps([originalAppId]); - await saveApps([app], onlyIfExists: !isTempId && !idChangeWasAllowed); + await saveApps([app], onlyIfExists: !isTempIdBool && !idChangeWasAllowed); } } - } else if (isTempId) { + } else if (isTempIdBool) { throw ObtainiumError('Could not get ID from APK'); } return downloadedFile; diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 0ac9b91..e3e1c3a 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -372,6 +372,10 @@ abstract class AppSource { return null; } + App endOfGetAppChanges(App app) { + return app; + } + Future sourceRequest(String url, {bool followRedirects = true, Map additionalSettings = @@ -541,6 +545,11 @@ intValidator(String? value, {bool positive = false}) { return null; } +bool isTempId(App app) { + // return app.id == generateTempID(app.url, app.additionalSettings); + return RegExp('^[0-9]+\$').hasMatch(app.id); +} + class SourceProvider { // Add more source classes here so they are available via the service List get sources => [ @@ -626,11 +635,6 @@ class SourceProvider { String standardUrl, Map additionalSettings) => (standardUrl + additionalSettings.toString()).hashCode.toString(); - bool isTempId(App app) { - // return app.id == generateTempID(app.url, app.additionalSettings); - return RegExp('^[0-9]+\$').hasMatch(app.id); - } - Future getApp( AppSource source, String url, Map additionalSettings, {App? currentApp, @@ -672,7 +676,7 @@ class SourceProvider { String apkVersion = apk.version.replaceAll('/', '-'); var name = currentApp != null ? currentApp.name.trim() : ''; name = name.isNotEmpty ? name : apk.names.name; - return App( + App finalApp = App( currentApp?.id ?? ((!source.appIdInferIsOptional || (source.appIdInferIsOptional && inferAppIdIfOptional)) @@ -698,6 +702,7 @@ class SourceProvider { source.appIdInferIsOptional && inferAppIdIfOptional // Optional ID inferring may be incorrect - allow correction on first install ); + return source.endOfGetAppChanges(finalApp); } // Returns errors in [results, errors] instead of throwing them From 7d7803247dfe6e8eb84bb28e642fe1b9f5d84334 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 14 Oct 2023 01:30:37 -0400 Subject: [PATCH 5/7] Update packages, increment version --- lib/main.dart | 2 +- pubspec.lock | 8 ++++---- pubspec.yaml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 4088f9b..c202d40 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.27'; +const String currentVersion = '0.14.28'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES diff --git a/pubspec.lock b/pubspec.lock index 3ea4961..39e4c03 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -246,10 +246,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 + sha256: "903dd4ba13eae7cef64acc480e91bf54c3ddd23b5b90b639c170f3911e489620" url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "6.0.0" flutter: dependency: "direct main" description: flutter @@ -546,10 +546,10 @@ packages: dependency: transitive description: name: permission_handler_android - sha256: ace7d15a3d1a4a0b91c041d01e5405df221edb9de9116525efc773c74e6fc790 + sha256: f9fddd3b46109bd69ff3f9efa5006d2d309b7aec0f3c1c5637a60a2d5659e76e url: "https://pub.dev" source: hosted - version: "11.0.5" + version: "11.1.0" permission_handler_apple: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index be36743..02990a4 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.27+219 # When changing this, update the tag in main() accordingly +version: 0.14.28+220 # When changing this, update the tag in main() accordingly environment: sdk: '>=3.0.0 <4.0.0' @@ -49,7 +49,7 @@ dependencies: permission_handler: ^11.0.0 fluttertoast: ^8.0.9 device_info_plus: ^9.0.0 - file_picker: ^5.2.10 + file_picker: ^6.0.0 animations: ^2.0.4 android_package_installer: git: From 8b4709c1323301b812635cd4440a5b6d74825ea9 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 14 Oct 2023 01:36:26 -0400 Subject: [PATCH 6/7] Fixed syntax errors in de.json --- assets/translations/de.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/translations/de.json b/assets/translations/de.json index bc0a558..9f9efbe 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -246,8 +246,8 @@ "backgroundUpdateReqsExplanation": "Die Hintergrundaktualisierung ist möglicherweise nicht für alle Apps möglich.", "backgroundUpdateLimitsExplanation": "Der Erfolg einer Hintergrundinstallation kann nur festgestellt werden, wenn Obtainium geöffnet wird.", "verifyLatestTag": "Überprüfe das „latest“ Tag", - "intermediateLinkRegex": "Filter für einen \„Zwischen\“-Link, der zuerst besucht werden soll", - "intermediateLinkNotFound": "\„Zwischen\“link nicht gefunden", + "intermediateLinkRegex": "Filter für einen „Zwischen“-Link, der zuerst besucht werden soll", + "intermediateLinkNotFound": "„Zwischen“link nicht gefunden", "exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)", "bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist", "autoSelectHighestVersionCode": "Automatisch höchste APK-Code-Version auswählen", From a726b09f266fe22ac61bae51c1a52aeaa10f7ec7 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 14 Oct 2023 05:24:08 -0400 Subject: [PATCH 7/7] Fix Source selection (HTML) which broke in the last release --- lib/app_sources/fdroidrepo.dart | 1 + lib/main.dart | 2 +- lib/providers/source_provider.dart | 3 ++- pubspec.yaml | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/app_sources/fdroidrepo.dart b/lib/app_sources/fdroidrepo.dart index 22b8e49..6152c2b 100644 --- a/lib/app_sources/fdroidrepo.dart +++ b/lib/app_sources/fdroidrepo.dart @@ -8,6 +8,7 @@ class FDroidRepo extends AppSource { FDroidRepo() { name = tr('fdroidThirdPartyRepo'); canSearch = true; + neverAutoSelect = true; additionalSourceAppSpecificSettingFormItems = [ [ diff --git a/lib/main.dart b/lib/main.dart index c202d40..0a6ada9 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.28'; +const String currentVersion = '0.14.29'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index e3e1c3a..ce07727 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -330,6 +330,7 @@ abstract class AppSource { bool appIdInferIsOptional = false; bool allowSubDomains = false; bool naiveStandardVersionDetection = false; + bool neverAutoSelect = false; AppSource() { name = runtimeType.toString(); @@ -604,7 +605,7 @@ class SourceProvider { } } if (source == null) { - for (var s in sources.where((element) => element.host == null)) { + for (var s in sources.where((element) => element.host == null && !element.neverAutoSelect)) { try { s.sourceSpecificStandardizeURL(url); source = s; diff --git a/pubspec.yaml b/pubspec.yaml index 02990a4..e90cf53 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.28+220 # When changing this, update the tag in main() accordingly +version: 0.14.29+221 # When changing this, update the tag in main() accordingly environment: sdk: '>=3.0.0 <4.0.0'