mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-29 20:43:28 +01:00 
			
		
		
		
	Update checking improvements (#38)
Still no auto retry for rate-limit. Instead, rate-limit errors are ignored and the unchecked Apps have to wait until the next cycle. Even this needs more testing before release.
This commit is contained in:
		| @@ -1,6 +1,7 @@ | ||||
| import 'dart:convert'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| class GitHub implements AppSource { | ||||
| @@ -76,7 +77,10 @@ class GitHub implements AppSource { | ||||
|       return APKDetails(version, targetRelease['apkUrls']); | ||||
|     } else { | ||||
|       if (res.headers['x-ratelimit-remaining'] == '0') { | ||||
|         throw 'Rate limit reached - try again in ${(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / 60000000).toString()} minutes'; | ||||
|         throw RateLimitError( | ||||
|             (int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / | ||||
|                     60000000) | ||||
|                 .round()); | ||||
|       } | ||||
|  | ||||
|       throw couldNotFindReleases; | ||||
|   | ||||
							
								
								
									
										8
									
								
								lib/custom_errors.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								lib/custom_errors.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| class RateLimitError { | ||||
|   late int remainingMinutes; | ||||
|   RateLimitError(this.remainingMinutes); | ||||
|  | ||||
|   @override | ||||
|   String toString() => | ||||
|       'Rate limit reached - try again in $remainingMinutes minutes'; | ||||
| } | ||||
							
								
								
									
										130
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						
									
										130
									
								
								lib/main.dart
									
									
									
									
									
								
							| @@ -1,6 +1,7 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:obtainium/app_sources/github.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/pages/home.dart'; | ||||
| import 'package:obtainium/providers/apps_provider.dart'; | ||||
| import 'package:obtainium/providers/notifications_provider.dart'; | ||||
| @@ -15,45 +16,73 @@ import 'package:device_info_plus/device_info_plus.dart'; | ||||
| const String currentReleaseTag = | ||||
|     'v0.4.1-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES | ||||
|  | ||||
| const String bgUpdateCheckTaskName = 'bg-update-check'; | ||||
|  | ||||
| bgUpdateCheck(int? ignoreAfterMicroseconds) async { | ||||
|   DateTime? ignoreAfter = ignoreAfterMicroseconds != null | ||||
|       ? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds) | ||||
|       : null; | ||||
|   var notificationsProvider = NotificationsProvider(); | ||||
|   await notificationsProvider.notify(checkingUpdatesNotification); | ||||
|   try { | ||||
|     var appsProvider = AppsProvider(); | ||||
|     await notificationsProvider.cancel(ErrorCheckingUpdatesNotification('').id); | ||||
|     await appsProvider.loadApps(); | ||||
|     // List<String> existingUpdateIds = // TODO: Uncomment this and below when it works | ||||
|     //     appsProvider.getExistingUpdates(installedOnly: true); | ||||
|     List<String> existingUpdateIds = | ||||
|         appsProvider.getExistingUpdates(installedOnly: true); | ||||
|     // DateTime nextIgnoreAfter = DateTime.now(); | ||||
|     try { | ||||
|       await appsProvider.checkUpdates(ignoreAfter: ignoreAfter); | ||||
|     } catch (e) { | ||||
|       if (e is RateLimitError) { | ||||
|         // Ignore these (scheduling another task as below does not work) | ||||
|         // Workmanager().registerOneOffTask( | ||||
|         //     bgUpdateCheckTaskName, bgUpdateCheckTaskName, | ||||
|         //     constraints: Constraints(networkType: NetworkType.connected), | ||||
|         //     initialDelay: Duration(minutes: e.remainingMinutes), | ||||
|         //     inputData: {'ignoreAfter': nextIgnoreAfter.microsecondsSinceEpoch}); | ||||
|       } else { | ||||
|         rethrow; | ||||
|       } | ||||
|     } | ||||
|     List<App> newUpdates = appsProvider | ||||
|         .getExistingUpdates(installedOnly: true) | ||||
|         .where((id) => !existingUpdateIds.contains(id)) | ||||
|         .map((e) => appsProvider.apps[e]!.app) | ||||
|         .toList(); | ||||
|     // List<String> silentlyUpdated = await appsProvider | ||||
|     //     .downloadAndInstallLatestApp( | ||||
|     //         [...newUpdates.map((e) => e.id), ...existingUpdateIds], null); | ||||
|     // if (silentlyUpdated.isNotEmpty) { | ||||
|     //   newUpdates | ||||
|     //       .where((element) => !silentlyUpdated.contains(element.id)) | ||||
|     //       .toList(); | ||||
|     //   notificationsProvider.notify( | ||||
|     //       SilentUpdateNotification( | ||||
|     //           silentlyUpdated.map((e) => appsProvider.apps[e]!.app).toList()), | ||||
|     //       cancelExisting: true); | ||||
|     // } | ||||
|     if (newUpdates.isNotEmpty) { | ||||
|       notificationsProvider.notify(UpdateNotification(newUpdates), | ||||
|           cancelExisting: true); | ||||
|     } | ||||
|     return Future.value(true); | ||||
|   } catch (e) { | ||||
|     notificationsProvider.notify(ErrorCheckingUpdatesNotification(e.toString()), | ||||
|         cancelExisting: true); | ||||
|     return Future.error(false); | ||||
|   } finally { | ||||
|     await notificationsProvider.cancel(checkingUpdatesNotification.id); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @pragma('vm:entry-point') | ||||
| void bgTaskCallback() { | ||||
|   // Background update checking process | ||||
|   Workmanager().executeTask((task, taskName) async { | ||||
|     var notificationsProvider = NotificationsProvider(); | ||||
|     await notificationsProvider.notify(checkingUpdatesNotification); | ||||
|     try { | ||||
|       var appsProvider = AppsProvider(); | ||||
|       await notificationsProvider | ||||
|           .cancel(ErrorCheckingUpdatesNotification('').id); | ||||
|       await appsProvider.loadApps(); | ||||
|       // List<String> existingUpdateIds = // TODO: Uncomment this and below when it works | ||||
|       //     appsProvider.getExistingUpdates(installedOnly: true); | ||||
|       List<App> newUpdates = await appsProvider.checkUpdates(); | ||||
|       // List<String> silentlyUpdated = await appsProvider | ||||
|       //     .downloadAndInstallLatestApp( | ||||
|       //         [...newUpdates.map((e) => e.id), ...existingUpdateIds], null); | ||||
|       // if (silentlyUpdated.isNotEmpty) { | ||||
|       //   newUpdates | ||||
|       //       .where((element) => !silentlyUpdated.contains(element.id)) | ||||
|       //       .toList(); | ||||
|       //   notificationsProvider.notify( | ||||
|       //       SilentUpdateNotification( | ||||
|       //           silentlyUpdated.map((e) => appsProvider.apps[e]!.app).toList()), | ||||
|       //       cancelExisting: true); | ||||
|       // } | ||||
|       if (newUpdates.isNotEmpty) { | ||||
|         notificationsProvider.notify(UpdateNotification(newUpdates), | ||||
|             cancelExisting: true); | ||||
|       } | ||||
|       return Future.value(true); | ||||
|     } catch (e) { | ||||
|       notificationsProvider.notify( | ||||
|           ErrorCheckingUpdatesNotification(e.toString()), | ||||
|           cancelExisting: true); | ||||
|       return Future.value(false); | ||||
|     } finally { | ||||
|       await notificationsProvider.cancel(checkingUpdatesNotification.id); | ||||
|     } | ||||
|   // Background process callback | ||||
|   Workmanager().executeTask((task, inputData) async { | ||||
|     return await bgUpdateCheck(inputData?['ignoreAfter']); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| @@ -95,16 +124,6 @@ class MyApp extends StatelessWidget { | ||||
|     if (settingsProvider.prefs == null) { | ||||
|       settingsProvider.initializeSettings(); | ||||
|     } else { | ||||
|       // Register the background update task according to the user's setting | ||||
|       if (settingsProvider.updateInterval > 0) { | ||||
|         Workmanager().registerPeriodicTask('bg-update-check', 'bg-update-check', | ||||
|             frequency: Duration(minutes: settingsProvider.updateInterval), | ||||
|             initialDelay: Duration(minutes: settingsProvider.updateInterval), | ||||
|             constraints: Constraints(networkType: NetworkType.connected), | ||||
|             existingWorkPolicy: ExistingWorkPolicy.replace); | ||||
|       } else { | ||||
|         Workmanager().cancelByUniqueName('bg-update-check'); | ||||
|       } | ||||
|       bool isFirstRun = settingsProvider.checkAndFlipFirstRun(); | ||||
|       if (isFirstRun) { | ||||
|         // If this is the first run, ask for notification permissions and add Obtainium to the Apps list | ||||
| @@ -119,9 +138,24 @@ class MyApp extends StatelessWidget { | ||||
|               currentReleaseTag, | ||||
|               [], | ||||
|               0, | ||||
|               ['true']) | ||||
|               ['true'], | ||||
|               null) | ||||
|         ]); | ||||
|       } | ||||
|       // Register the background update task according to the user's setting | ||||
|       if (settingsProvider.updateInterval == 0) { | ||||
|         Workmanager().cancelByUniqueName(bgUpdateCheckTaskName); | ||||
|       } else { | ||||
|         Workmanager().registerPeriodicTask( | ||||
|             bgUpdateCheckTaskName, bgUpdateCheckTaskName, | ||||
|             frequency: Duration(minutes: settingsProvider.updateInterval), | ||||
|             initialDelay: Duration(minutes: settingsProvider.updateInterval), | ||||
|             constraints: Constraints(networkType: NetworkType.connected), | ||||
|             existingWorkPolicy: ExistingWorkPolicy.keep, | ||||
|             backoffPolicy: BackoffPolicy.linear, | ||||
|             backoffPolicyDelay: | ||||
|                 const Duration(minutes: minUpdateIntervalMinutes)); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return DynamicColorBuilder( | ||||
|   | ||||
| @@ -15,8 +15,8 @@ class GitHubStars implements MassAppSource { | ||||
|     if (args.length != requiredArgs.length) { | ||||
|       throw 'Wrong number of arguments provided'; | ||||
|     } | ||||
|     Response res = | ||||
|         await get(Uri.parse('https://api.github.com/users/${args[0]}/starred')); | ||||
|     Response res = await get(Uri.parse( | ||||
|         'https://api.github.com/users/${args[0]}/starred?per_page=100')); //TODO: Make requests for more pages until you run out | ||||
|     if (res.statusCode == 200) { | ||||
|       return (jsonDecode(res.body) as List<dynamic>) | ||||
|           .map((e) => e['html_url'] as String) | ||||
|   | ||||
| @@ -85,6 +85,15 @@ class _AppPageState extends State<AppPage> { | ||||
|                   textAlign: TextAlign.center, | ||||
|                   style: Theme.of(context).textTheme.bodyLarge, | ||||
|                 ), | ||||
|                 const SizedBox( | ||||
|                   height: 32, | ||||
|                 ), | ||||
|                 Text( | ||||
|                   'Last Update Check: ${app?.app.lastUpdateCheck == null ? 'Never' : '\n${app?.app.lastUpdateCheck?.toLocal()}'}', | ||||
|                   textAlign: TextAlign.center, | ||||
|                   style: const TextStyle( | ||||
|                       fontStyle: FontStyle.italic, fontSize: 12), | ||||
|                 ) | ||||
|               ], | ||||
|             ), | ||||
|       bottomSheet: Padding( | ||||
|   | ||||
| @@ -40,7 +40,8 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|  | ||||
|     Future<List<List<String>>> addApps(List<String> urls) async { | ||||
|       await settingsProvider.getInstallPermission(); | ||||
|       List<dynamic> results = await sourceProvider.getApps(urls); | ||||
|       List<dynamic> results = await sourceProvider.getApps(urls, | ||||
|           ignoreUrls: appsProvider.apps.values.map((e) => e.app.url).toList()); | ||||
|       List<App> apps = results[0]; | ||||
|       Map<String, dynamic> errorsMap = results[1]; | ||||
|       for (var app in apps) { | ||||
|   | ||||
| @@ -169,41 +169,41 @@ class _SettingsPageState extends State<SettingsPage> { | ||||
|                                     labelText: | ||||
|                                         'Background Update Checking Interval'), | ||||
|                                 value: settingsProvider.updateInterval, | ||||
|                                 items: const [ | ||||
|                                   DropdownMenuItem( | ||||
|                                     value: 15, | ||||
|                                     child: Text('15 Minutes'), | ||||
|                                   ), | ||||
|                                   DropdownMenuItem( | ||||
|                                     value: 30, | ||||
|                                     child: Text('30 Minutes'), | ||||
|                                   ), | ||||
|                                   DropdownMenuItem( | ||||
|                                     value: 60, | ||||
|                                     child: Text('1 Hour'), | ||||
|                                   ), | ||||
|                                   DropdownMenuItem( | ||||
|                                     value: 360, | ||||
|                                     child: Text('6 Hours'), | ||||
|                                   ), | ||||
|                                   DropdownMenuItem( | ||||
|                                     value: 720, | ||||
|                                     child: Text('12 Hours'), | ||||
|                                   ), | ||||
|                                   DropdownMenuItem( | ||||
|                                     value: 1440, | ||||
|                                     child: Text('1 Day'), | ||||
|                                   ), | ||||
|                                   DropdownMenuItem( | ||||
|                                     value: 0, | ||||
|                                     child: Text('Never - Manual Only'), | ||||
|                                   ), | ||||
|                                 ], | ||||
|                                 items: updateIntervals.map((e) { | ||||
|                                   int displayNum = (e < 60 | ||||
|                                           ? e | ||||
|                                           : e < 1440 | ||||
|                                               ? e / 60 | ||||
|                                               : e / 1440) | ||||
|                                       .round(); | ||||
|                                   var displayUnit = (e < 60 | ||||
|                                       ? 'Minute' | ||||
|                                       : e < 1440 | ||||
|                                           ? 'Hour' | ||||
|                                           : 'Day'); | ||||
|  | ||||
|                                   String display = e == 0 | ||||
|                                       ? 'Never - Manual Only' | ||||
|                                       : '$displayNum $displayUnit${displayNum == 1 ? '' : 's'}'; | ||||
|                                   return DropdownMenuItem( | ||||
|                                       value: e, child: Text(display)); | ||||
|                                 }).toList(), | ||||
|                                 onChanged: (value) { | ||||
|                                   if (value != null) { | ||||
|                                     settingsProvider.updateInterval = value; | ||||
|                                   } | ||||
|                                 }), | ||||
|                             const SizedBox( | ||||
|                               height: 8, | ||||
|                             ), | ||||
|                             Text( | ||||
|                               'Large App collections may require multiple cycles', | ||||
|                               style: Theme.of(context) | ||||
|                                   .textTheme | ||||
|                                   .labelMedium! | ||||
|                                   .merge(const TextStyle( | ||||
|                                       fontStyle: FontStyle.italic)), | ||||
|                             ), | ||||
|                             const Spacer(), | ||||
|                             Row( | ||||
|                               mainAxisAlignment: MainAxisAlignment.center, | ||||
|   | ||||
| @@ -297,12 +297,23 @@ class AppsProvider with ChangeNotifier { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   Future<List<App>> checkUpdates() async { | ||||
|   Future<List<App>> checkUpdates({DateTime? ignoreAfter}) async { | ||||
|     List<App> updates = []; | ||||
|     if (!gettingUpdates) { | ||||
|       gettingUpdates = true; | ||||
|  | ||||
|       List<String> appIds = apps.keys.toList(); | ||||
|       if (ignoreAfter != null) { | ||||
|         appIds = appIds | ||||
|             .where((id) => | ||||
|                 apps[id]!.app.lastUpdateCheck != null && | ||||
|                 apps[id]!.app.lastUpdateCheck!.isBefore(ignoreAfter)) | ||||
|             .toList(); | ||||
|       } | ||||
|       appIds.sort((a, b) => (apps[a]!.app.lastUpdateCheck ?? | ||||
|               DateTime.fromMicrosecondsSinceEpoch(0)) | ||||
|           .compareTo(apps[b]!.app.lastUpdateCheck ?? | ||||
|               DateTime.fromMicrosecondsSinceEpoch(0))); | ||||
|       for (int i = 0; i < appIds.length; i++) { | ||||
|         App? newApp = await getUpdate(appIds[i]); | ||||
|         if (newApp != null) { | ||||
|   | ||||
| @@ -13,6 +13,16 @@ enum SortColumnSettings { added, nameAuthor, authorName } | ||||
|  | ||||
| enum SortOrderSettings { ascending, descending } | ||||
|  | ||||
| const maxAPIRateLimitMinutes = 30; | ||||
| const minUpdateIntervalMinutes = maxAPIRateLimitMinutes + 30; | ||||
| const maxUpdateIntervalMinutes = 4320; | ||||
| List<int> updateIntervals = [15, 30, 60, 120, 180, 360, 720, 1440, 4320, 0] | ||||
|     .where((element) => | ||||
|         (element >= minUpdateIntervalMinutes && | ||||
|             element <= maxUpdateIntervalMinutes) || | ||||
|         element == 0) | ||||
|     .toList(); | ||||
|  | ||||
| class SettingsProvider with ChangeNotifier { | ||||
|   SharedPreferences? prefs; | ||||
|  | ||||
| @@ -45,7 +55,17 @@ class SettingsProvider with ChangeNotifier { | ||||
|   } | ||||
|  | ||||
|   int get updateInterval { | ||||
|     return prefs?.getInt('updateInterval') ?? 1440; | ||||
|     var min = prefs?.getInt('updateInterval') ?? 180; | ||||
|     if (!updateIntervals.contains(min)) { | ||||
|       var temp = updateIntervals[0]; | ||||
|       for (var i in updateIntervals) { | ||||
|         if (min > i && i != 0) { | ||||
|           temp = i; | ||||
|         } | ||||
|       } | ||||
|       min = temp; | ||||
|     } | ||||
|     return min; | ||||
|   } | ||||
|  | ||||
|   set updateInterval(int min) { | ||||
|   | ||||
| @@ -38,6 +38,7 @@ class App { | ||||
|   List<String> apkUrls = []; | ||||
|   late int preferredApkIndex; | ||||
|   late List<String> additionalData; | ||||
|   late DateTime? lastUpdateCheck; | ||||
|   App( | ||||
|       this.id, | ||||
|       this.url, | ||||
| @@ -47,7 +48,8 @@ class App { | ||||
|       this.latestVersion, | ||||
|       this.apkUrls, | ||||
|       this.preferredApkIndex, | ||||
|       this.additionalData); | ||||
|       this.additionalData, | ||||
|       this.lastUpdateCheck); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
| @@ -69,7 +71,10 @@ class App { | ||||
|       json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int, | ||||
|       json['additionalData'] == null | ||||
|           ? SourceProvider().getSource(json['url']).additionalDataDefaults | ||||
|           : List<String>.from(jsonDecode(json['additionalData']))); | ||||
|           : List<String>.from(jsonDecode(json['additionalData'])), | ||||
|       json['lastUpdateCheck'] == null | ||||
|           ? null | ||||
|           : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck'])); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         'id': id, | ||||
| @@ -80,7 +85,8 @@ class App { | ||||
|         'latestVersion': latestVersion, | ||||
|         'apkUrls': jsonEncode(apkUrls), | ||||
|         'preferredApkIndex': preferredApkIndex, | ||||
|         'additionalData': jsonEncode(additionalData) | ||||
|         'additionalData': jsonEncode(additionalData), | ||||
|         'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch | ||||
|       }; | ||||
| } | ||||
|  | ||||
| @@ -195,15 +201,17 @@ class SourceProvider { | ||||
|         apk.version, | ||||
|         apk.apkUrls, | ||||
|         apk.apkUrls.length - 1, | ||||
|         additionalData); | ||||
|         additionalData, | ||||
|         DateTime.now()); | ||||
|   } | ||||
|  | ||||
|   /// Returns a length 2 list, where the first element is a list of Apps and | ||||
|   /// the second is a Map<String, dynamic> of URLs and errors | ||||
|   Future<List<dynamic>> getApps(List<String> urls) async { | ||||
|   Future<List<dynamic>> getApps(List<String> urls, | ||||
|       {List<String> ignoreUrls = const []}) async { | ||||
|     List<App> apps = []; | ||||
|     Map<String, dynamic> errors = {}; | ||||
|     for (var url in urls) { | ||||
|     for (var url in urls.where((element) => !ignoreUrls.contains(element))) { | ||||
|       try { | ||||
|         var source = getSource(url); | ||||
|         apps.add(await getApp(source, url, source.additionalDataDefaults)); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user