diff --git a/README.md b/README.md index 11c9ca51..18692e78 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ Currently supported App sources: - [IzzyOnDroid](https://android.izzysoft.de/) - [Mullvad](https://mullvad.net/en/) - [Signal](https://signal.org/) -- [APKMirror](https://apkmirror.com/) ## Limitations - App installs are assumed to have succeeded; failures and cancelled installs cannot be detected. diff --git a/assets/screenshots/1.apps.png b/assets/screenshots/1.apps.png index 0b77b4ae..d46fa405 100644 Binary files a/assets/screenshots/1.apps.png and b/assets/screenshots/1.apps.png differ diff --git a/assets/screenshots/2.dark_theme.png b/assets/screenshots/2.dark_theme.png index 96d1abe1..fff96b99 100644 Binary files a/assets/screenshots/2.dark_theme.png and b/assets/screenshots/2.dark_theme.png differ diff --git a/assets/screenshots/3.material_you.png b/assets/screenshots/3.material_you.png index e8421c0e..aee7e759 100644 Binary files a/assets/screenshots/3.material_you.png and b/assets/screenshots/3.material_you.png differ diff --git a/assets/screenshots/4.app.png b/assets/screenshots/4.app.png index 1f7cc7f4..a9960514 100644 Binary files a/assets/screenshots/4.app.png and b/assets/screenshots/4.app.png differ diff --git a/assets/screenshots/5.apk_picker.png b/assets/screenshots/5.apk_picker.png index ded26d00..a6d2abff 100644 Binary files a/assets/screenshots/5.apk_picker.png and b/assets/screenshots/5.apk_picker.png differ diff --git a/assets/screenshots/6.apk_install.png b/assets/screenshots/6.apk_install.png index 2743f6a4..756c6676 100644 Binary files a/assets/screenshots/6.apk_install.png and b/assets/screenshots/6.apk_install.png differ diff --git a/lib/main.dart b/lib/main.dart index 29f1c9b1..12e5df0a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/pages/home.dart'; import 'package:obtainium/providers/apps_provider.dart'; @@ -13,12 +14,14 @@ import 'package:workmanager/workmanager.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:device_info_plus/device_info_plus.dart'; +const String currentVersion = '0.6.0'; 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'; bgUpdateCheck(int? ignoreAfterMicroseconds) async { + WidgetsFlutterBinding.ensureInitialized(); DateTime? ignoreAfter = ignoreAfterMicroseconds != null ? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds) : null; @@ -27,21 +30,25 @@ bgUpdateCheck(int? ignoreAfterMicroseconds) async { try { var appsProvider = AppsProvider(); await notificationsProvider.cancel(ErrorCheckingUpdatesNotification('').id); - await appsProvider.loadApps(); + await appsProvider.loadApps(shouldCorrectInstallStatus: false); List existingUpdateIds = appsProvider.getExistingUpdates(installedOnly: true); DateTime nextIgnoreAfter = DateTime.now(); String? err; try { await appsProvider.checkUpdates( - ignoreAfter: ignoreAfter, immediatelyThrowRateLimitError: true); + ignoreAfter: ignoreAfter, + immediatelyThrowRateLimitError: true, + immediatelyThrowSocketError: true, + shouldCorrectInstallStatus: false); } catch (e) { - if (e is RateLimitError) { + if (e is RateLimitError || e is SocketException) { String nextTaskName = '$bgUpdateCheckTaskName-${nextIgnoreAfter.microsecondsSinceEpoch.toString()}'; Workmanager().registerOneOffTask(nextTaskName, nextTaskName, constraints: Constraints(networkType: NetworkType.connected), - initialDelay: Duration(minutes: e.remainingMinutes), + initialDelay: Duration( + minutes: e is RateLimitError ? e.remainingMinutes : 15), inputData: {'ignoreAfter': nextIgnoreAfter.microsecondsSinceEpoch}); } else { err = e.toString(); @@ -68,16 +75,15 @@ bgUpdateCheck(int? ignoreAfterMicroseconds) async { // } if (newUpdates.isNotEmpty) { - notificationsProvider.notify(UpdateNotification(newUpdates), - cancelExisting: true); + notificationsProvider.notify(UpdateNotification(newUpdates)); } if (err != null) { throw err; } return Future.value(true); } catch (e) { - notificationsProvider.notify(ErrorCheckingUpdatesNotification(e.toString()), - cancelExisting: true); + notificationsProvider + .notify(ErrorCheckingUpdatesNotification(e.toString())); return Future.error(false); } finally { await notificationsProvider.cancel(checkingUpdatesNotification.id); @@ -94,7 +100,7 @@ void bgTaskCallback() { void main() async { WidgetsFlutterBinding.ensureInitialized(); - if ((await DeviceInfoPlugin().androidInfo).version.sdkInt! >= 29) { + if ((await DeviceInfoPlugin().androidInfo).version.sdkInt >= 29) { SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent), ); @@ -143,7 +149,7 @@ class _ObtainiumState extends State { Permission.notification.request(); appsProvider.saveApps([ App( - 'imranr98_obtainium_${GitHub().host}', + 'dev.imranr.obtainium', 'https://github.com/ImranR98/Obtainium', 'ImranR98', 'Obtainium', diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index 673417ea..310701b5 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -22,7 +22,6 @@ class _AddAppPageState extends State { String userInput = ''; AppSource? pickedSource; List additionalData = []; - String customName = ''; bool validAdditionalData = true; @override @@ -80,9 +79,6 @@ class _AddAppPageState extends State { .doesSourceHaveRequiredAdditionalData( source) : true; - if (source == null) { - customName = ''; - } } }); }, @@ -90,56 +86,72 @@ class _AddAppPageState extends State { const SizedBox( width: 16, ), - ElevatedButton( - onPressed: gettingAppInfo || - pickedSource == null || - (pickedSource!.additionalDataFormItems - .isNotEmpty && - !validAdditionalData) - ? null - : () { - HapticFeedback.selectionClick(); - setState(() { - gettingAppInfo = true; - }); - sourceProvider - .getApp(pickedSource!, userInput, - additionalData, - customName: customName) - .then((app) { - var appsProvider = - context.read(); - var settingsProvider = - context.read(); - if (appsProvider.apps - .containsKey(app.id)) { - throw 'App already added'; - } - settingsProvider - .getInstallPermission() - .then((_) { - appsProvider - .saveApps([app]).then((_) { + gettingAppInfo + ? const CircularProgressIndicator() + : ElevatedButton( + onPressed: gettingAppInfo || + pickedSource == null || + (pickedSource!.additionalDataFormItems + .isNotEmpty && + !validAdditionalData) + ? null + : () async { + setState(() { + gettingAppInfo = true; + }); + var appsProvider = + context.read(); + var settingsProvider = + context.read(); + () async { + HapticFeedback.selectionClick(); + App app = + await sourceProvider.getApp( + pickedSource!, + userInput, + additionalData); + await settingsProvider + .getInstallPermission(); + // ignore: use_build_context_synchronously + var apkUrl = await appsProvider + .selectApkUrl(app, context); + 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( context, MaterialPageRoute( builder: (context) => AppPage( appId: app.id))); + }).catchError((e) { + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text(e.toString())), + ); + }).whenComplete(() { + setState(() { + gettingAppInfo = false; + }); }); - }); - }).catchError((e) { - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar(content: Text(e.toString())), - ); - }).whenComplete(() { - setState(() { - gettingAppInfo = false; - }); - }); - }, - child: const Text('Add')) + }, + child: const Text('Add')) ], ), if (pickedSource != null) @@ -174,21 +186,6 @@ class _AddAppPageState extends State { const SizedBox( height: 8, ), - if (pickedSource != null) - GeneratedForm( - items: [ - [ - GeneratedFormItem( - label: 'Custom App Name', - required: false) - ] - ], - onValueChanges: (values, valid) { - setState(() { - customName = values[0]; - }); - }, - defaultValues: [customName]) ], ) else diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 7dfc5bdb..f9d60a23 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form_modal.dart'; import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/settings_provider.dart'; @@ -46,6 +45,7 @@ class _AppPageState extends State { body: RefreshIndicator( child: settingsProvider.showAppWebpage ? WebView( + backgroundColor: Theme.of(context).colorScheme.background, initialUrl: app?.app.url, javascriptMode: JavascriptMode.unrestricted, ) @@ -56,8 +56,18 @@ class _AppPageState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + app?.installedInfo != null + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.memory( + app!.installedInfo!.icon!, + scale: 1.5, + ) + ]) + : Container(), Text( - app?.app.name ?? 'App', + app?.installedInfo?.name ?? app?.app.name ?? 'App', textAlign: TextAlign.center, style: Theme.of(context).textTheme.displayLarge, ), @@ -126,7 +136,8 @@ class _AppPageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - if (app?.app.installedVersion != app?.app.latestVersion) + if (app?.app.installedVersion != null && + app?.app.installedVersion != app?.app.latestVersion) IconButton( onPressed: app?.downloadProgress != null ? null @@ -135,8 +146,8 @@ class _AppPageState extends State { context: context, builder: (BuildContext ctx) { return AlertDialog( - title: Text( - 'App Already ${app?.app.installedVersion == null ? 'Installed' : 'Updated'}?'), + title: const Text( + 'App Already up to Date?'), actions: [ TextButton( onPressed: () { @@ -161,54 +172,13 @@ class _AppPageState extends State { .pop(); }, child: const Text( - 'Yes, Mark as Installed')) - ], - ); - }); - }, - tooltip: 'Mark as Installed', - 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')) + 'Yes, Mark as Updated')) ], ); }); }, - tooltip: 'Mark as Not Installed', - icon: const Icon(Icons.no_cell_outlined)), + tooltip: 'Mark as Updated', + icon: const Icon(Icons.done)), if (source != null && source.additionalDataFormItems.isNotEmpty) IconButton( @@ -220,30 +190,15 @@ class _AppPageState extends State { builder: (BuildContext ctx) { return GeneratedFormModal( title: 'Additional Options', - items: [ - ...source - .additionalDataFormItems, - [ - GeneratedFormItem( - label: 'App Name', - required: true) - ] - ], + items: source + .additionalDataFormItems, defaultValues: app != null - ? [ - ...app - .app.additionalData, - app.app.name - ] - : [ - ...source - .additionalDataDefaults - ]); + ? app.app.additionalData + : source + .additionalDataDefaults); }).then((values) { if (app != null && values != null) { var changedApp = app.app; - var name = values.removeLast(); - changedApp.name = name; changedApp.additionalData = values; appsProvider.saveApps( [changedApp]).then((value) { @@ -265,12 +220,18 @@ class _AppPageState extends State { ? () { HapticFeedback.heavyImpact(); appsProvider - .downloadAndInstallLatestApp( + .downloadAndInstallLatestApps( [app!.app.id], context).then((res) { if (res.isNotEmpty && mounted) { Navigator.of(context).pop(); } + }).catchError((e) { + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text(e.toString())), + ); }); } : null, @@ -288,7 +249,7 @@ class _AppPageState extends State { return AlertDialog( title: const Text('Remove App?'), 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: [ TextButton( onPressed: () { diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index b41578ac..e597ca1d 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -89,7 +89,8 @@ class AppsPageState extends State { .toList(); 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; } } @@ -103,13 +104,13 @@ class AppsPageState extends State { } sortedApps.sort((a, b) { + var nameA = a.installedInfo?.name ?? a.app.name; + var nameB = b.installedInfo?.name ?? b.app.name; int result = 0; if (settingsProvider.sortColumn == SortColumnSettings.authorName) { - result = - (a.app.author + a.app.name).compareTo(b.app.author + b.app.name); + result = (a.app.author + nameA).compareTo(b.app.author + nameB); } else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) { - result = - (a.app.name + a.app.author).compareTo(b.app.name + b.app.author); + result = (nameA + a.app.author).compareTo(nameB + b.app.author); } return result; }); @@ -166,7 +167,11 @@ class AppsPageState extends State { onLongPress: () { 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}'), trailing: sortedApps[index].downloadProgress != null ? Text( @@ -178,7 +183,9 @@ class AppsPageState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ - const Text('Update Available'), + Text(appsProvider.areDownloadsRunning() + ? 'Please Wait...' + : 'Update Available'), SourceProvider() .getSource(sortedApps[index].app.url) .changeLogPageFromStandardUrl( @@ -303,7 +310,7 @@ class AppsPageState extends State { message: '${existingUpdateIdsAllOrSelected.length} update${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'} and ${newInstallIdsAllOrSelected.length} new install${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}.', items: formInputs, - defaultValues: const ['true', 'true'], + defaultValues: const ['true'], initValid: true, ); }).then((values) { @@ -311,7 +318,7 @@ class AppsPageState extends State { bool shouldInstallUpdates = values.length < 2 || values[0] == 'true'; bool shouldInstallNew = - values.length < 2 || values[1] == 'true'; + values.length >= 2 && values[1] == 'true'; settingsProvider .getInstallPermission() .then((_) { @@ -324,8 +331,14 @@ class AppsPageState extends State { toInstall .addAll(newInstallIdsAllOrSelected); } - appsProvider.downloadAndInstallLatestApp( - toInstall, context); + appsProvider + .downloadAndInstallLatestApps( + toInstall, context) + .catchError((e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString())), + ); + }); }); } }); @@ -349,7 +362,7 @@ class AppsPageState extends State { padding: const EdgeInsets.only(top: 6), child: Row( mainAxisAlignment: - MainAxisAlignment.spaceBetween, + MainAxisAlignment.spaceAround, children: [ IconButton( onPressed: @@ -364,7 +377,10 @@ class AppsPageState extends State { ctx) { return AlertDialog( 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: [ TextButton( onPressed: @@ -383,8 +399,10 @@ class AppsPageState extends State { .saveApps(selectedIds.map((e) { var a = appsProvider.apps[e]!.app; - a.installedVersion = - null; + if (a.installedVersion != + null) { + a.installedVersion = a.latestVersion; + } return a; }).toList()); @@ -398,57 +416,7 @@ class AppsPageState extends State { }); }, tooltip: - 'Mark Selected Apps as Not Installed', - 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', + 'Mark Selected Apps as Updated', icon: const Icon(Icons.done)), IconButton( onPressed: () { diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index a725e734..2597314e 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -9,9 +9,12 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.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/custom_errors.dart'; import 'package:obtainium/providers/notifications_provider.dart'; +import 'package:package_archive_info/package_archive_info.dart'; import 'package:provider/provider.dart'; import 'package:path_provider/path_provider.dart'; import 'package:flutter_fgbg/flutter_fgbg.dart'; @@ -21,14 +24,15 @@ import 'package:http/http.dart'; class AppInMemory { late App app; 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; File file; - ApkFile(this.appId, this.file); + DownloadedApp(this.appId, this.file); } 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) bool isForeground = true; - late Stream foregroundStream; - late StreamSubscription foregroundSubscription; + late Stream? foregroundStream; + late StreamSubscription? foregroundSubscription; AppsProvider( {bool shouldLoadApps = false, bool shouldCheckUpdatesAfterLoad = 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) { + // 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((_) { + if (shouldDeleteAPKs) { + deleteSavedAPKs(); + } if (shouldCheckUpdatesAfterLoad) { checkUpdates(); } @@ -64,38 +68,85 @@ class AppsProvider with ChangeNotifier { } } - Future downloadApp(String apkUrl, String appId) async { - apkUrl = await SourceProvider() - .getSource(apps[appId]!.app.url) - .apkUrlPrefetchModifier(apkUrl); + downloadApk(String apkUrl, String fileName, Function? onProgress, + Function? urlModifier, + {bool useExistingIfExists = true}) async { + var destDir = (await getExternalStorageDirectory())!.path; + if (urlModifier != null) { + apkUrl = await urlModifier(apkUrl); + } StreamedResponse response = await Client().send(Request('GET', Uri.parse(apkUrl))); - File downloadFile = - File('${(await getExternalStorageDirectory())!.path}/$appId.apk'); - if (downloadFile.existsSync()) { - downloadFile.deleteSync(); - } - var length = response.contentLength; - var received = 0; - var sink = downloadFile.openWrite(); - - await response.stream.map((s) { - received += s.length; - apps[appId]!.downloadProgress = - (length != null ? received / length * 100 : 30); - notifyListeners(); - return s; - }).pipe(sink); + File downloadFile = File('$destDir/$fileName.apk'); + var alreadyExists = downloadFile.existsSync(); + if (!alreadyExists || !useExistingIfExists) { + if (alreadyExists) { + downloadFile.deleteSync(); + } - await sink.close(); - apps[appId]!.downloadProgress = null; - notifyListeners(); + var length = response.contentLength; + var received = 0; + double? progress; + 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'; + 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 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(); + }, SourceProvider().getSource(app.url).apkUrlPrefetchModifier); + // Delete older versions of the APK if any + for (var file in downloadFile.parent.listSync()) { + var fn = file.path.split('/').last; + if (fn.startsWith('${app.id}-') && + fn.endsWith('.apk') && + fn != '$fileName.apk') { + file.delete(); + } } - 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 @@ -106,8 +157,8 @@ class AppsProvider with ChangeNotifier { // TODO: This is unreliable - try to get from OS in the future var osInfo = await DeviceInfoPlugin().androidInfo; return app.installedVersion != null && - osInfo.version.sdkInt! >= 30 && - osInfo.version.release!.compareTo('12') >= 0; + osInfo.version.sdkInt >= 30 && + osInfo.version.release.compareTo('12') >= 0; } Future 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 // 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 - Future installApk(ApkFile file) async { - await InstallPlugin.installApk(file.file.path, 'dev.imranr.obtainium'); + Future installApk(DownloadedApp file) async { + 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.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 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> addToErrorMap( + Map> 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 @@ -140,37 +242,16 @@ class AppsProvider with ChangeNotifier { // 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 // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result - Future> downloadAndInstallLatestApp( + Future> downloadAndInstallLatestApps( List appIds, BuildContext? context) async { - Map appsToInstall = {}; + List appsToInstall = []; for (var id in appIds) { if (apps[id] == null) { throw 'App not found'; } - // If the App has more than one APK, the user should pick one (if context provided) - 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; - } - } + String? apkUrl = await selectApkUrl(apps[id]!.app, context); + if (apkUrl != null) { int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl); if (urlInd != apps[id]!.app.preferredApkIndex) { @@ -180,18 +261,28 @@ class AppsProvider with ChangeNotifier { if (context != null || (await canInstallSilently(apps[id]!.app) && apps[id]!.app.apkUrls.length == 1)) { - appsToInstall.putIfAbsent(id, () => apkUrl!); + appsToInstall.add(id); } } } + Map> errors = {}; - List downloadedFiles = await Future.wait(appsToInstall.entries - .map((entry) => downloadApp(entry.value, entry.key))); + List downloadedFiles = + 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 silentUpdates = []; - List regularInstalls = []; + List silentUpdates = []; + List regularInstalls = []; for (var f in downloadedFiles) { - bool willBeSilent = await canInstallSilently(apps[f.appId]!.app); + bool willBeSilent = await canInstallSilently(apps[f!.appId]!.app); if (willBeSilent) { silentUpdates.add(f); } else { @@ -200,9 +291,9 @@ class AppsProvider with ChangeNotifier { } // If Obtainium is being installed, it should be the last one - List moveObtainiumToEnd(List items) { + List moveObtainiumToEnd(List items) { String obtainiumId = 'imranr98_obtainium_${GitHub().host}'; - ApkFile? temp; + DownloadedApp? temp; items.removeWhere((element) { bool res = element.appId == obtainiumId; if (res) { @@ -233,11 +324,23 @@ class AppsProvider with ChangeNotifier { await askUserToReturnToForeground(context, waitForFG: true); } 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 getAppsDir() async { @@ -249,16 +352,83 @@ class AppsProvider with ChangeNotifier { return appsDir; } + // Delete all stored APKs except those likely to still be needed Future deleteSavedAPKs() async { - (await getExternalStorageDirectory()) + List? apks = (await getExternalStorageDirectory()) ?.listSync() .where((element) => element.path.endsWith('.apk')) - .forEach((element) { - element.deleteSync(); - }); + .toList(); + 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 loadApps() async { + Future 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 loadApps({shouldCorrectInstallStatus = true}) async { + while (loadingApps) { + await Future.delayed(const Duration(microseconds: 1)); + } loadingApps = true; notifyListeners(); List appFiles = (await getAppsDir()) @@ -266,22 +436,54 @@ class AppsProvider with ChangeNotifier { .where((item) => item.path.toLowerCase().endsWith('.json')) .toList(); apps.clear(); + var sp = SourceProvider(); + List> errors = []; for (int i = 0; i < appFiles.length; i++) { App app = 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; notifyListeners(); + // For any that are not installed (by ID == package name), set to not installed if needed + if (shouldCorrectInstallStatus) { + List 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 saveApps(List apps) async { + Future saveApps(List apps, + {bool shouldCorrectInstallStatus = true}) async { 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') .writeAsStringSync(jsonEncode(app.toJson())); this.apps.update( - app.id, (value) => AppInMemory(app, value.downloadProgress), - ifAbsent: () => AppInMemory(app, null)); + app.id, (value) => AppInMemory(app, value.downloadProgress, info), + ifAbsent: () => AppInMemory(app, null, info)); } notifyListeners(); } @@ -308,61 +510,73 @@ class AppsProvider with ChangeNotifier { return app.latestVersion != apps[app.id]?.app.installedVersion; } - Future getUpdate(String appId) async { + Future getUpdate(String appId, + {bool shouldCorrectInstallStatus = true}) async { App? currentApp = apps[appId]!.app; SourceProvider sourceProvider = SourceProvider(); App newApp = await sourceProvider.getApp( sourceProvider.getSource(currentApp.url), currentApp.url, currentApp.additionalData, - customName: currentApp.name); + name: currentApp.name, + id: currentApp.id); newApp.installedVersion = currentApp.installedVersion; if (currentApp.preferredApkIndex < newApp.apkUrls.length) { newApp.preferredApkIndex = currentApp.preferredApkIndex; } - await saveApps([newApp]); + await saveApps([newApp], + shouldCorrectInstallStatus: shouldCorrectInstallStatus); return newApp.latestVersion != currentApp.latestVersion ? newApp : null; } Future> checkUpdates( {DateTime? ignoreAfter, - bool immediatelyThrowRateLimitError = false}) async { + bool immediatelyThrowRateLimitError = false, + bool shouldCorrectInstallStatus = true, + bool immediatelyThrowSocketError = false}) async { List updates = []; Map> errors = {}; if (!gettingUpdates) { gettingUpdates = true; - List appIds = apps.keys.toList(); - if (ignoreAfter != null) { - appIds = appIds - .where((id) => - apps[id]!.app.lastUpdateCheck == null || - apps[id]!.app.lastUpdateCheck!.isBefore(ignoreAfter)) - .toList(); - } - appIds.sort((a, b) => (apps[a]!.app.lastUpdateCheck ?? - DateTime.fromMicrosecondsSinceEpoch(0)) - .compareTo(apps[b]!.app.lastUpdateCheck ?? - DateTime.fromMicrosecondsSinceEpoch(0))); - - for (int i = 0; i < appIds.length; i++) { - App? newApp; - try { - newApp = await getUpdate(appIds[i]); - } catch (e) { - if (e is RateLimitError && immediatelyThrowRateLimitError) { - rethrow; - } - var tempIds = errors.remove(e.toString()); - tempIds ??= []; - tempIds.add(appIds[i]); - errors.putIfAbsent(e.toString(), () => tempIds!); + try { + List appIds = apps.keys.toList(); + if (ignoreAfter != null) { + appIds = appIds + .where((id) => + apps[id]!.app.lastUpdateCheck == null || + apps[id]!.app.lastUpdateCheck!.isBefore(ignoreAfter)) + .toList(); } - if (newApp != null) { - updates.add(newApp); + appIds.sort((a, b) => (apps[a]!.app.lastUpdateCheck ?? + DateTime.fromMicrosecondsSinceEpoch(0)) + .compareTo(apps[b]!.app.lastUpdateCheck ?? + DateTime.fromMicrosecondsSinceEpoch(0))); + + for (int i = 0; i < appIds.length; i++) { + App? newApp; + try { + newApp = await getUpdate(appIds[i], + shouldCorrectInstallStatus: shouldCorrectInstallStatus); + } catch (e) { + 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); + } } + } finally { + gettingUpdates = false; } - gettingUpdates = false; } if (errors.isNotEmpty) { String finalError = ''; @@ -413,9 +627,13 @@ class AppsProvider with ChangeNotifier { List importedApps = (jsonDecode(appsJSON) as List) .map((e) => App.fromJson(e)) .toList(); + while (loadingApps) { + await Future.delayed(const Duration(microseconds: 1)); + } for (App a in importedApps) { - a.installedVersion = - apps.containsKey(a.id) ? apps[a]?.app.installedVersion : null; + if (apps[a.id]?.app.installedVersion != null) { + a.installedVersion = apps[a.id]?.app.installedVersion; + } } await saveApps(importedApps); notifyListeners(); @@ -424,7 +642,7 @@ class AppsProvider with ChangeNotifier { @override void dispose() { - foregroundSubscription.cancel(); + foregroundSubscription?.cancel(); super.dispose(); } } diff --git a/lib/providers/notifications_provider.dart b/lib/providers/notifications_provider.dart index db5b1968..7705cf5d 100644 --- a/lib/providers/notifications_provider.dart +++ b/lib/providers/notifications_provider.dart @@ -61,6 +61,24 @@ class ErrorCheckingUpdatesNotification extends ObtainiumNotification { Importance.high); } +class AppsRemovedNotification extends ObtainiumNotification { + AppsRemovedNotification(List> 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( 1, 'Complete App Installation', diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 9d0b7304..4accdcf5 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -4,7 +4,6 @@ import 'dart:convert'; import 'package:html/dom.dart'; -import 'package:obtainium/app_sources/apkmirror.dart'; import 'package:obtainium/app_sources/fdroid.dart'; import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/app_sources/gitlab.dart'; @@ -54,7 +53,7 @@ class App { @override 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 json) => App( @@ -157,7 +156,7 @@ class SourceProvider { Mullvad(), Signal(), SourceForge(), - APKMirror() + // APKMirror() ]; // Add more mass source classes here so they are available via the service @@ -189,18 +188,21 @@ class SourceProvider { return false; } + String generateTempID(AppNames names, AppSource source) => + '${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}'; + Future getApp(AppSource source, String url, List additionalData, - {String customName = ''}) async { + {String name = '', String? id}) async { String standardUrl = source.standardizeURL(preStandardizeUrl(url)); AppNames names = source.getAppNames(standardUrl); APKDetails apk = await source.getLatestAPKDetails(standardUrl, additionalData); return App( - '${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}', + id ?? generateTempID(names, source), standardUrl, names.author[0].toUpperCase() + names.author.substring(1), - customName.trim().isNotEmpty - ? customName + name.trim().isNotEmpty + ? name : names.name[0].toUpperCase() + names.name.substring(1), null, apk.version, diff --git a/pubspec.lock b/pubspec.lock index 808ff083..1cfbb766 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -14,7 +14,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.3.1" + version: "3.3.2" args: dependency: transitive description: @@ -112,42 +112,14 @@ packages: name: device_info_plus url: "https://pub.dartlang.org" source: hosted - version: "5.0.5" - 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" + version: "8.0.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" - 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" + version: "7.0.0" dynamic_color: dependency: "direct main" description: @@ -182,7 +154,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "5.2.0+1" + version: "5.2.2" flutter: dependency: "direct main" description: flutter @@ -215,14 +187,14 @@ packages: name: flutter_local_notifications url: "https://pub.dartlang.org" source: hosted - version: "12.0.0" + version: "12.0.3" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "2.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: @@ -253,14 +225,14 @@ packages: name: fluttertoast url: "https://pub.dartlang.org" source: hosted - version: "8.0.9" + version: "8.1.1" html: dependency: "direct main" description: name: html url: "https://pub.dartlang.org" source: hosted - version: "0.15.0" + version: "0.15.1" http: dependency: "direct main" description: @@ -274,14 +246,14 @@ packages: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.0.2" image: dependency: transitive description: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "3.2.2" install_plugin_v2: dependency: "direct main" description: @@ -289,6 +261,13 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: transitive description: @@ -309,7 +288,7 @@ packages: name: lints url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" matcher: dependency: transitive description: @@ -323,7 +302,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.1.5" meta: dependency: transitive description: @@ -345,6 +324,20 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: transitive description: @@ -407,21 +400,21 @@ packages: name: permission_handler url: "https://pub.dartlang.org" source: hosted - version: "10.1.0" + version: "10.2.0" permission_handler_android: dependency: transitive description: name: permission_handler_android url: "https://pub.dartlang.org" source: hosted - version: "10.1.0" + version: "10.2.0" permission_handler_apple: dependency: transitive description: name: permission_handler_apple url: "https://pub.dartlang.org" source: hosted - version: "9.0.6" + version: "9.0.7" permission_handler_platform_interface: dependency: transitive description: @@ -435,14 +428,14 @@ packages: name: permission_handler_windows url: "https://pub.dartlang.org" source: hosted - version: "0.1.1" + version: "0.1.2" petitparser: dependency: transitive description: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "5.1.0" platform: dependency: transitive description: @@ -470,49 +463,21 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "6.0.3" + version: "6.0.4" share_plus: dependency: "direct main" description: name: share_plus url: "https://pub.dartlang.org" source: hosted - version: "4.5.3" - 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" + version: "6.1.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.1.1" - 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" + version: "3.2.0" shared_preferences: dependency: "direct main" description: @@ -526,7 +491,7 @@ packages: name: shared_preferences_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.13" + version: "2.0.14" shared_preferences_ios: dependency: transitive description: @@ -580,7 +545,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.9.1" + version: "1.9.0" stack_trace: dependency: transitive description: @@ -594,7 +559,7 @@ packages: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -615,7 +580,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.14" + version: "0.4.12" timezone: dependency: transitive description: @@ -643,7 +608,7 @@ packages: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.19" + version: "6.0.21" url_launcher_ios: dependency: transitive description: @@ -686,13 +651,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.6" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.2" webview_flutter: dependency: "direct main" description: @@ -727,14 +699,14 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" workmanager: dependency: "direct main" description: name: workmanager url: "https://pub.dartlang.org" source: hosted - version: "0.5.0" + version: "0.5.1" xdg_directories: dependency: transitive description: @@ -757,5 +729,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.0-79.0.dev <3.0.0" + dart: ">=2.18.2 <3.0.0" flutter: ">=3.3.0" diff --git a/pubspec.yaml b/pubspec.yaml index c2ca9bc5..b57fbc36 100644 --- a/pubspec.yaml +++ b/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 # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.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: - 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. # To automatically upgrade your package dependencies to the latest versions @@ -49,11 +49,13 @@ dependencies: url_launcher: ^6.1.5 permission_handler: ^10.0.0 fluttertoast: ^8.0.9 - device_info_plus: ^5.0.5 + device_info_plus: ^8.0.0 file_picker: ^5.1.0 animations: ^2.0.4 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: