Switched to package names as app ids (#69)
* Fixes #14 (although detection is disabled in background processes due to the bug described in #60) * Added App icons and basic installed detection * Real Package Names Used as IDs + App Icons (INCONVENIENT FOR PREVIOUS VERSION USERS) * Switch to using extracted names (no custom names) * Fixes #57 * Fixes #67 * Fixes #64 * Fixes #61 * Commented out APKMirror and added code to remove their Apps * Updated README * Switched to Flutter stable (causes some UI elements to switch back to the old material design style, but this will be fixed in later Flutter releases) * BG task silently retries on network errors * Updated screenshots
| @@ -13,7 +13,6 @@ Currently supported App sources: | |||||||
| - [IzzyOnDroid](https://android.izzysoft.de/) | - [IzzyOnDroid](https://android.izzysoft.de/) | ||||||
| - [Mullvad](https://mullvad.net/en/) | - [Mullvad](https://mullvad.net/en/) | ||||||
| - [Signal](https://signal.org/) | - [Signal](https://signal.org/) | ||||||
| - [APKMirror](https://apkmirror.com/) |  | ||||||
|  |  | ||||||
| ## Limitations | ## Limitations | ||||||
| - App installs are assumed to have succeeded; failures and cancelled installs cannot be detected. | - App installs are assumed to have succeeded; failures and cancelled installs cannot be detected. | ||||||
|   | |||||||
| Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 228 KiB | 
| Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 162 KiB | 
| Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 170 KiB | 
| Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 146 KiB | 
| Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 188 KiB | 
| Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 192 KiB | 
| @@ -1,6 +1,7 @@ | |||||||
|  | import 'dart:io'; | ||||||
|  |  | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter/services.dart'; | import 'package:flutter/services.dart'; | ||||||
| import 'package:obtainium/app_sources/github.dart'; |  | ||||||
| import 'package:obtainium/custom_errors.dart'; | import 'package:obtainium/custom_errors.dart'; | ||||||
| import 'package:obtainium/pages/home.dart'; | import 'package:obtainium/pages/home.dart'; | ||||||
| import 'package:obtainium/providers/apps_provider.dart'; | import 'package:obtainium/providers/apps_provider.dart'; | ||||||
| @@ -13,12 +14,14 @@ import 'package:workmanager/workmanager.dart'; | |||||||
| import 'package:dynamic_color/dynamic_color.dart'; | import 'package:dynamic_color/dynamic_color.dart'; | ||||||
| import 'package:device_info_plus/device_info_plus.dart'; | import 'package:device_info_plus/device_info_plus.dart'; | ||||||
|  |  | ||||||
|  | const String currentVersion = '0.6.0'; | ||||||
| const String currentReleaseTag = | const String currentReleaseTag = | ||||||
|     'v0.5.10-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES |     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES | ||||||
|  |  | ||||||
| const String bgUpdateCheckTaskName = 'bg-update-check'; | const String bgUpdateCheckTaskName = 'bg-update-check'; | ||||||
|  |  | ||||||
| bgUpdateCheck(int? ignoreAfterMicroseconds) async { | bgUpdateCheck(int? ignoreAfterMicroseconds) async { | ||||||
|  |   WidgetsFlutterBinding.ensureInitialized(); | ||||||
|   DateTime? ignoreAfter = ignoreAfterMicroseconds != null |   DateTime? ignoreAfter = ignoreAfterMicroseconds != null | ||||||
|       ? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds) |       ? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds) | ||||||
|       : null; |       : null; | ||||||
| @@ -27,21 +30,25 @@ bgUpdateCheck(int? ignoreAfterMicroseconds) async { | |||||||
|   try { |   try { | ||||||
|     var appsProvider = AppsProvider(); |     var appsProvider = AppsProvider(); | ||||||
|     await notificationsProvider.cancel(ErrorCheckingUpdatesNotification('').id); |     await notificationsProvider.cancel(ErrorCheckingUpdatesNotification('').id); | ||||||
|     await appsProvider.loadApps(); |     await appsProvider.loadApps(shouldCorrectInstallStatus: false); | ||||||
|     List<String> existingUpdateIds = |     List<String> existingUpdateIds = | ||||||
|         appsProvider.getExistingUpdates(installedOnly: true); |         appsProvider.getExistingUpdates(installedOnly: true); | ||||||
|     DateTime nextIgnoreAfter = DateTime.now(); |     DateTime nextIgnoreAfter = DateTime.now(); | ||||||
|     String? err; |     String? err; | ||||||
|     try { |     try { | ||||||
|       await appsProvider.checkUpdates( |       await appsProvider.checkUpdates( | ||||||
|           ignoreAfter: ignoreAfter, immediatelyThrowRateLimitError: true); |           ignoreAfter: ignoreAfter, | ||||||
|  |           immediatelyThrowRateLimitError: true, | ||||||
|  |           immediatelyThrowSocketError: true, | ||||||
|  |           shouldCorrectInstallStatus: false); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       if (e is RateLimitError) { |       if (e is RateLimitError || e is SocketException) { | ||||||
|         String nextTaskName = |         String nextTaskName = | ||||||
|             '$bgUpdateCheckTaskName-${nextIgnoreAfter.microsecondsSinceEpoch.toString()}'; |             '$bgUpdateCheckTaskName-${nextIgnoreAfter.microsecondsSinceEpoch.toString()}'; | ||||||
|         Workmanager().registerOneOffTask(nextTaskName, nextTaskName, |         Workmanager().registerOneOffTask(nextTaskName, nextTaskName, | ||||||
|             constraints: Constraints(networkType: NetworkType.connected), |             constraints: Constraints(networkType: NetworkType.connected), | ||||||
|             initialDelay: Duration(minutes: e.remainingMinutes), |             initialDelay: Duration( | ||||||
|  |                 minutes: e is RateLimitError ? e.remainingMinutes : 15), | ||||||
|             inputData: {'ignoreAfter': nextIgnoreAfter.microsecondsSinceEpoch}); |             inputData: {'ignoreAfter': nextIgnoreAfter.microsecondsSinceEpoch}); | ||||||
|       } else { |       } else { | ||||||
|         err = e.toString(); |         err = e.toString(); | ||||||
| @@ -68,16 +75,15 @@ bgUpdateCheck(int? ignoreAfterMicroseconds) async { | |||||||
|     // } |     // } | ||||||
|  |  | ||||||
|     if (newUpdates.isNotEmpty) { |     if (newUpdates.isNotEmpty) { | ||||||
|       notificationsProvider.notify(UpdateNotification(newUpdates), |       notificationsProvider.notify(UpdateNotification(newUpdates)); | ||||||
|           cancelExisting: true); |  | ||||||
|     } |     } | ||||||
|     if (err != null) { |     if (err != null) { | ||||||
|       throw err; |       throw err; | ||||||
|     } |     } | ||||||
|     return Future.value(true); |     return Future.value(true); | ||||||
|   } catch (e) { |   } catch (e) { | ||||||
|     notificationsProvider.notify(ErrorCheckingUpdatesNotification(e.toString()), |     notificationsProvider | ||||||
|         cancelExisting: true); |         .notify(ErrorCheckingUpdatesNotification(e.toString())); | ||||||
|     return Future.error(false); |     return Future.error(false); | ||||||
|   } finally { |   } finally { | ||||||
|     await notificationsProvider.cancel(checkingUpdatesNotification.id); |     await notificationsProvider.cancel(checkingUpdatesNotification.id); | ||||||
| @@ -94,7 +100,7 @@ void bgTaskCallback() { | |||||||
|  |  | ||||||
| void main() async { | void main() async { | ||||||
|   WidgetsFlutterBinding.ensureInitialized(); |   WidgetsFlutterBinding.ensureInitialized(); | ||||||
|   if ((await DeviceInfoPlugin().androidInfo).version.sdkInt! >= 29) { |   if ((await DeviceInfoPlugin().androidInfo).version.sdkInt >= 29) { | ||||||
|     SystemChrome.setSystemUIOverlayStyle( |     SystemChrome.setSystemUIOverlayStyle( | ||||||
|       const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent), |       const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent), | ||||||
|     ); |     ); | ||||||
| @@ -143,7 +149,7 @@ class _ObtainiumState extends State<Obtainium> { | |||||||
|         Permission.notification.request(); |         Permission.notification.request(); | ||||||
|         appsProvider.saveApps([ |         appsProvider.saveApps([ | ||||||
|           App( |           App( | ||||||
|               'imranr98_obtainium_${GitHub().host}', |               'dev.imranr.obtainium', | ||||||
|               'https://github.com/ImranR98/Obtainium', |               'https://github.com/ImranR98/Obtainium', | ||||||
|               'ImranR98', |               'ImranR98', | ||||||
|               'Obtainium', |               'Obtainium', | ||||||
|   | |||||||
| @@ -22,7 +22,6 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|   String userInput = ''; |   String userInput = ''; | ||||||
|   AppSource? pickedSource; |   AppSource? pickedSource; | ||||||
|   List<String> additionalData = []; |   List<String> additionalData = []; | ||||||
|   String customName = ''; |  | ||||||
|   bool validAdditionalData = true; |   bool validAdditionalData = true; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -80,9 +79,6 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                                                 .doesSourceHaveRequiredAdditionalData( |                                                 .doesSourceHaveRequiredAdditionalData( | ||||||
|                                                     source) |                                                     source) | ||||||
|                                             : true; |                                             : true; | ||||||
|                                         if (source == null) { |  | ||||||
|                                           customName = ''; |  | ||||||
|                                         } |  | ||||||
|                                       } |                                       } | ||||||
|                                     }); |                                     }); | ||||||
|                                   }, |                                   }, | ||||||
| @@ -90,56 +86,72 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                           const SizedBox( |                           const SizedBox( | ||||||
|                             width: 16, |                             width: 16, | ||||||
|                           ), |                           ), | ||||||
|                           ElevatedButton( |                           gettingAppInfo | ||||||
|                               onPressed: gettingAppInfo || |                               ? const CircularProgressIndicator() | ||||||
|                                       pickedSource == null || |                               : ElevatedButton( | ||||||
|                                       (pickedSource!.additionalDataFormItems |                                   onPressed: gettingAppInfo || | ||||||
|                                               .isNotEmpty && |                                           pickedSource == null || | ||||||
|                                           !validAdditionalData) |                                           (pickedSource!.additionalDataFormItems | ||||||
|                                   ? null |                                                   .isNotEmpty && | ||||||
|                                   : () { |                                               !validAdditionalData) | ||||||
|                                       HapticFeedback.selectionClick(); |                                       ? null | ||||||
|                                       setState(() { |                                       : () async { | ||||||
|                                         gettingAppInfo = true; |                                           setState(() { | ||||||
|                                       }); |                                             gettingAppInfo = true; | ||||||
|                                       sourceProvider |                                           }); | ||||||
|                                           .getApp(pickedSource!, userInput, |                                           var appsProvider = | ||||||
|                                               additionalData, |                                               context.read<AppsProvider>(); | ||||||
|                                               customName: customName) |                                           var settingsProvider = | ||||||
|                                           .then((app) { |                                               context.read<SettingsProvider>(); | ||||||
|                                         var appsProvider = |                                           () async { | ||||||
|                                             context.read<AppsProvider>(); |                                             HapticFeedback.selectionClick(); | ||||||
|                                         var settingsProvider = |                                             App app = | ||||||
|                                             context.read<SettingsProvider>(); |                                                 await sourceProvider.getApp( | ||||||
|                                         if (appsProvider.apps |                                                     pickedSource!, | ||||||
|                                             .containsKey(app.id)) { |                                                     userInput, | ||||||
|                                           throw 'App already added'; |                                                     additionalData); | ||||||
|                                         } |                                             await settingsProvider | ||||||
|                                         settingsProvider |                                                 .getInstallPermission(); | ||||||
|                                             .getInstallPermission() |                                             // ignore: use_build_context_synchronously | ||||||
|                                             .then((_) { |                                             var apkUrl = await appsProvider | ||||||
|                                           appsProvider |                                                 .selectApkUrl(app, context); | ||||||
|                                               .saveApps([app]).then((_) { |                                             if (apkUrl == null) { | ||||||
|  |                                               throw 'Cancelled'; | ||||||
|  |                                             } | ||||||
|  |                                             app.preferredApkIndex = | ||||||
|  |                                                 app.apkUrls.indexOf(apkUrl); | ||||||
|  |                                             var downloadedApk = | ||||||
|  |                                                 await appsProvider | ||||||
|  |                                                     .downloadApp(app); | ||||||
|  |                                             app.id = downloadedApk.appId; | ||||||
|  |                                             if (appsProvider.apps | ||||||
|  |                                                 .containsKey(app.id)) { | ||||||
|  |                                               throw 'App already added'; | ||||||
|  |                                             } | ||||||
|  |                                             await appsProvider.saveApps([app]); | ||||||
|  |  | ||||||
|  |                                             return app; | ||||||
|  |                                           }() | ||||||
|  |                                               .then((app) { | ||||||
|                                             Navigator.push( |                                             Navigator.push( | ||||||
|                                                 context, |                                                 context, | ||||||
|                                                 MaterialPageRoute( |                                                 MaterialPageRoute( | ||||||
|                                                     builder: (context) => |                                                     builder: (context) => | ||||||
|                                                         AppPage( |                                                         AppPage( | ||||||
|                                                             appId: app.id))); |                                                             appId: app.id))); | ||||||
|  |                                           }).catchError((e) { | ||||||
|  |                                             ScaffoldMessenger.of(context) | ||||||
|  |                                                 .showSnackBar( | ||||||
|  |                                               SnackBar( | ||||||
|  |                                                   content: Text(e.toString())), | ||||||
|  |                                             ); | ||||||
|  |                                           }).whenComplete(() { | ||||||
|  |                                             setState(() { | ||||||
|  |                                               gettingAppInfo = false; | ||||||
|  |                                             }); | ||||||
|                                           }); |                                           }); | ||||||
|                                         }); |                                         }, | ||||||
|                                       }).catchError((e) { |                                   child: const Text('Add')) | ||||||
|                                         ScaffoldMessenger.of(context) |  | ||||||
|                                             .showSnackBar( |  | ||||||
|                                           SnackBar(content: Text(e.toString())), |  | ||||||
|                                         ); |  | ||||||
|                                       }).whenComplete(() { |  | ||||||
|                                         setState(() { |  | ||||||
|                                           gettingAppInfo = false; |  | ||||||
|                                         }); |  | ||||||
|                                       }); |  | ||||||
|                                     }, |  | ||||||
|                               child: const Text('Add')) |  | ||||||
|                         ], |                         ], | ||||||
|                       ), |                       ), | ||||||
|                       if (pickedSource != null) |                       if (pickedSource != null) | ||||||
| @@ -174,21 +186,6 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                               const SizedBox( |                               const SizedBox( | ||||||
|                                 height: 8, |                                 height: 8, | ||||||
|                               ), |                               ), | ||||||
|                             if (pickedSource != null) |  | ||||||
|                               GeneratedForm( |  | ||||||
|                                   items: [ |  | ||||||
|                                     [ |  | ||||||
|                                       GeneratedFormItem( |  | ||||||
|                                           label: 'Custom App Name', |  | ||||||
|                                           required: false) |  | ||||||
|                                     ] |  | ||||||
|                                   ], |  | ||||||
|                                   onValueChanges: (values, valid) { |  | ||||||
|                                     setState(() { |  | ||||||
|                                       customName = values[0]; |  | ||||||
|                                     }); |  | ||||||
|                                   }, |  | ||||||
|                                   defaultValues: [customName]) |  | ||||||
|                           ], |                           ], | ||||||
|                         ) |                         ) | ||||||
|                       else |                       else | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter/services.dart'; | import 'package:flutter/services.dart'; | ||||||
| import 'package:obtainium/components/generated_form.dart'; |  | ||||||
| import 'package:obtainium/components/generated_form_modal.dart'; | import 'package:obtainium/components/generated_form_modal.dart'; | ||||||
| import 'package:obtainium/providers/apps_provider.dart'; | import 'package:obtainium/providers/apps_provider.dart'; | ||||||
| import 'package:obtainium/providers/settings_provider.dart'; | import 'package:obtainium/providers/settings_provider.dart'; | ||||||
| @@ -46,6 +45,7 @@ class _AppPageState extends State<AppPage> { | |||||||
|       body: RefreshIndicator( |       body: RefreshIndicator( | ||||||
|           child: settingsProvider.showAppWebpage |           child: settingsProvider.showAppWebpage | ||||||
|               ? WebView( |               ? WebView( | ||||||
|  |                   backgroundColor: Theme.of(context).colorScheme.background, | ||||||
|                   initialUrl: app?.app.url, |                   initialUrl: app?.app.url, | ||||||
|                   javascriptMode: JavascriptMode.unrestricted, |                   javascriptMode: JavascriptMode.unrestricted, | ||||||
|                 ) |                 ) | ||||||
| @@ -56,8 +56,18 @@ class _AppPageState extends State<AppPage> { | |||||||
|                       mainAxisAlignment: MainAxisAlignment.center, |                       mainAxisAlignment: MainAxisAlignment.center, | ||||||
|                       crossAxisAlignment: CrossAxisAlignment.stretch, |                       crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|                       children: [ |                       children: [ | ||||||
|  |                         app?.installedInfo != null | ||||||
|  |                             ? Row( | ||||||
|  |                                 mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |                                 children: [ | ||||||
|  |                                     Image.memory( | ||||||
|  |                                       app!.installedInfo!.icon!, | ||||||
|  |                                       scale: 1.5, | ||||||
|  |                                     ) | ||||||
|  |                                   ]) | ||||||
|  |                             : Container(), | ||||||
|                         Text( |                         Text( | ||||||
|                           app?.app.name ?? 'App', |                           app?.installedInfo?.name ?? app?.app.name ?? 'App', | ||||||
|                           textAlign: TextAlign.center, |                           textAlign: TextAlign.center, | ||||||
|                           style: Theme.of(context).textTheme.displayLarge, |                           style: Theme.of(context).textTheme.displayLarge, | ||||||
|                         ), |                         ), | ||||||
| @@ -126,7 +136,8 @@ class _AppPageState extends State<AppPage> { | |||||||
|                   child: Row( |                   child: Row( | ||||||
|                       mainAxisAlignment: MainAxisAlignment.spaceEvenly, |                       mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||||||
|                       children: [ |                       children: [ | ||||||
|                         if (app?.app.installedVersion != app?.app.latestVersion) |                         if (app?.app.installedVersion != null && | ||||||
|  |                             app?.app.installedVersion != app?.app.latestVersion) | ||||||
|                           IconButton( |                           IconButton( | ||||||
|                               onPressed: app?.downloadProgress != null |                               onPressed: app?.downloadProgress != null | ||||||
|                                   ? null |                                   ? null | ||||||
| @@ -135,8 +146,8 @@ class _AppPageState extends State<AppPage> { | |||||||
|                                           context: context, |                                           context: context, | ||||||
|                                           builder: (BuildContext ctx) { |                                           builder: (BuildContext ctx) { | ||||||
|                                             return AlertDialog( |                                             return AlertDialog( | ||||||
|                                               title: Text( |                                               title: const Text( | ||||||
|                                                   'App Already ${app?.app.installedVersion == null ? 'Installed' : 'Updated'}?'), |                                                   'App Already up to Date?'), | ||||||
|                                               actions: [ |                                               actions: [ | ||||||
|                                                 TextButton( |                                                 TextButton( | ||||||
|                                                     onPressed: () { |                                                     onPressed: () { | ||||||
| @@ -161,54 +172,13 @@ class _AppPageState extends State<AppPage> { | |||||||
|                                                           .pop(); |                                                           .pop(); | ||||||
|                                                     }, |                                                     }, | ||||||
|                                                     child: const Text( |                                                     child: const Text( | ||||||
|                                                         'Yes, Mark as Installed')) |                                                         'Yes, Mark as Updated')) | ||||||
|                                               ], |                                               ], | ||||||
|                                             ); |                                             ); | ||||||
|                                           }); |                                           }); | ||||||
|                                     }, |                                     }, | ||||||
|                               tooltip: 'Mark as Installed', |                               tooltip: 'Mark as Updated', | ||||||
|                               icon: const Icon(Icons.done)) |                               icon: const Icon(Icons.done)), | ||||||
|                         else |  | ||||||
|                           IconButton( |  | ||||||
|                               onPressed: app?.downloadProgress != null |  | ||||||
|                                   ? null |  | ||||||
|                                   : () { |  | ||||||
|                                       showDialog( |  | ||||||
|                                           context: context, |  | ||||||
|                                           builder: (BuildContext ctx) { |  | ||||||
|                                             return AlertDialog( |  | ||||||
|                                               title: const Text( |  | ||||||
|                                                   'App Not Installed?'), |  | ||||||
|                                               actions: [ |  | ||||||
|                                                 TextButton( |  | ||||||
|                                                     onPressed: () { |  | ||||||
|                                                       Navigator.of(context) |  | ||||||
|                                                           .pop(); |  | ||||||
|                                                     }, |  | ||||||
|                                                     child: const Text('No')), |  | ||||||
|                                                 TextButton( |  | ||||||
|                                                     onPressed: () { |  | ||||||
|                                                       HapticFeedback |  | ||||||
|                                                           .selectionClick(); |  | ||||||
|                                                       var updatedApp = app?.app; |  | ||||||
|                                                       if (updatedApp != null) { |  | ||||||
|                                                         updatedApp |  | ||||||
|                                                                 .installedVersion = |  | ||||||
|                                                             null; |  | ||||||
|                                                         appsProvider.saveApps( |  | ||||||
|                                                             [updatedApp]); |  | ||||||
|                                                       } |  | ||||||
|                                                       Navigator.of(context) |  | ||||||
|                                                           .pop(); |  | ||||||
|                                                     }, |  | ||||||
|                                                     child: const Text( |  | ||||||
|                                                         'Yes, Mark as Not Installed')) |  | ||||||
|                                               ], |  | ||||||
|                                             ); |  | ||||||
|                                           }); |  | ||||||
|                                     }, |  | ||||||
|                               tooltip: 'Mark as Not Installed', |  | ||||||
|                               icon: const Icon(Icons.no_cell_outlined)), |  | ||||||
|                         if (source != null && |                         if (source != null && | ||||||
|                             source.additionalDataFormItems.isNotEmpty) |                             source.additionalDataFormItems.isNotEmpty) | ||||||
|                           IconButton( |                           IconButton( | ||||||
| @@ -220,30 +190,15 @@ class _AppPageState extends State<AppPage> { | |||||||
|                                           builder: (BuildContext ctx) { |                                           builder: (BuildContext ctx) { | ||||||
|                                             return GeneratedFormModal( |                                             return GeneratedFormModal( | ||||||
|                                                 title: 'Additional Options', |                                                 title: 'Additional Options', | ||||||
|                                                 items: [ |                                                 items: source | ||||||
|                                                   ...source |                                                     .additionalDataFormItems, | ||||||
|                                                       .additionalDataFormItems, |  | ||||||
|                                                   [ |  | ||||||
|                                                     GeneratedFormItem( |  | ||||||
|                                                         label: 'App Name', |  | ||||||
|                                                         required: true) |  | ||||||
|                                                   ] |  | ||||||
|                                                 ], |  | ||||||
|                                                 defaultValues: app != null |                                                 defaultValues: app != null | ||||||
|                                                     ? [ |                                                     ? app.app.additionalData | ||||||
|                                                         ...app |                                                     : source | ||||||
|                                                             .app.additionalData, |                                                         .additionalDataDefaults); | ||||||
|                                                         app.app.name |  | ||||||
|                                                       ] |  | ||||||
|                                                     : [ |  | ||||||
|                                                         ...source |  | ||||||
|                                                             .additionalDataDefaults |  | ||||||
|                                                       ]); |  | ||||||
|                                           }).then((values) { |                                           }).then((values) { | ||||||
|                                         if (app != null && values != null) { |                                         if (app != null && values != null) { | ||||||
|                                           var changedApp = app.app; |                                           var changedApp = app.app; | ||||||
|                                           var name = values.removeLast(); |  | ||||||
|                                           changedApp.name = name; |  | ||||||
|                                           changedApp.additionalData = values; |                                           changedApp.additionalData = values; | ||||||
|                                           appsProvider.saveApps( |                                           appsProvider.saveApps( | ||||||
|                                               [changedApp]).then((value) { |                                               [changedApp]).then((value) { | ||||||
| @@ -265,12 +220,18 @@ class _AppPageState extends State<AppPage> { | |||||||
|                                     ? () { |                                     ? () { | ||||||
|                                         HapticFeedback.heavyImpact(); |                                         HapticFeedback.heavyImpact(); | ||||||
|                                         appsProvider |                                         appsProvider | ||||||
|                                             .downloadAndInstallLatestApp( |                                             .downloadAndInstallLatestApps( | ||||||
|                                                 [app!.app.id], |                                                 [app!.app.id], | ||||||
|                                                 context).then((res) { |                                                 context).then((res) { | ||||||
|                                           if (res.isNotEmpty && mounted) { |                                           if (res.isNotEmpty && mounted) { | ||||||
|                                             Navigator.of(context).pop(); |                                             Navigator.of(context).pop(); | ||||||
|                                           } |                                           } | ||||||
|  |                                         }).catchError((e) { | ||||||
|  |                                           ScaffoldMessenger.of(context) | ||||||
|  |                                               .showSnackBar( | ||||||
|  |                                             SnackBar( | ||||||
|  |                                                 content: Text(e.toString())), | ||||||
|  |                                           ); | ||||||
|                                         }); |                                         }); | ||||||
|                                       } |                                       } | ||||||
|                                     : null, |                                     : null, | ||||||
| @@ -288,7 +249,7 @@ class _AppPageState extends State<AppPage> { | |||||||
|                                         return AlertDialog( |                                         return AlertDialog( | ||||||
|                                           title: const Text('Remove App?'), |                                           title: const Text('Remove App?'), | ||||||
|                                           content: Text( |                                           content: Text( | ||||||
|                                               'This will remove \'${app?.app.name}\' from Obtainium.${app?.app.installedVersion != null ? '\n\nNote that while Obtainium will no longer track its updates, the App will remain installed.' : ''}'), |                                               'This will remove \'${app?.installedInfo?.name ?? app?.app.name}\' from Obtainium.${app?.app.installedVersion != null ? '\n\nNote that while Obtainium will no longer track its updates, the App will remain installed.' : ''}'), | ||||||
|                                           actions: [ |                                           actions: [ | ||||||
|                                             TextButton( |                                             TextButton( | ||||||
|                                                 onPressed: () { |                                                 onPressed: () { | ||||||
|   | |||||||
| @@ -89,7 +89,8 @@ class AppsPageState extends State<AppsPage> { | |||||||
|             .toList(); |             .toList(); | ||||||
|  |  | ||||||
|         for (var t in nameTokens) { |         for (var t in nameTokens) { | ||||||
|           if (!app.app.name.toLowerCase().contains(t.toLowerCase())) { |           var name = app.installedInfo?.name ?? app.app.name; | ||||||
|  |           if (!name.toLowerCase().contains(t.toLowerCase())) { | ||||||
|             return false; |             return false; | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| @@ -103,13 +104,13 @@ class AppsPageState extends State<AppsPage> { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     sortedApps.sort((a, b) { |     sortedApps.sort((a, b) { | ||||||
|  |       var nameA = a.installedInfo?.name ?? a.app.name; | ||||||
|  |       var nameB = b.installedInfo?.name ?? b.app.name; | ||||||
|       int result = 0; |       int result = 0; | ||||||
|       if (settingsProvider.sortColumn == SortColumnSettings.authorName) { |       if (settingsProvider.sortColumn == SortColumnSettings.authorName) { | ||||||
|         result = |         result = (a.app.author + nameA).compareTo(b.app.author + nameB); | ||||||
|             (a.app.author + a.app.name).compareTo(b.app.author + b.app.name); |  | ||||||
|       } else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) { |       } else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) { | ||||||
|         result = |         result = (nameA + a.app.author).compareTo(nameB + b.app.author); | ||||||
|             (a.app.name + a.app.author).compareTo(b.app.name + b.app.author); |  | ||||||
|       } |       } | ||||||
|       return result; |       return result; | ||||||
|     }); |     }); | ||||||
| @@ -166,7 +167,11 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                 onLongPress: () { |                 onLongPress: () { | ||||||
|                   toggleAppSelected(sortedApps[index].app.id); |                   toggleAppSelected(sortedApps[index].app.id); | ||||||
|                 }, |                 }, | ||||||
|                 title: Text(sortedApps[index].app.name), |                 leading: sortedApps[index].installedInfo != null | ||||||
|  |                     ? Image.memory(sortedApps[index].installedInfo!.icon!) | ||||||
|  |                     : null, | ||||||
|  |                 title: Text(sortedApps[index].installedInfo?.name ?? | ||||||
|  |                     sortedApps[index].app.name), | ||||||
|                 subtitle: Text('By ${sortedApps[index].app.author}'), |                 subtitle: Text('By ${sortedApps[index].app.author}'), | ||||||
|                 trailing: sortedApps[index].downloadProgress != null |                 trailing: sortedApps[index].downloadProgress != null | ||||||
|                     ? Text( |                     ? Text( | ||||||
| @@ -178,7 +183,9 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                             mainAxisAlignment: MainAxisAlignment.center, |                             mainAxisAlignment: MainAxisAlignment.center, | ||||||
|                             crossAxisAlignment: CrossAxisAlignment.end, |                             crossAxisAlignment: CrossAxisAlignment.end, | ||||||
|                             children: [ |                             children: [ | ||||||
|                               const Text('Update Available'), |                               Text(appsProvider.areDownloadsRunning() | ||||||
|  |                                   ? 'Please Wait...' | ||||||
|  |                                   : 'Update Available'), | ||||||
|                               SourceProvider() |                               SourceProvider() | ||||||
|                                           .getSource(sortedApps[index].app.url) |                                           .getSource(sortedApps[index].app.url) | ||||||
|                                           .changeLogPageFromStandardUrl( |                                           .changeLogPageFromStandardUrl( | ||||||
| @@ -303,7 +310,7 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                                     message: |                                     message: | ||||||
|                                         '${existingUpdateIdsAllOrSelected.length} update${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'} and ${newInstallIdsAllOrSelected.length} new install${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}.', |                                         '${existingUpdateIdsAllOrSelected.length} update${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'} and ${newInstallIdsAllOrSelected.length} new install${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}.', | ||||||
|                                     items: formInputs, |                                     items: formInputs, | ||||||
|                                     defaultValues: const ['true', 'true'], |                                     defaultValues: const ['true'], | ||||||
|                                     initValid: true, |                                     initValid: true, | ||||||
|                                   ); |                                   ); | ||||||
|                                 }).then((values) { |                                 }).then((values) { | ||||||
| @@ -311,7 +318,7 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                                 bool shouldInstallUpdates = |                                 bool shouldInstallUpdates = | ||||||
|                                     values.length < 2 || values[0] == 'true'; |                                     values.length < 2 || values[0] == 'true'; | ||||||
|                                 bool shouldInstallNew = |                                 bool shouldInstallNew = | ||||||
|                                     values.length < 2 || values[1] == 'true'; |                                     values.length >= 2 && values[1] == 'true'; | ||||||
|                                 settingsProvider |                                 settingsProvider | ||||||
|                                     .getInstallPermission() |                                     .getInstallPermission() | ||||||
|                                     .then((_) { |                                     .then((_) { | ||||||
| @@ -324,8 +331,14 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                                     toInstall |                                     toInstall | ||||||
|                                         .addAll(newInstallIdsAllOrSelected); |                                         .addAll(newInstallIdsAllOrSelected); | ||||||
|                                   } |                                   } | ||||||
|                                   appsProvider.downloadAndInstallLatestApp( |                                   appsProvider | ||||||
|                                       toInstall, context); |                                       .downloadAndInstallLatestApps( | ||||||
|  |                                           toInstall, context) | ||||||
|  |                                       .catchError((e) { | ||||||
|  |                                     ScaffoldMessenger.of(context).showSnackBar( | ||||||
|  |                                       SnackBar(content: Text(e.toString())), | ||||||
|  |                                     ); | ||||||
|  |                                   }); | ||||||
|                                 }); |                                 }); | ||||||
|                               } |                               } | ||||||
|                             }); |                             }); | ||||||
| @@ -349,7 +362,7 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                                     padding: const EdgeInsets.only(top: 6), |                                     padding: const EdgeInsets.only(top: 6), | ||||||
|                                     child: Row( |                                     child: Row( | ||||||
|                                         mainAxisAlignment: |                                         mainAxisAlignment: | ||||||
|                                             MainAxisAlignment.spaceBetween, |                                             MainAxisAlignment.spaceAround, | ||||||
|                                         children: [ |                                         children: [ | ||||||
|                                           IconButton( |                                           IconButton( | ||||||
|                                               onPressed: |                                               onPressed: | ||||||
| @@ -364,7 +377,10 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                                                                       ctx) { |                                                                       ctx) { | ||||||
|                                                                 return AlertDialog( |                                                                 return AlertDialog( | ||||||
|                                                                   title: Text( |                                                                   title: Text( | ||||||
|                                                                       'Mark ${selectedIds.length} Selected Apps as Not Installed?'), |                                                                       'Mark ${selectedIds.length} Selected Apps as Updated?'), | ||||||
|  |                                                                   content: | ||||||
|  |                                                                       const Text( | ||||||
|  |                                                                           'Only applies to installed but out of date Apps.'), | ||||||
|                                                                   actions: [ |                                                                   actions: [ | ||||||
|                                                                     TextButton( |                                                                     TextButton( | ||||||
|                                                                         onPressed: |                                                                         onPressed: | ||||||
| @@ -383,8 +399,10 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                                                                               .saveApps(selectedIds.map((e) { |                                                                               .saveApps(selectedIds.map((e) { | ||||||
|                                                                             var a = |                                                                             var a = | ||||||
|                                                                                 appsProvider.apps[e]!.app; |                                                                                 appsProvider.apps[e]!.app; | ||||||
|                                                                             a.installedVersion = |                                                                             if (a.installedVersion != | ||||||
|                                                                                 null; |                                                                                 null) { | ||||||
|  |                                                                               a.installedVersion = a.latestVersion; | ||||||
|  |                                                                             } | ||||||
|                                                                             return a; |                                                                             return a; | ||||||
|                                                                           }).toList()); |                                                                           }).toList()); | ||||||
|  |  | ||||||
| @@ -398,57 +416,7 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                                                               }); |                                                               }); | ||||||
|                                                         }, |                                                         }, | ||||||
|                                               tooltip: |                                               tooltip: | ||||||
|                                                   'Mark Selected Apps as Not Installed', |                                                   'Mark Selected Apps as Updated', | ||||||
|                                               icon: const Icon( |  | ||||||
|                                                   Icons.no_cell_outlined)), |  | ||||||
|                                           IconButton( |  | ||||||
|                                               onPressed: |  | ||||||
|                                                   appsProvider |  | ||||||
|                                                           .areDownloadsRunning() |  | ||||||
|                                                       ? null |  | ||||||
|                                                       : () { |  | ||||||
|                                                           showDialog( |  | ||||||
|                                                               context: context, |  | ||||||
|                                                               builder: |  | ||||||
|                                                                   (BuildContext |  | ||||||
|                                                                       ctx) { |  | ||||||
|                                                                 return AlertDialog( |  | ||||||
|                                                                   title: Text( |  | ||||||
|                                                                       'Mark ${selectedIds.length} Selected Apps as Installed/Updated?'), |  | ||||||
|                                                                   actions: [ |  | ||||||
|                                                                     TextButton( |  | ||||||
|                                                                         onPressed: |  | ||||||
|                                                                             () { |  | ||||||
|                                                                           Navigator.of(context) |  | ||||||
|                                                                               .pop(); |  | ||||||
|                                                                         }, |  | ||||||
|                                                                         child: const Text( |  | ||||||
|                                                                             'No')), |  | ||||||
|                                                                     TextButton( |  | ||||||
|                                                                         onPressed: |  | ||||||
|                                                                             () { |  | ||||||
|                                                                           HapticFeedback |  | ||||||
|                                                                               .selectionClick(); |  | ||||||
|                                                                           appsProvider |  | ||||||
|                                                                               .saveApps(selectedIds.map((e) { |  | ||||||
|                                                                             var a = |  | ||||||
|                                                                                 appsProvider.apps[e]!.app; |  | ||||||
|                                                                             a.installedVersion = |  | ||||||
|                                                                                 a.latestVersion; |  | ||||||
|                                                                             return a; |  | ||||||
|                                                                           }).toList()); |  | ||||||
|  |  | ||||||
|                                                                           Navigator.of(context) |  | ||||||
|                                                                               .pop(); |  | ||||||
|                                                                         }, |  | ||||||
|                                                                         child: const Text( |  | ||||||
|                                                                             'Yes')) |  | ||||||
|                                                                   ], |  | ||||||
|                                                                 ); |  | ||||||
|                                                               }); |  | ||||||
|                                                         }, |  | ||||||
|                                               tooltip: |  | ||||||
|                                                   'Mark Selected Apps as Installed/Updated', |  | ||||||
|                                               icon: const Icon(Icons.done)), |                                               icon: const Icon(Icons.done)), | ||||||
|                                           IconButton( |                                           IconButton( | ||||||
|                                             onPressed: () { |                                             onPressed: () { | ||||||
|   | |||||||
| @@ -9,9 +9,12 @@ import 'package:device_info_plus/device_info_plus.dart'; | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter/services.dart'; | import 'package:flutter/services.dart'; | ||||||
| import 'package:install_plugin_v2/install_plugin_v2.dart'; | import 'package:install_plugin_v2/install_plugin_v2.dart'; | ||||||
|  | import 'package:installed_apps/app_info.dart'; | ||||||
|  | import 'package:installed_apps/installed_apps.dart'; | ||||||
| import 'package:obtainium/app_sources/github.dart'; | import 'package:obtainium/app_sources/github.dart'; | ||||||
| import 'package:obtainium/custom_errors.dart'; | import 'package:obtainium/custom_errors.dart'; | ||||||
| import 'package:obtainium/providers/notifications_provider.dart'; | import 'package:obtainium/providers/notifications_provider.dart'; | ||||||
|  | import 'package:package_archive_info/package_archive_info.dart'; | ||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
| import 'package:path_provider/path_provider.dart'; | import 'package:path_provider/path_provider.dart'; | ||||||
| import 'package:flutter_fgbg/flutter_fgbg.dart'; | import 'package:flutter_fgbg/flutter_fgbg.dart'; | ||||||
| @@ -21,14 +24,15 @@ import 'package:http/http.dart'; | |||||||
| class AppInMemory { | class AppInMemory { | ||||||
|   late App app; |   late App app; | ||||||
|   double? downloadProgress; |   double? downloadProgress; | ||||||
|  |   AppInfo? installedInfo; // Also indicates that an App is installed | ||||||
|  |  | ||||||
|   AppInMemory(this.app, this.downloadProgress); |   AppInMemory(this.app, this.downloadProgress, this.installedInfo); | ||||||
| } | } | ||||||
|  |  | ||||||
| class ApkFile { | class DownloadedApp { | ||||||
|   String appId; |   String appId; | ||||||
|   File file; |   File file; | ||||||
|   ApkFile(this.appId, this.file); |   DownloadedApp(this.appId, this.file); | ||||||
| } | } | ||||||
|  |  | ||||||
| class AppsProvider with ChangeNotifier { | class AppsProvider with ChangeNotifier { | ||||||
| @@ -39,24 +43,24 @@ class AppsProvider with ChangeNotifier { | |||||||
|  |  | ||||||
|   // Variables to keep track of the app foreground status (installs can't run in the background) |   // Variables to keep track of the app foreground status (installs can't run in the background) | ||||||
|   bool isForeground = true; |   bool isForeground = true; | ||||||
|   late Stream<FGBGType> foregroundStream; |   late Stream<FGBGType>? foregroundStream; | ||||||
|   late StreamSubscription<FGBGType> foregroundSubscription; |   late StreamSubscription<FGBGType>? foregroundSubscription; | ||||||
|  |  | ||||||
|   AppsProvider( |   AppsProvider( | ||||||
|       {bool shouldLoadApps = false, |       {bool shouldLoadApps = false, | ||||||
|       bool shouldCheckUpdatesAfterLoad = false, |       bool shouldCheckUpdatesAfterLoad = false, | ||||||
|       bool shouldDeleteAPKs = false}) { |       bool shouldDeleteAPKs = false}) { | ||||||
|     // Subscribe to changes in the app foreground status |  | ||||||
|     foregroundStream = FGBGEvents.stream.asBroadcastStream(); |  | ||||||
|     foregroundSubscription = foregroundStream.listen((event) async { |  | ||||||
|       isForeground = event == FGBGType.foreground; |  | ||||||
|       if (isForeground) await loadApps(); |  | ||||||
|     }); |  | ||||||
|     if (shouldDeleteAPKs) { |  | ||||||
|       deleteSavedAPKs(); |  | ||||||
|     } |  | ||||||
|     if (shouldLoadApps) { |     if (shouldLoadApps) { | ||||||
|  |       // Subscribe to changes in the app foreground status | ||||||
|  |       foregroundStream = FGBGEvents.stream.asBroadcastStream(); | ||||||
|  |       foregroundSubscription = foregroundStream?.listen((event) async { | ||||||
|  |         isForeground = event == FGBGType.foreground; | ||||||
|  |         if (isForeground) await loadApps(); | ||||||
|  |       }); | ||||||
|       loadApps().then((_) { |       loadApps().then((_) { | ||||||
|  |         if (shouldDeleteAPKs) { | ||||||
|  |           deleteSavedAPKs(); | ||||||
|  |         } | ||||||
|         if (shouldCheckUpdatesAfterLoad) { |         if (shouldCheckUpdatesAfterLoad) { | ||||||
|           checkUpdates(); |           checkUpdates(); | ||||||
|         } |         } | ||||||
| @@ -64,38 +68,85 @@ class AppsProvider with ChangeNotifier { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<ApkFile> downloadApp(String apkUrl, String appId) async { |   downloadApk(String apkUrl, String fileName, Function? onProgress, | ||||||
|     apkUrl = await SourceProvider() |       Function? urlModifier, | ||||||
|         .getSource(apps[appId]!.app.url) |       {bool useExistingIfExists = true}) async { | ||||||
|         .apkUrlPrefetchModifier(apkUrl); |     var destDir = (await getExternalStorageDirectory())!.path; | ||||||
|  |     if (urlModifier != null) { | ||||||
|  |       apkUrl = await urlModifier(apkUrl); | ||||||
|  |     } | ||||||
|     StreamedResponse response = |     StreamedResponse response = | ||||||
|         await Client().send(Request('GET', Uri.parse(apkUrl))); |         await Client().send(Request('GET', Uri.parse(apkUrl))); | ||||||
|     File downloadFile = |     File downloadFile = File('$destDir/$fileName.apk'); | ||||||
|         File('${(await getExternalStorageDirectory())!.path}/$appId.apk'); |     var alreadyExists = downloadFile.existsSync(); | ||||||
|     if (downloadFile.existsSync()) { |     if (!alreadyExists || !useExistingIfExists) { | ||||||
|       downloadFile.deleteSync(); |       if (alreadyExists) { | ||||||
|     } |         downloadFile.deleteSync(); | ||||||
|     var length = response.contentLength; |       } | ||||||
|     var received = 0; |  | ||||||
|     var sink = downloadFile.openWrite(); |  | ||||||
|  |  | ||||||
|     await response.stream.map((s) { |       var length = response.contentLength; | ||||||
|       received += s.length; |       var received = 0; | ||||||
|       apps[appId]!.downloadProgress = |       double? progress; | ||||||
|           (length != null ? received / length * 100 : 30); |       var sink = downloadFile.openWrite(); | ||||||
|  |  | ||||||
|  |       await response.stream.map((s) { | ||||||
|  |         received += s.length; | ||||||
|  |         progress = (length != null ? received / length * 100 : 30); | ||||||
|  |         if (onProgress != null) { | ||||||
|  |           onProgress(progress); | ||||||
|  |         } | ||||||
|  |         return s; | ||||||
|  |       }).pipe(sink); | ||||||
|  |  | ||||||
|  |       await sink.close(); | ||||||
|  |       progress = null; | ||||||
|  |       if (onProgress != null) { | ||||||
|  |         onProgress(progress); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (response.statusCode != 200) { | ||||||
|  |         downloadFile.deleteSync(); | ||||||
|  |         throw response.reasonPhrase ?? 'Unknown Error'; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return downloadFile; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Downloads the App (preferred URL) and returns an ApkFile object | ||||||
|  |   // If the app was already saved, updates it's download progress % in memory | ||||||
|  |   // But also works for Apps that are not saved | ||||||
|  |   Future<DownloadedApp> downloadApp(App app) async { | ||||||
|  |     var fileName = '${app.id}-${app.latestVersion}-${app.preferredApkIndex}'; | ||||||
|  |     File downloadFile = await downloadApk(app.apkUrls[app.preferredApkIndex], | ||||||
|  |         '${app.id}-${app.latestVersion}-${app.preferredApkIndex}', | ||||||
|  |         (double? progress) { | ||||||
|  |       if (apps[app.id] != null) { | ||||||
|  |         apps[app.id]!.downloadProgress = progress; | ||||||
|  |       } | ||||||
|       notifyListeners(); |       notifyListeners(); | ||||||
|       return s; |     }, SourceProvider().getSource(app.url).apkUrlPrefetchModifier); | ||||||
|     }).pipe(sink); |     // Delete older versions of the APK if any | ||||||
|  |     for (var file in downloadFile.parent.listSync()) { | ||||||
|     await sink.close(); |       var fn = file.path.split('/').last; | ||||||
|     apps[appId]!.downloadProgress = null; |       if (fn.startsWith('${app.id}-') && | ||||||
|     notifyListeners(); |           fn.endsWith('.apk') && | ||||||
|  |           fn != '$fileName.apk') { | ||||||
|     if (response.statusCode != 200) { |         file.delete(); | ||||||
|       downloadFile.deleteSync(); |       } | ||||||
|       throw response.reasonPhrase ?? 'Unknown Error'; |  | ||||||
|     } |     } | ||||||
|     return ApkFile(appId, downloadFile); |     // If the ID has changed (as it should on first download), replace it | ||||||
|  |     var newInfo = await PackageArchiveInfo.fromPath(downloadFile.path); | ||||||
|  |     if (app.id != newInfo.packageName) { | ||||||
|  |       var originalAppId = app.id; | ||||||
|  |       app.id = newInfo.packageName; | ||||||
|  |       downloadFile = downloadFile.renameSync( | ||||||
|  |           '${downloadFile.parent.path}/${app.id}-${app.latestVersion}-${app.preferredApkIndex}.apk'); | ||||||
|  |       if (apps[originalAppId] != null) { | ||||||
|  |         await removeApps([originalAppId]); | ||||||
|  |         await saveApps([app]); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return DownloadedApp(app.id, downloadFile); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   bool areDownloadsRunning() => apps.values |   bool areDownloadsRunning() => apps.values | ||||||
| @@ -106,8 +157,8 @@ class AppsProvider with ChangeNotifier { | |||||||
|     // TODO: This is unreliable - try to get from OS in the future |     // TODO: This is unreliable - try to get from OS in the future | ||||||
|     var osInfo = await DeviceInfoPlugin().androidInfo; |     var osInfo = await DeviceInfoPlugin().androidInfo; | ||||||
|     return app.installedVersion != null && |     return app.installedVersion != null && | ||||||
|         osInfo.version.sdkInt! >= 30 && |         osInfo.version.sdkInt >= 30 && | ||||||
|         osInfo.version.release!.compareTo('12') >= 0; |         osInfo.version.release.compareTo('12') >= 0; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> askUserToReturnToForeground(BuildContext context, |   Future<void> askUserToReturnToForeground(BuildContext context, | ||||||
| @@ -128,11 +179,62 @@ class AppsProvider with ChangeNotifier { | |||||||
|   // So we only know that the install prompt was shown, but the user could still cancel w/o us knowing |   // So we only know that the install prompt was shown, but the user could still cancel w/o us knowing | ||||||
|   // If appropriate criteria are met, the update (never a fresh install) happens silently  in the background |   // If appropriate criteria are met, the update (never a fresh install) happens silently  in the background | ||||||
|   // But even then, we don't know if it actually succeeded |   // But even then, we don't know if it actually succeeded | ||||||
|   Future<void> installApk(ApkFile file) async { |   Future<void> installApk(DownloadedApp file) async { | ||||||
|     await InstallPlugin.installApk(file.file.path, 'dev.imranr.obtainium'); |     var newInfo = await PackageArchiveInfo.fromPath(file.file.path); | ||||||
|  |     AppInfo? appInfo; | ||||||
|  |     try { | ||||||
|  |       appInfo = await InstalledApps.getAppInfo(apps[file.appId]!.app.id); | ||||||
|  |     } catch (e) { | ||||||
|  |       // OK | ||||||
|  |     } | ||||||
|  |     if (appInfo != null && | ||||||
|  |         int.parse(newInfo.buildNumber) < appInfo.versionCode!) { | ||||||
|  |       throw 'Can\'t install an older version'; | ||||||
|  |     } | ||||||
|  |     if (appInfo == null || | ||||||
|  |         int.parse(newInfo.buildNumber) > appInfo.versionCode!) { | ||||||
|  |       await InstallPlugin.installApk(file.file.path, 'dev.imranr.obtainium'); | ||||||
|  |     } | ||||||
|     apps[file.appId]!.app.installedVersion = |     apps[file.appId]!.app.installedVersion = | ||||||
|         apps[file.appId]!.app.latestVersion; |         apps[file.appId]!.app.latestVersion; | ||||||
|     await saveApps([apps[file.appId]!.app]); |     // Don't correct install status as installation may not be done yet | ||||||
|  |     await saveApps([apps[file.appId]!.app], shouldCorrectInstallStatus: false); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<String?> selectApkUrl(App app, BuildContext? context) async { | ||||||
|  |     // If the App has more than one APK, the user should pick one (if context provided) | ||||||
|  |     String? apkUrl = app.apkUrls[app.preferredApkIndex]; | ||||||
|  |     if (app.apkUrls.length > 1 && context != null) { | ||||||
|  |       apkUrl = await showDialog( | ||||||
|  |           context: context, | ||||||
|  |           builder: (BuildContext ctx) { | ||||||
|  |             return APKPicker(app: app, initVal: apkUrl); | ||||||
|  |           }); | ||||||
|  |     } | ||||||
|  |     // If the picked APK comes from an origin different from the source, get user confirmation (if context provided) | ||||||
|  |     if (apkUrl != null && | ||||||
|  |         Uri.parse(apkUrl).origin != Uri.parse(app.url).origin && | ||||||
|  |         context != null) { | ||||||
|  |       if (await showDialog( | ||||||
|  |               context: context, | ||||||
|  |               builder: (BuildContext ctx) { | ||||||
|  |                 return APKOriginWarningDialog( | ||||||
|  |                     sourceUrl: app.url, apkUrl: apkUrl!); | ||||||
|  |               }) != | ||||||
|  |           true) { | ||||||
|  |         apkUrl = null; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return apkUrl; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Map<String, List<String>> addToErrorMap( | ||||||
|  |       Map<String, List<String>> errors, String appId, String error) { | ||||||
|  |     var tempIds = errors.remove(error); | ||||||
|  |     tempIds ??= []; | ||||||
|  |     tempIds.add(appId); | ||||||
|  |     errors.putIfAbsent(error, () => tempIds!); | ||||||
|  |     return errors; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Given a list of AppIds, uses stored info about the apps to download APKs and install them |   // Given a list of AppIds, uses stored info about the apps to download APKs and install them | ||||||
| @@ -140,37 +242,16 @@ class AppsProvider with ChangeNotifier { | |||||||
|   // If no BuildContext is provided, apps that require user interaction are ignored |   // If no BuildContext is provided, apps that require user interaction are ignored | ||||||
|   // If user input is needed and the App is in the background, a notification is sent to get the user's attention |   // If user input is needed and the App is in the background, a notification is sent to get the user's attention | ||||||
|   // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result |   // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result | ||||||
|   Future<List<String>> downloadAndInstallLatestApp( |   Future<List<String>> downloadAndInstallLatestApps( | ||||||
|       List<String> appIds, BuildContext? context) async { |       List<String> appIds, BuildContext? context) async { | ||||||
|     Map<String, String> appsToInstall = {}; |     List<String> appsToInstall = []; | ||||||
|     for (var id in appIds) { |     for (var id in appIds) { | ||||||
|       if (apps[id] == null) { |       if (apps[id] == null) { | ||||||
|         throw 'App not found'; |         throw 'App not found'; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // If the App has more than one APK, the user should pick one (if context provided) |       String? apkUrl = await selectApkUrl(apps[id]!.app, context); | ||||||
|       String? apkUrl = apps[id]!.app.apkUrls[apps[id]!.app.preferredApkIndex]; |  | ||||||
|       if (apps[id]!.app.apkUrls.length > 1 && context != null) { |  | ||||||
|         apkUrl = await showDialog( |  | ||||||
|             context: context, |  | ||||||
|             builder: (BuildContext ctx) { |  | ||||||
|               return APKPicker(app: apps[id]!.app, initVal: apkUrl); |  | ||||||
|             }); |  | ||||||
|       } |  | ||||||
|       // If the picked APK comes from an origin different from the source, get user confirmation (if context provided) |  | ||||||
|       if (apkUrl != null && |  | ||||||
|           Uri.parse(apkUrl).origin != Uri.parse(apps[id]!.app.url).origin && |  | ||||||
|           context != null) { |  | ||||||
|         if (await showDialog( |  | ||||||
|                 context: context, |  | ||||||
|                 builder: (BuildContext ctx) { |  | ||||||
|                   return APKOriginWarningDialog( |  | ||||||
|                       sourceUrl: apps[id]!.app.url, apkUrl: apkUrl!); |  | ||||||
|                 }) != |  | ||||||
|             true) { |  | ||||||
|           apkUrl = null; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       if (apkUrl != null) { |       if (apkUrl != null) { | ||||||
|         int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl); |         int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl); | ||||||
|         if (urlInd != apps[id]!.app.preferredApkIndex) { |         if (urlInd != apps[id]!.app.preferredApkIndex) { | ||||||
| @@ -180,18 +261,28 @@ class AppsProvider with ChangeNotifier { | |||||||
|         if (context != null || |         if (context != null || | ||||||
|             (await canInstallSilently(apps[id]!.app) && |             (await canInstallSilently(apps[id]!.app) && | ||||||
|                 apps[id]!.app.apkUrls.length == 1)) { |                 apps[id]!.app.apkUrls.length == 1)) { | ||||||
|           appsToInstall.putIfAbsent(id, () => apkUrl!); |           appsToInstall.add(id); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |     Map<String, List<String>> errors = {}; | ||||||
|  |  | ||||||
|     List<ApkFile> downloadedFiles = await Future.wait(appsToInstall.entries |     List<DownloadedApp?> downloadedFiles = | ||||||
|         .map((entry) => downloadApp(entry.value, entry.key))); |         await Future.wait(appsToInstall.map((id) async { | ||||||
|  |       try { | ||||||
|  |         return await downloadApp(apps[id]!.app); | ||||||
|  |       } catch (e) { | ||||||
|  |         addToErrorMap(errors, id, e.toString()); | ||||||
|  |       } | ||||||
|  |       return null; | ||||||
|  |     })); | ||||||
|  |     downloadedFiles = | ||||||
|  |         downloadedFiles.where((element) => element != null).toList(); | ||||||
|  |  | ||||||
|     List<ApkFile> silentUpdates = []; |     List<DownloadedApp> silentUpdates = []; | ||||||
|     List<ApkFile> regularInstalls = []; |     List<DownloadedApp> regularInstalls = []; | ||||||
|     for (var f in downloadedFiles) { |     for (var f in downloadedFiles) { | ||||||
|       bool willBeSilent = await canInstallSilently(apps[f.appId]!.app); |       bool willBeSilent = await canInstallSilently(apps[f!.appId]!.app); | ||||||
|       if (willBeSilent) { |       if (willBeSilent) { | ||||||
|         silentUpdates.add(f); |         silentUpdates.add(f); | ||||||
|       } else { |       } else { | ||||||
| @@ -200,9 +291,9 @@ class AppsProvider with ChangeNotifier { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // If Obtainium is being installed, it should be the last one |     // If Obtainium is being installed, it should be the last one | ||||||
|     List<ApkFile> moveObtainiumToEnd(List<ApkFile> items) { |     List<DownloadedApp> moveObtainiumToEnd(List<DownloadedApp> items) { | ||||||
|       String obtainiumId = 'imranr98_obtainium_${GitHub().host}'; |       String obtainiumId = 'imranr98_obtainium_${GitHub().host}'; | ||||||
|       ApkFile? temp; |       DownloadedApp? temp; | ||||||
|       items.removeWhere((element) { |       items.removeWhere((element) { | ||||||
|         bool res = element.appId == obtainiumId; |         bool res = element.appId == obtainiumId; | ||||||
|         if (res) { |         if (res) { | ||||||
| @@ -233,11 +324,23 @@ class AppsProvider with ChangeNotifier { | |||||||
|         await askUserToReturnToForeground(context, waitForFG: true); |         await askUserToReturnToForeground(context, waitForFG: true); | ||||||
|       } |       } | ||||||
|       for (var i in regularInstalls) { |       for (var i in regularInstalls) { | ||||||
|         await installApk(i); |         try { | ||||||
|  |           await installApk(i); | ||||||
|  |         } catch (e) { | ||||||
|  |           addToErrorMap(errors, i.appId, e.toString()); | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |     if (errors.isNotEmpty) { | ||||||
|  |       String finalError = ''; | ||||||
|  |       for (var e in errors.keys) { | ||||||
|  |         finalError += | ||||||
|  |             '$e ${errors[e]!.map((e) => apps[e]!.app.name).toString()}. '; | ||||||
|  |       } | ||||||
|  |       throw finalError; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return downloadedFiles.map((e) => e.appId).toList(); |     return downloadedFiles.map((e) => e!.appId).toList(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<Directory> getAppsDir() async { |   Future<Directory> getAppsDir() async { | ||||||
| @@ -249,16 +352,83 @@ class AppsProvider with ChangeNotifier { | |||||||
|     return appsDir; |     return appsDir; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // Delete all stored APKs except those likely to still be needed | ||||||
|   Future<void> deleteSavedAPKs() async { |   Future<void> deleteSavedAPKs() async { | ||||||
|     (await getExternalStorageDirectory()) |     List<FileSystemEntity>? apks = (await getExternalStorageDirectory()) | ||||||
|         ?.listSync() |         ?.listSync() | ||||||
|         .where((element) => element.path.endsWith('.apk')) |         .where((element) => element.path.endsWith('.apk')) | ||||||
|         .forEach((element) { |         .toList(); | ||||||
|       element.deleteSync(); |     if (apks != null && apks.isNotEmpty) { | ||||||
|     }); |       for (var apk in apks) { | ||||||
|  |         var shouldDelete = true; | ||||||
|  |         var temp = apk.path.split('/').last; | ||||||
|  |         temp = temp.substring(0, temp.length - 4); | ||||||
|  |         var fn = temp.split('-'); | ||||||
|  |         if (fn.length == 3) { | ||||||
|  |           var possibleId = fn[0]; | ||||||
|  |           var possibleVersion = fn[1]; | ||||||
|  |           var possibleApkUrlIndex = fn[2]; | ||||||
|  |           if (apps[possibleId] != null) { | ||||||
|  |             if (apps[possibleId] != null && | ||||||
|  |                 apps[possibleId]?.app != null && | ||||||
|  |                 apps[possibleId]!.app.installedVersion != | ||||||
|  |                     apps[possibleId]!.app.latestVersion && | ||||||
|  |                 apps[possibleId]!.app.latestVersion == possibleVersion && | ||||||
|  |                 apps[possibleId]!.app.preferredApkIndex.toString() == | ||||||
|  |                     possibleApkUrlIndex) { | ||||||
|  |               shouldDelete = false; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (shouldDelete) apk.delete(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> loadApps() async { |   Future<AppInfo?> getInstalledInfo(String? packageName) async { | ||||||
|  |     if (packageName != null) { | ||||||
|  |       try { | ||||||
|  |         return await InstalledApps.getAppInfo(packageName); | ||||||
|  |       } catch (e) { | ||||||
|  |         // OK | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   String standardizeVersionString(String versionString) { | ||||||
|  |     return versionString.characters | ||||||
|  |         .where((p0) => ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.'] | ||||||
|  |             .contains(p0)) | ||||||
|  |         .join(''); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // If the App says it is installed by installedInfo is null, set it to not installed | ||||||
|  |   // If the App says is is not installed but installedInfo exists, try to set it to installed as latest version... | ||||||
|  |   // ...if the latestVersion seems to match the version in installedInfo (not guaranteed) | ||||||
|  |   App? correctInstallStatus(App app, AppInfo? installedInfo) { | ||||||
|  |     var modded = false; | ||||||
|  |     if (installedInfo == null && app.installedVersion != null) { | ||||||
|  |       app.installedVersion = null; | ||||||
|  |       modded = true; | ||||||
|  |     } | ||||||
|  |     if (installedInfo != null && app.installedVersion == null) { | ||||||
|  |       if (standardizeVersionString(app.latestVersion) == | ||||||
|  |           installedInfo.versionName) { | ||||||
|  |         app.installedVersion = app.latestVersion; | ||||||
|  |       } else { | ||||||
|  |         app.installedVersion = installedInfo.versionName; | ||||||
|  |       } | ||||||
|  |       modded = true; | ||||||
|  |     } | ||||||
|  |     return modded ? app : null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> loadApps({shouldCorrectInstallStatus = true}) async { | ||||||
|  |     while (loadingApps) { | ||||||
|  |       await Future.delayed(const Duration(microseconds: 1)); | ||||||
|  |     } | ||||||
|     loadingApps = true; |     loadingApps = true; | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|     List<FileSystemEntity> appFiles = (await getAppsDir()) |     List<FileSystemEntity> appFiles = (await getAppsDir()) | ||||||
| @@ -266,22 +436,54 @@ class AppsProvider with ChangeNotifier { | |||||||
|         .where((item) => item.path.toLowerCase().endsWith('.json')) |         .where((item) => item.path.toLowerCase().endsWith('.json')) | ||||||
|         .toList(); |         .toList(); | ||||||
|     apps.clear(); |     apps.clear(); | ||||||
|  |     var sp = SourceProvider(); | ||||||
|  |     List<List<String>> errors = []; | ||||||
|     for (int i = 0; i < appFiles.length; i++) { |     for (int i = 0; i < appFiles.length; i++) { | ||||||
|       App app = |       App app = | ||||||
|           App.fromJson(jsonDecode(File(appFiles[i].path).readAsStringSync())); |           App.fromJson(jsonDecode(File(appFiles[i].path).readAsStringSync())); | ||||||
|       apps.putIfAbsent(app.id, () => AppInMemory(app, null)); |       var info = await getInstalledInfo(app.id); | ||||||
|  |       try { | ||||||
|  |         sp.getSource(app.url); | ||||||
|  |         apps.putIfAbsent(app.id, () => AppInMemory(app, null, info)); | ||||||
|  |       } catch (e) { | ||||||
|  |         errors.add([app.id, app.name, e.toString()]); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     if (errors.isNotEmpty) { | ||||||
|  |       removeApps(errors.map((e) => e[0]).toList()); | ||||||
|  |       NotificationsProvider().notify( | ||||||
|  |           AppsRemovedNotification(errors.map((e) => [e[1], e[2]]).toList())); | ||||||
|     } |     } | ||||||
|     loadingApps = false; |     loadingApps = false; | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|  |     // For any that are not installed (by ID == package name), set to not installed if needed | ||||||
|  |     if (shouldCorrectInstallStatus) { | ||||||
|  |       List<App> modifiedApps = []; | ||||||
|  |       for (var app in apps.values) { | ||||||
|  |         var moddedApp = correctInstallStatus(app.app, app.installedInfo); | ||||||
|  |         if (moddedApp != null) { | ||||||
|  |           modifiedApps.add(moddedApp); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (modifiedApps.isNotEmpty) { | ||||||
|  |         await saveApps(modifiedApps, shouldCorrectInstallStatus: false); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> saveApps(List<App> apps) async { |   Future<void> saveApps(List<App> apps, | ||||||
|  |       {bool shouldCorrectInstallStatus = true}) async { | ||||||
|     for (var app in apps) { |     for (var app in apps) { | ||||||
|  |       AppInfo? info = await getInstalledInfo(app.id); | ||||||
|  |       app.name = info?.name ?? app.name; | ||||||
|  |       if (shouldCorrectInstallStatus) { | ||||||
|  |         app = correctInstallStatus(app, info) ?? app; | ||||||
|  |       } | ||||||
|       File('${(await getAppsDir()).path}/${app.id}.json') |       File('${(await getAppsDir()).path}/${app.id}.json') | ||||||
|           .writeAsStringSync(jsonEncode(app.toJson())); |           .writeAsStringSync(jsonEncode(app.toJson())); | ||||||
|       this.apps.update( |       this.apps.update( | ||||||
|           app.id, (value) => AppInMemory(app, value.downloadProgress), |           app.id, (value) => AppInMemory(app, value.downloadProgress, info), | ||||||
|           ifAbsent: () => AppInMemory(app, null)); |           ifAbsent: () => AppInMemory(app, null, info)); | ||||||
|     } |     } | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|   } |   } | ||||||
| @@ -308,61 +510,73 @@ class AppsProvider with ChangeNotifier { | |||||||
|     return app.latestVersion != apps[app.id]?.app.installedVersion; |     return app.latestVersion != apps[app.id]?.app.installedVersion; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<App?> getUpdate(String appId) async { |   Future<App?> getUpdate(String appId, | ||||||
|  |       {bool shouldCorrectInstallStatus = true}) async { | ||||||
|     App? currentApp = apps[appId]!.app; |     App? currentApp = apps[appId]!.app; | ||||||
|     SourceProvider sourceProvider = SourceProvider(); |     SourceProvider sourceProvider = SourceProvider(); | ||||||
|     App newApp = await sourceProvider.getApp( |     App newApp = await sourceProvider.getApp( | ||||||
|         sourceProvider.getSource(currentApp.url), |         sourceProvider.getSource(currentApp.url), | ||||||
|         currentApp.url, |         currentApp.url, | ||||||
|         currentApp.additionalData, |         currentApp.additionalData, | ||||||
|         customName: currentApp.name); |         name: currentApp.name, | ||||||
|  |         id: currentApp.id); | ||||||
|     newApp.installedVersion = currentApp.installedVersion; |     newApp.installedVersion = currentApp.installedVersion; | ||||||
|     if (currentApp.preferredApkIndex < newApp.apkUrls.length) { |     if (currentApp.preferredApkIndex < newApp.apkUrls.length) { | ||||||
|       newApp.preferredApkIndex = currentApp.preferredApkIndex; |       newApp.preferredApkIndex = currentApp.preferredApkIndex; | ||||||
|     } |     } | ||||||
|     await saveApps([newApp]); |     await saveApps([newApp], | ||||||
|  |         shouldCorrectInstallStatus: shouldCorrectInstallStatus); | ||||||
|     return newApp.latestVersion != currentApp.latestVersion ? newApp : null; |     return newApp.latestVersion != currentApp.latestVersion ? newApp : null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<List<App>> checkUpdates( |   Future<List<App>> checkUpdates( | ||||||
|       {DateTime? ignoreAfter, |       {DateTime? ignoreAfter, | ||||||
|       bool immediatelyThrowRateLimitError = false}) async { |       bool immediatelyThrowRateLimitError = false, | ||||||
|  |       bool shouldCorrectInstallStatus = true, | ||||||
|  |       bool immediatelyThrowSocketError = false}) async { | ||||||
|     List<App> updates = []; |     List<App> updates = []; | ||||||
|     Map<String, List<String>> errors = {}; |     Map<String, List<String>> errors = {}; | ||||||
|     if (!gettingUpdates) { |     if (!gettingUpdates) { | ||||||
|       gettingUpdates = true; |       gettingUpdates = true; | ||||||
|  |  | ||||||
|       List<String> appIds = apps.keys.toList(); |       try { | ||||||
|       if (ignoreAfter != null) { |         List<String> appIds = apps.keys.toList(); | ||||||
|         appIds = appIds |         if (ignoreAfter != null) { | ||||||
|             .where((id) => |           appIds = appIds | ||||||
|                 apps[id]!.app.lastUpdateCheck == null || |               .where((id) => | ||||||
|                 apps[id]!.app.lastUpdateCheck!.isBefore(ignoreAfter)) |                   apps[id]!.app.lastUpdateCheck == null || | ||||||
|             .toList(); |                   apps[id]!.app.lastUpdateCheck!.isBefore(ignoreAfter)) | ||||||
|       } |               .toList(); | ||||||
|       appIds.sort((a, b) => (apps[a]!.app.lastUpdateCheck ?? |         } | ||||||
|               DateTime.fromMicrosecondsSinceEpoch(0)) |         appIds.sort((a, b) => (apps[a]!.app.lastUpdateCheck ?? | ||||||
|           .compareTo(apps[b]!.app.lastUpdateCheck ?? |                 DateTime.fromMicrosecondsSinceEpoch(0)) | ||||||
|               DateTime.fromMicrosecondsSinceEpoch(0))); |             .compareTo(apps[b]!.app.lastUpdateCheck ?? | ||||||
|  |                 DateTime.fromMicrosecondsSinceEpoch(0))); | ||||||
|  |  | ||||||
|       for (int i = 0; i < appIds.length; i++) { |         for (int i = 0; i < appIds.length; i++) { | ||||||
|         App? newApp; |           App? newApp; | ||||||
|         try { |           try { | ||||||
|           newApp = await getUpdate(appIds[i]); |             newApp = await getUpdate(appIds[i], | ||||||
|         } catch (e) { |                 shouldCorrectInstallStatus: shouldCorrectInstallStatus); | ||||||
|           if (e is RateLimitError && immediatelyThrowRateLimitError) { |           } catch (e) { | ||||||
|             rethrow; |             if (e is RateLimitError && immediatelyThrowRateLimitError) { | ||||||
|  |               rethrow; | ||||||
|  |             } | ||||||
|  |             if (e is SocketException && immediatelyThrowSocketError) { | ||||||
|  |               rethrow; | ||||||
|  |             } | ||||||
|  |             var tempIds = errors.remove(e.toString()); | ||||||
|  |             tempIds ??= []; | ||||||
|  |             tempIds.add(appIds[i]); | ||||||
|  |             errors.putIfAbsent(e.toString(), () => tempIds!); | ||||||
|  |           } | ||||||
|  |           if (newApp != null) { | ||||||
|  |             updates.add(newApp); | ||||||
|           } |           } | ||||||
|           var tempIds = errors.remove(e.toString()); |  | ||||||
|           tempIds ??= []; |  | ||||||
|           tempIds.add(appIds[i]); |  | ||||||
|           errors.putIfAbsent(e.toString(), () => tempIds!); |  | ||||||
|         } |  | ||||||
|         if (newApp != null) { |  | ||||||
|           updates.add(newApp); |  | ||||||
|         } |         } | ||||||
|  |       } finally { | ||||||
|  |         gettingUpdates = false; | ||||||
|       } |       } | ||||||
|       gettingUpdates = false; |  | ||||||
|     } |     } | ||||||
|     if (errors.isNotEmpty) { |     if (errors.isNotEmpty) { | ||||||
|       String finalError = ''; |       String finalError = ''; | ||||||
| @@ -413,9 +627,13 @@ class AppsProvider with ChangeNotifier { | |||||||
|     List<App> importedApps = (jsonDecode(appsJSON) as List<dynamic>) |     List<App> importedApps = (jsonDecode(appsJSON) as List<dynamic>) | ||||||
|         .map((e) => App.fromJson(e)) |         .map((e) => App.fromJson(e)) | ||||||
|         .toList(); |         .toList(); | ||||||
|  |     while (loadingApps) { | ||||||
|  |       await Future.delayed(const Duration(microseconds: 1)); | ||||||
|  |     } | ||||||
|     for (App a in importedApps) { |     for (App a in importedApps) { | ||||||
|       a.installedVersion = |       if (apps[a.id]?.app.installedVersion != null) { | ||||||
|           apps.containsKey(a.id) ? apps[a]?.app.installedVersion : null; |         a.installedVersion = apps[a.id]?.app.installedVersion; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|     await saveApps(importedApps); |     await saveApps(importedApps); | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
| @@ -424,7 +642,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void dispose() { |   void dispose() { | ||||||
|     foregroundSubscription.cancel(); |     foregroundSubscription?.cancel(); | ||||||
|     super.dispose(); |     super.dispose(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -61,6 +61,24 @@ class ErrorCheckingUpdatesNotification extends ObtainiumNotification { | |||||||
|             Importance.high); |             Importance.high); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class AppsRemovedNotification extends ObtainiumNotification { | ||||||
|  |   AppsRemovedNotification(List<List<String>> namedReasons) | ||||||
|  |       : super( | ||||||
|  |             6, | ||||||
|  |             'Apps Removed', | ||||||
|  |             '', | ||||||
|  |             'APPS_REMOVED', | ||||||
|  |             'Apps Removed', | ||||||
|  |             'Notifies the user that one or more Apps were removed due to errors while loading them', | ||||||
|  |             Importance.max) { | ||||||
|  |     message = ''; | ||||||
|  |     for (var r in namedReasons) { | ||||||
|  |       message += '${r[0]} was removed due to this error: ${r[1]}. \n'; | ||||||
|  |     } | ||||||
|  |     message = message.trim(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| final completeInstallationNotification = ObtainiumNotification( | final completeInstallationNotification = ObtainiumNotification( | ||||||
|     1, |     1, | ||||||
|     'Complete App Installation', |     'Complete App Installation', | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ | |||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
|  |  | ||||||
| import 'package:html/dom.dart'; | import 'package:html/dom.dart'; | ||||||
| import 'package:obtainium/app_sources/apkmirror.dart'; |  | ||||||
| import 'package:obtainium/app_sources/fdroid.dart'; | import 'package:obtainium/app_sources/fdroid.dart'; | ||||||
| import 'package:obtainium/app_sources/github.dart'; | import 'package:obtainium/app_sources/github.dart'; | ||||||
| import 'package:obtainium/app_sources/gitlab.dart'; | import 'package:obtainium/app_sources/gitlab.dart'; | ||||||
| @@ -54,7 +53,7 @@ class App { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String toString() { |   String toString() { | ||||||
|     return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls'; |     return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALDATA: ${additionalData.toString()} LASTCHECK: ${lastUpdateCheck.toString()}'; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   factory App.fromJson(Map<String, dynamic> json) => App( |   factory App.fromJson(Map<String, dynamic> json) => App( | ||||||
| @@ -157,7 +156,7 @@ class SourceProvider { | |||||||
|     Mullvad(), |     Mullvad(), | ||||||
|     Signal(), |     Signal(), | ||||||
|     SourceForge(), |     SourceForge(), | ||||||
|     APKMirror() |     // APKMirror() | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   // Add more mass source classes here so they are available via the service |   // Add more mass source classes here so they are available via the service | ||||||
| @@ -189,18 +188,21 @@ class SourceProvider { | |||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   String generateTempID(AppNames names, AppSource source) => | ||||||
|  |       '${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}'; | ||||||
|  |  | ||||||
|   Future<App> getApp(AppSource source, String url, List<String> additionalData, |   Future<App> getApp(AppSource source, String url, List<String> additionalData, | ||||||
|       {String customName = ''}) async { |       {String name = '', String? id}) async { | ||||||
|     String standardUrl = source.standardizeURL(preStandardizeUrl(url)); |     String standardUrl = source.standardizeURL(preStandardizeUrl(url)); | ||||||
|     AppNames names = source.getAppNames(standardUrl); |     AppNames names = source.getAppNames(standardUrl); | ||||||
|     APKDetails apk = |     APKDetails apk = | ||||||
|         await source.getLatestAPKDetails(standardUrl, additionalData); |         await source.getLatestAPKDetails(standardUrl, additionalData); | ||||||
|     return App( |     return App( | ||||||
|         '${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}', |         id ?? generateTempID(names, source), | ||||||
|         standardUrl, |         standardUrl, | ||||||
|         names.author[0].toUpperCase() + names.author.substring(1), |         names.author[0].toUpperCase() + names.author.substring(1), | ||||||
|         customName.trim().isNotEmpty |         name.trim().isNotEmpty | ||||||
|             ? customName |             ? name | ||||||
|             : names.name[0].toUpperCase() + names.name.substring(1), |             : names.name[0].toUpperCase() + names.name.substring(1), | ||||||
|         null, |         null, | ||||||
|         apk.version, |         apk.version, | ||||||
|   | |||||||
							
								
								
									
										142
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						| @@ -14,7 +14,7 @@ packages: | |||||||
|       name: archive |       name: archive | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.3.1" |     version: "3.3.2" | ||||||
|   args: |   args: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -112,42 +112,14 @@ packages: | |||||||
|       name: device_info_plus |       name: device_info_plus | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "5.0.5" |     version: "8.0.0" | ||||||
|   device_info_plus_linux: |  | ||||||
|     dependency: transitive |  | ||||||
|     description: |  | ||||||
|       name: device_info_plus_linux |  | ||||||
|       url: "https://pub.dartlang.org" |  | ||||||
|     source: hosted |  | ||||||
|     version: "4.0.2" |  | ||||||
|   device_info_plus_macos: |  | ||||||
|     dependency: transitive |  | ||||||
|     description: |  | ||||||
|       name: device_info_plus_macos |  | ||||||
|       url: "https://pub.dartlang.org" |  | ||||||
|     source: hosted |  | ||||||
|     version: "4.0.2" |  | ||||||
|   device_info_plus_platform_interface: |   device_info_plus_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: device_info_plus_platform_interface |       name: device_info_plus_platform_interface | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "4.0.1" |     version: "7.0.0" | ||||||
|   device_info_plus_web: |  | ||||||
|     dependency: transitive |  | ||||||
|     description: |  | ||||||
|       name: device_info_plus_web |  | ||||||
|       url: "https://pub.dartlang.org" |  | ||||||
|     source: hosted |  | ||||||
|     version: "4.0.2" |  | ||||||
|   device_info_plus_windows: |  | ||||||
|     dependency: transitive |  | ||||||
|     description: |  | ||||||
|       name: device_info_plus_windows |  | ||||||
|       url: "https://pub.dartlang.org" |  | ||||||
|     source: hosted |  | ||||||
|     version: "5.0.2" |  | ||||||
|   dynamic_color: |   dynamic_color: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -182,7 +154,7 @@ packages: | |||||||
|       name: file_picker |       name: file_picker | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "5.2.0+1" |     version: "5.2.2" | ||||||
|   flutter: |   flutter: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: flutter |     description: flutter | ||||||
| @@ -215,14 +187,14 @@ packages: | |||||||
|       name: flutter_local_notifications |       name: flutter_local_notifications | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "12.0.0" |     version: "12.0.3" | ||||||
|   flutter_local_notifications_linux: |   flutter_local_notifications_linux: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: flutter_local_notifications_linux |       name: flutter_local_notifications_linux | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.0.0" |     version: "2.0.0" | ||||||
|   flutter_local_notifications_platform_interface: |   flutter_local_notifications_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -253,14 +225,14 @@ packages: | |||||||
|       name: fluttertoast |       name: fluttertoast | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "8.0.9" |     version: "8.1.1" | ||||||
|   html: |   html: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: html |       name: html | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.15.0" |     version: "0.15.1" | ||||||
|   http: |   http: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -274,14 +246,14 @@ packages: | |||||||
|       name: http_parser |       name: http_parser | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "4.0.1" |     version: "4.0.2" | ||||||
|   image: |   image: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: image |       name: image | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.2.0" |     version: "3.2.2" | ||||||
|   install_plugin_v2: |   install_plugin_v2: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -289,6 +261,13 @@ packages: | |||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.0.0" |     version: "1.0.0" | ||||||
|  |   installed_apps: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: installed_apps | ||||||
|  |       url: "https://pub.dartlang.org" | ||||||
|  |     source: hosted | ||||||
|  |     version: "1.3.1" | ||||||
|   js: |   js: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -309,7 +288,7 @@ packages: | |||||||
|       name: lints |       name: lints | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.0" |     version: "2.0.1" | ||||||
|   matcher: |   matcher: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -323,7 +302,7 @@ packages: | |||||||
|       name: material_color_utilities |       name: material_color_utilities | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.2.0" |     version: "0.1.5" | ||||||
|   meta: |   meta: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -345,6 +324,20 @@ packages: | |||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.0.0" |     version: "1.0.0" | ||||||
|  |   package_archive_info: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: package_archive_info | ||||||
|  |       url: "https://pub.dartlang.org" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.1.0" | ||||||
|  |   package_info: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: package_info | ||||||
|  |       url: "https://pub.dartlang.org" | ||||||
|  |     source: hosted | ||||||
|  |     version: "2.0.2" | ||||||
|   path: |   path: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -407,21 +400,21 @@ packages: | |||||||
|       name: permission_handler |       name: permission_handler | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "10.1.0" |     version: "10.2.0" | ||||||
|   permission_handler_android: |   permission_handler_android: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: permission_handler_android |       name: permission_handler_android | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "10.1.0" |     version: "10.2.0" | ||||||
|   permission_handler_apple: |   permission_handler_apple: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: permission_handler_apple |       name: permission_handler_apple | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "9.0.6" |     version: "9.0.7" | ||||||
|   permission_handler_platform_interface: |   permission_handler_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -435,14 +428,14 @@ packages: | |||||||
|       name: permission_handler_windows |       name: permission_handler_windows | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.1.1" |     version: "0.1.2" | ||||||
|   petitparser: |   petitparser: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: petitparser |       name: petitparser | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "5.0.0" |     version: "5.1.0" | ||||||
|   platform: |   platform: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -470,49 +463,21 @@ packages: | |||||||
|       name: provider |       name: provider | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "6.0.3" |     version: "6.0.4" | ||||||
|   share_plus: |   share_plus: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: share_plus |       name: share_plus | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "4.5.3" |     version: "6.1.0" | ||||||
|   share_plus_linux: |  | ||||||
|     dependency: transitive |  | ||||||
|     description: |  | ||||||
|       name: share_plus_linux |  | ||||||
|       url: "https://pub.dartlang.org" |  | ||||||
|     source: hosted |  | ||||||
|     version: "3.0.0" |  | ||||||
|   share_plus_macos: |  | ||||||
|     dependency: transitive |  | ||||||
|     description: |  | ||||||
|       name: share_plus_macos |  | ||||||
|       url: "https://pub.dartlang.org" |  | ||||||
|     source: hosted |  | ||||||
|     version: "3.0.1" |  | ||||||
|   share_plus_platform_interface: |   share_plus_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: share_plus_platform_interface |       name: share_plus_platform_interface | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.1.1" |     version: "3.2.0" | ||||||
|   share_plus_web: |  | ||||||
|     dependency: transitive |  | ||||||
|     description: |  | ||||||
|       name: share_plus_web |  | ||||||
|       url: "https://pub.dartlang.org" |  | ||||||
|     source: hosted |  | ||||||
|     version: "3.1.0" |  | ||||||
|   share_plus_windows: |  | ||||||
|     dependency: transitive |  | ||||||
|     description: |  | ||||||
|       name: share_plus_windows |  | ||||||
|       url: "https://pub.dartlang.org" |  | ||||||
|     source: hosted |  | ||||||
|     version: "3.0.1" |  | ||||||
|   shared_preferences: |   shared_preferences: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -526,7 +491,7 @@ packages: | |||||||
|       name: shared_preferences_android |       name: shared_preferences_android | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.13" |     version: "2.0.14" | ||||||
|   shared_preferences_ios: |   shared_preferences_ios: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -580,7 +545,7 @@ packages: | |||||||
|       name: source_span |       name: source_span | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.9.1" |     version: "1.9.0" | ||||||
|   stack_trace: |   stack_trace: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -594,7 +559,7 @@ packages: | |||||||
|       name: stream_channel |       name: stream_channel | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.1" |     version: "2.1.0" | ||||||
|   string_scanner: |   string_scanner: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -615,7 +580,7 @@ packages: | |||||||
|       name: test_api |       name: test_api | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.4.14" |     version: "0.4.12" | ||||||
|   timezone: |   timezone: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -643,7 +608,7 @@ packages: | |||||||
|       name: url_launcher_android |       name: url_launcher_android | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "6.0.19" |     version: "6.0.21" | ||||||
|   url_launcher_ios: |   url_launcher_ios: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -686,13 +651,20 @@ packages: | |||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.1" |     version: "3.0.1" | ||||||
|  |   uuid: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: uuid | ||||||
|  |       url: "https://pub.dartlang.org" | ||||||
|  |     source: hosted | ||||||
|  |     version: "3.0.6" | ||||||
|   vector_math: |   vector_math: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: vector_math |       name: vector_math | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.4" |     version: "2.1.2" | ||||||
|   webview_flutter: |   webview_flutter: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -727,14 +699,14 @@ packages: | |||||||
|       name: win32 |       name: win32 | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.0" |     version: "3.0.1" | ||||||
|   workmanager: |   workmanager: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: workmanager |       name: workmanager | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.5.0" |     version: "0.5.1" | ||||||
|   xdg_directories: |   xdg_directories: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -757,5 +729,5 @@ packages: | |||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.1.1" |     version: "3.1.1" | ||||||
| sdks: | sdks: | ||||||
|   dart: ">=2.19.0-79.0.dev <3.0.0" |   dart: ">=2.18.2 <3.0.0" | ||||||
|   flutter: ">=3.3.0" |   flutter: ">=3.3.0" | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						| @@ -17,10 +17,10 @@ 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 | # 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 | # 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. | # of the product and file versions while build-number is used as the build suffix. | ||||||
| version: 0.5.10+31 # When changing this, update the tag in main() accordingly | version: 0.6.0+44 # When changing this, update the tag in main() accordingly | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: '>=2.19.0-79.0.dev <3.0.0' |   sdk: '>=2.18.2 <3.0.0' | ||||||
|  |  | ||||||
| # Dependencies specify other packages that your package needs in order to work. | # Dependencies specify other packages that your package needs in order to work. | ||||||
| # To automatically upgrade your package dependencies to the latest versions | # To automatically upgrade your package dependencies to the latest versions | ||||||
| @@ -49,11 +49,13 @@ dependencies: | |||||||
|   url_launcher: ^6.1.5 |   url_launcher: ^6.1.5 | ||||||
|   permission_handler: ^10.0.0 |   permission_handler: ^10.0.0 | ||||||
|   fluttertoast: ^8.0.9 |   fluttertoast: ^8.0.9 | ||||||
|   device_info_plus: ^5.0.5 |   device_info_plus: ^8.0.0 | ||||||
|   file_picker: ^5.1.0 |   file_picker: ^5.1.0 | ||||||
|   animations: ^2.0.4 |   animations: ^2.0.4 | ||||||
|   install_plugin_v2: ^1.0.0 |   install_plugin_v2: ^1.0.0 | ||||||
|   share_plus: ^4.4.0 |   share_plus: ^6.0.1 | ||||||
|  |   installed_apps: ^1.3.1 | ||||||
|  |   package_archive_info: ^0.1.0 | ||||||
|  |  | ||||||
|  |  | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   | |||||||