mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-31 05:23:28 +01:00 
			
		
		
		
	Merge pull request #542 from ImranR98/dev
Add (Incomplete) XAPK Support (#541), Auto-Check Updates on Start (#539), UI Tweaks (#540)
This commit is contained in:
		| @@ -121,7 +121,7 @@ | |||||||
|     "followSystem": "System folgen", |     "followSystem": "System folgen", | ||||||
|     "obtainium": "Obtainium", |     "obtainium": "Obtainium", | ||||||
|     "materialYou": "Material You", |     "materialYou": "Material You", | ||||||
|     "useBlackTheme": "Use pure black dark theme", |     "useBlackTheme": "Use Pure Black Dark Theme", | ||||||
|     "appSortBy": "App sortieren nach", |     "appSortBy": "App sortieren nach", | ||||||
|     "authorName": "Autor/Name", |     "authorName": "Autor/Name", | ||||||
|     "nameAuthor": "Name/Autor", |     "nameAuthor": "Name/Autor", | ||||||
| @@ -231,6 +231,7 @@ | |||||||
|     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", |     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", | ||||||
|     "about": "About", |     "about": "About", | ||||||
|     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", |     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", | ||||||
|  |     "checkOnStart": "Check Once on Start", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "App entfernen?", |         "one": "App entfernen?", | ||||||
|         "other": "Apps entfernen?" |         "other": "Apps entfernen?" | ||||||
|   | |||||||
| @@ -121,7 +121,7 @@ | |||||||
|     "followSystem": "Follow System", |     "followSystem": "Follow System", | ||||||
|     "obtainium": "Obtainium", |     "obtainium": "Obtainium", | ||||||
|     "materialYou": "Material You", |     "materialYou": "Material You", | ||||||
|     "useBlackTheme": "Use pure black dark theme", |     "useBlackTheme": "Use Pure Black Dark Theme", | ||||||
|     "appSortBy": "App Sort By", |     "appSortBy": "App Sort By", | ||||||
|     "authorName": "Author/Name", |     "authorName": "Author/Name", | ||||||
|     "nameAuthor": "Name/Author", |     "nameAuthor": "Name/Author", | ||||||
| @@ -231,6 +231,7 @@ | |||||||
|     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", |     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", | ||||||
|     "about": "About", |     "about": "About", | ||||||
|     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", |     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", | ||||||
|  |     "checkOnStart": "Check Once on Start", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Remove App?", |         "one": "Remove App?", | ||||||
|         "other": "Remove Apps?" |         "other": "Remove Apps?" | ||||||
|   | |||||||
| @@ -231,6 +231,7 @@ | |||||||
|     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", |     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", | ||||||
|     "about": "About", |     "about": "About", | ||||||
|     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", |     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", | ||||||
|  |     "checkOnStart": "Check Once on Start", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "¿Eliminar Aplicación?", |         "one": "¿Eliminar Aplicación?", | ||||||
|         "other": "¿Eliminar Aplicaciones?" |         "other": "¿Eliminar Aplicaciones?" | ||||||
|   | |||||||
| @@ -231,6 +231,7 @@ | |||||||
|     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", |     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", | ||||||
|     "about": "About", |     "about": "About", | ||||||
|     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", |     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", | ||||||
|  |     "checkOnStart": "Check Once on Start", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "برنامه حذف شود؟", |         "one": "برنامه حذف شود؟", | ||||||
|         "other": "برنامه ها حذف شوند؟" |         "other": "برنامه ها حذف شوند؟" | ||||||
|   | |||||||
| @@ -121,7 +121,7 @@ | |||||||
|     "followSystem": "Suivre le système", |     "followSystem": "Suivre le système", | ||||||
|     "obtainium": "Obtainium", |     "obtainium": "Obtainium", | ||||||
|     "materialYou": "Material You", |     "materialYou": "Material You", | ||||||
|     "useBlackTheme": "Use pure black dark theme", |     "useBlackTheme": "Use Pure Black Dark Theme", | ||||||
|     "appSortBy": "Applications triées par", |     "appSortBy": "Applications triées par", | ||||||
|     "authorName": "Auteur/Nom", |     "authorName": "Auteur/Nom", | ||||||
|     "nameAuthor": "Nom/Auteur", |     "nameAuthor": "Nom/Auteur", | ||||||
| @@ -231,6 +231,7 @@ | |||||||
|     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", |     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", | ||||||
|     "about": "About", |     "about": "About", | ||||||
|     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", |     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", | ||||||
|  |     "checkOnStart": "Check Once on Start", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Supprimer l'application ?", |         "one": "Supprimer l'application ?", | ||||||
|         "other": "Supprimer les applications ?" |         "other": "Supprimer les applications ?" | ||||||
|   | |||||||
| @@ -230,6 +230,7 @@ | |||||||
|     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", |     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", | ||||||
|     "about": "About", |     "about": "About", | ||||||
|     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", |     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", | ||||||
|  |     "checkOnStart": "Check Once on Start", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Eltávolítja az alkalmazást?", |         "one": "Eltávolítja az alkalmazást?", | ||||||
|         "other": "Eltávolítja az alkalmazást?" |         "other": "Eltávolítja az alkalmazást?" | ||||||
|   | |||||||
| @@ -121,7 +121,7 @@ | |||||||
|     "followSystem": "Segui sistema", |     "followSystem": "Segui sistema", | ||||||
|     "obtainium": "Obtainium", |     "obtainium": "Obtainium", | ||||||
|     "materialYou": "Material You", |     "materialYou": "Material You", | ||||||
|     "useBlackTheme": "Use pure black dark theme", |     "useBlackTheme": "Use Pure Black Dark Theme", | ||||||
|     "appSortBy": "App ordinate per", |     "appSortBy": "App ordinate per", | ||||||
|     "authorName": "Autore/Nome", |     "authorName": "Autore/Nome", | ||||||
|     "nameAuthor": "Nome/Autore", |     "nameAuthor": "Nome/Autore", | ||||||
| @@ -231,6 +231,7 @@ | |||||||
|     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", |     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", | ||||||
|     "about": "About", |     "about": "About", | ||||||
|     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", |     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", | ||||||
|  |     "checkOnStart": "Check Once on Start", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Rimuovere l'App?", |         "one": "Rimuovere l'App?", | ||||||
|         "other": "Rimuovere le App?" |         "other": "Rimuovere le App?" | ||||||
|   | |||||||
| @@ -231,6 +231,7 @@ | |||||||
|     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", |     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", | ||||||
|     "about": "About", |     "about": "About", | ||||||
|     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", |     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", | ||||||
|  |     "checkOnStart": "Check Once on Start", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "アプリを削除しますか?", |         "one": "アプリを削除しますか?", | ||||||
|         "other": "アプリを削除しますか?" |         "other": "アプリを削除しますか?" | ||||||
|   | |||||||
| @@ -231,6 +231,7 @@ | |||||||
|     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", |     "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", | ||||||
|     "about": "About", |     "about": "About", | ||||||
|     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", |     "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", | ||||||
|  |     "checkOnStart": "Check Once on Start", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "是否删除应用?", |         "one": "是否删除应用?", | ||||||
|         "other": "是否删除应用?" |         "other": "是否删除应用?" | ||||||
|   | |||||||
| @@ -57,9 +57,9 @@ class APKPure extends AppSource { | |||||||
|       } catch (err) { |       } catch (err) { | ||||||
|         // ignore |         // ignore | ||||||
|       } |       } | ||||||
|  |       String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK'; | ||||||
|       List<MapEntry<String, String>> apkUrls = [ |       List<MapEntry<String, String>> apkUrls = [ | ||||||
|         MapEntry('$appId.apk', 'https://d.$host/b/APK/$appId?version=latest') |         MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest') | ||||||
|       ]; |       ]; | ||||||
|       String author = html |       String author = html | ||||||
|               .querySelector('span.info-sdk') |               .querySelector('span.info-sdk') | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart'; | |||||||
| // ignore: implementation_imports | // ignore: implementation_imports | ||||||
| import 'package:easy_localization/src/localization.dart'; | import 'package:easy_localization/src/localization.dart'; | ||||||
|  |  | ||||||
| const String currentVersion = '0.13.0'; | const String currentVersion = '0.13.1'; | ||||||
| const String currentReleaseTag = | const String currentReleaseTag = | ||||||
|     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES |     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES | ||||||
|  |  | ||||||
|   | |||||||
| @@ -159,9 +159,16 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|             app.preferredApkIndex = |             app.preferredApkIndex = | ||||||
|                 app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value); |                 app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value); | ||||||
|             // ignore: use_build_context_synchronously |             // ignore: use_build_context_synchronously | ||||||
|             var downloadedApk = await appsProvider.downloadApp( |             var downloadedArtifact = await appsProvider.downloadApp( | ||||||
|                 app, globalNavigatorKey.currentContext); |                 app, globalNavigatorKey.currentContext); | ||||||
|             app.id = downloadedApk.appId; |             DownloadedApk? downloadedFile; | ||||||
|  |             DownloadedXApkDir? downloadedDir; | ||||||
|  |             if (downloadedArtifact is DownloadedApk) { | ||||||
|  |               downloadedFile = downloadedArtifact; | ||||||
|  |             } else { | ||||||
|  |               downloadedDir = downloadedArtifact as DownloadedXApkDir; | ||||||
|  |             } | ||||||
|  |             app.id = downloadedFile?.appId ?? downloadedDir!.appId; | ||||||
|           } |           } | ||||||
|           if (appsProvider.apps.containsKey(app.id)) { |           if (appsProvider.apps.containsKey(app.id)) { | ||||||
|             throw ObtainiumError(tr('appAlreadyAdded')); |             throw ObtainiumError(tr('appAlreadyAdded')); | ||||||
|   | |||||||
| @@ -52,6 +52,9 @@ class AppsPageState extends State<AppsPage> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey = | ||||||
|  |       GlobalKey<RefreshIndicatorState>(); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     var appsProvider = context.watch<AppsProvider>(); |     var appsProvider = context.watch<AppsProvider>(); | ||||||
| @@ -61,6 +64,27 @@ class AppsPageState extends State<AppsPage> { | |||||||
|     var currentFilterIsUpdatesOnly = |     var currentFilterIsUpdatesOnly = | ||||||
|         filter.isIdenticalTo(updatesOnlyFilter, settingsProvider); |         filter.isIdenticalTo(updatesOnlyFilter, settingsProvider); | ||||||
|  |  | ||||||
|  |     refresh() { | ||||||
|  |       HapticFeedback.lightImpact(); | ||||||
|  |       setState(() { | ||||||
|  |         refreshingSince = DateTime.now(); | ||||||
|  |       }); | ||||||
|  |       return appsProvider.checkUpdates().catchError((e) { | ||||||
|  |         showError(e, context); | ||||||
|  |       }).whenComplete(() { | ||||||
|  |         setState(() { | ||||||
|  |           refreshingSince = null; | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!appsProvider.loadingApps && | ||||||
|  |         appsProvider.apps.isNotEmpty && | ||||||
|  |         settingsProvider.checkJustStarted() && | ||||||
|  |         settingsProvider.checkOnStart) { | ||||||
|  |       _refreshIndicatorKey.currentState?.show(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     selectedAppIds = selectedAppIds |     selectedAppIds = selectedAppIds | ||||||
|         .where((element) => listedApps.map((e) => e.app.id).contains(element)) |         .where((element) => listedApps.map((e) => e.app.id).contains(element)) | ||||||
|         .toSet(); |         .toSet(); | ||||||
| @@ -315,7 +339,7 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                               ?.isBefore(refreshingSince!) ?? |                               ?.isBefore(refreshingSince!) ?? | ||||||
|                           true)) |                           true)) | ||||||
|                       .length / |                       .length / | ||||||
|                   appsProvider.apps.length, |                   (appsProvider.apps.isNotEmpty ? appsProvider.apps.length : 1), | ||||||
|             ), |             ), | ||||||
|           ) |           ) | ||||||
|       ]; |       ]; | ||||||
| @@ -515,10 +539,12 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                         ? FontWeight.bold |                         ? FontWeight.bold | ||||||
|                         : FontWeight.normal)), |                         : FontWeight.normal)), | ||||||
|             trailing: listedApps[index].downloadProgress != null |             trailing: listedApps[index].downloadProgress != null | ||||||
|                 ? Text(tr('percentProgress', args: [ |                 ? SizedBox( | ||||||
|  |                     width: 110, | ||||||
|  |                     child: Text(tr('percentProgress', args: [ | ||||||
|                       listedApps[index].downloadProgress?.toInt().toString() ?? |                       listedApps[index].downloadProgress?.toInt().toString() ?? | ||||||
|                           '100' |                           '100' | ||||||
|                   ])) |                     ]))) | ||||||
|                 : trailingRow, |                 : trailingRow, | ||||||
|             onTap: () { |             onTap: () { | ||||||
|               if (selectedAppIds.isNotEmpty) { |               if (selectedAppIds.isNotEmpty) { | ||||||
| @@ -1017,19 +1043,8 @@ class AppsPageState extends State<AppsPage> { | |||||||
|     return Scaffold( |     return Scaffold( | ||||||
|       backgroundColor: Theme.of(context).colorScheme.surface, |       backgroundColor: Theme.of(context).colorScheme.surface, | ||||||
|       body: RefreshIndicator( |       body: RefreshIndicator( | ||||||
|           onRefresh: () { |           key: _refreshIndicatorKey, | ||||||
|             HapticFeedback.lightImpact(); |           onRefresh: refresh, | ||||||
|             setState(() { |  | ||||||
|               refreshingSince = DateTime.now(); |  | ||||||
|             }); |  | ||||||
|             return appsProvider.checkUpdates().catchError((e) { |  | ||||||
|               showError(e, context); |  | ||||||
|             }).whenComplete(() { |  | ||||||
|               setState(() { |  | ||||||
|                 refreshingSince = null; |  | ||||||
|               }); |  | ||||||
|             }); |  | ||||||
|           }, |  | ||||||
|           child: CustomScrollView(slivers: <Widget>[ |           child: CustomScrollView(slivers: <Widget>[ | ||||||
|             CustomAppBar(title: tr('appsString')), |             CustomAppBar(title: tr('appsString')), | ||||||
|             ...getLoadingWidgets(), |             ...getLoadingWidgets(), | ||||||
|   | |||||||
| @@ -228,6 +228,18 @@ class _SettingsPageState extends State<SettingsPage> { | |||||||
|                                   color: Theme.of(context).colorScheme.primary), |                                   color: Theme.of(context).colorScheme.primary), | ||||||
|                             ), |                             ), | ||||||
|                             intervalDropdown, |                             intervalDropdown, | ||||||
|  |                             height16, | ||||||
|  |                             Row( | ||||||
|  |                               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|  |                               children: [ | ||||||
|  |                                 Flexible(child: Text(tr('checkOnStart'))), | ||||||
|  |                                 Switch( | ||||||
|  |                                     value: settingsProvider.checkOnStart, | ||||||
|  |                                     onChanged: (value) { | ||||||
|  |                                       settingsProvider.checkOnStart = value; | ||||||
|  |                                     }) | ||||||
|  |                               ], | ||||||
|  |                             ), | ||||||
|                             height32, |                             height32, | ||||||
|                             Text( |                             Text( | ||||||
|                               tr('sourceSpecific'), |                               tr('sourceSpecific'), | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ import 'package:flutter_fgbg/flutter_fgbg.dart'; | |||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
| import 'package:http/http.dart'; | import 'package:http/http.dart'; | ||||||
| import 'package:android_intent_plus/android_intent.dart'; | import 'package:android_intent_plus/android_intent.dart'; | ||||||
|  | import 'package:archive/archive.dart'; | ||||||
|  |  | ||||||
| class AppInMemory { | class AppInMemory { | ||||||
|   late App app; |   late App app; | ||||||
| @@ -46,6 +47,13 @@ class DownloadedApk { | |||||||
|   DownloadedApk(this.appId, this.file); |   DownloadedApk(this.appId, this.file); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class DownloadedXApkDir { | ||||||
|  |   String appId; | ||||||
|  |   File file; | ||||||
|  |   Directory extracted; | ||||||
|  |   DownloadedXApkDir(this.appId, this.file, this.extracted); | ||||||
|  | } | ||||||
|  |  | ||||||
| List<String> generateStandardVersionRegExStrings() { | List<String> generateStandardVersionRegExStrings() { | ||||||
|   // TODO: Look into RegEx for non-Latin characters / non-Arabic numerals |   // TODO: Look into RegEx for non-Latin characters / non-Arabic numerals | ||||||
|   var basics = [ |   var basics = [ | ||||||
| @@ -164,7 +172,27 @@ class AppsProvider with ChangeNotifier { | |||||||
|     return downloadedFile; |     return downloadedFile; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<DownloadedApk> downloadApp(App app, BuildContext? context) async { |   handleAPKIDChange(App app, PackageArchiveInfo newInfo, File downloadedFile, | ||||||
|  |       String downloadUrl) async { | ||||||
|  |     // If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed | ||||||
|  |     // The former case should be handled (give the App its real ID), the latter is a security issue | ||||||
|  |     if (app.id != newInfo.packageName) { | ||||||
|  |       var isTempId = SourceProvider().isTempId(app); | ||||||
|  |       if (apps[app.id] != null && !isTempId) { | ||||||
|  |         throw IDChangedError(); | ||||||
|  |       } | ||||||
|  |       var originalAppId = app.id; | ||||||
|  |       app.id = newInfo.packageName; | ||||||
|  |       downloadedFile = downloadedFile.renameSync( | ||||||
|  |           '${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.apk'); | ||||||
|  |       if (apps[originalAppId] != null) { | ||||||
|  |         await removeApps([originalAppId]); | ||||||
|  |         await saveApps([app], onlyIfExists: !isTempId); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<Object> downloadApp(App app, BuildContext? context) async { | ||||||
|     NotificationsProvider? notificationsProvider = |     NotificationsProvider? notificationsProvider = | ||||||
|         context?.read<NotificationsProvider>(); |         context?.read<NotificationsProvider>(); | ||||||
|     var notifId = DownloadNotification(app.finalName, 0).id; |     var notifId = DownloadNotification(app.finalName, 0).id; | ||||||
| @@ -194,33 +222,42 @@ class AppsProvider with ChangeNotifier { | |||||||
|         } |         } | ||||||
|         prevProg = prog; |         prevProg = prog; | ||||||
|       }); |       }); | ||||||
|       // If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed |       PackageArchiveInfo? newInfo; | ||||||
|       // The former case should be handled (give the App its real ID), the latter is a security issue |       try { | ||||||
|       var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path); |         newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path); | ||||||
|       if (app.id != newInfo.packageName) { |       } catch (e) { | ||||||
|         var isTempId = SourceProvider().isTempId(app); |         // Assume it's an XAPK | ||||||
|         if (apps[app.id] != null && !isTempId) { |         fileName = '${app.id}-${downloadUrl.hashCode}.xapk'; | ||||||
|           throw IDChangedError(); |         String newPath = '${downloadedFile.parent.path}/$fileName'; | ||||||
|  |         downloadedFile.renameSync(newPath); | ||||||
|  |         downloadedFile = File(newPath); | ||||||
|       } |       } | ||||||
|         var originalAppId = app.id; |       Directory? xapkDir; | ||||||
|         app.id = newInfo.packageName; |       if (newInfo == null) { | ||||||
|         downloadedFile = downloadedFile.renameSync( |         String xapkDirPath = '${downloadedFile.path}-dir'; | ||||||
|             '${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.apk'); |         unzipFile(downloadedFile.path, '${downloadedFile.path}-dir'); | ||||||
|         if (apps[originalAppId] != null) { |         xapkDir = Directory(xapkDirPath); | ||||||
|           await removeApps([originalAppId]); |         var apks = xapkDir | ||||||
|           await saveApps([app], onlyIfExists: !isTempId); |             .listSync() | ||||||
|  |             .where((e) => e.path.toLowerCase().endsWith('.apk')) | ||||||
|  |             .toList(); | ||||||
|  |         newInfo = await PackageArchiveInfo.fromPath(apks.first.path); | ||||||
|       } |       } | ||||||
|       } |       await handleAPKIDChange(app, newInfo, downloadedFile, downloadUrl); | ||||||
|       // Delete older versions of the APK if any |       // Delete older versions of the file if any | ||||||
|       for (var file in downloadedFile.parent.listSync()) { |       for (var file in downloadedFile.parent.listSync()) { | ||||||
|         var fn = file.path.split('/').last; |         var fn = file.path.split('/').last; | ||||||
|         if (fn.startsWith('${app.id}-') && |         if (fn.startsWith('${app.id}-') && | ||||||
|             fn.endsWith('.apk') && |             fn.toLowerCase().endsWith(xapkDir == null ? '.apk' : '.xapk') && | ||||||
|             fn != downloadedFile.path.split('/').last) { |             fn != downloadedFile.path.split('/').last) { | ||||||
|           file.delete(); |           file.delete(); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |       if (xapkDir != null) { | ||||||
|  |         return DownloadedXApkDir(app.id, downloadedFile, xapkDir); | ||||||
|  |       } else { | ||||||
|         return DownloadedApk(app.id, downloadedFile); |         return DownloadedApk(app.id, downloadedFile); | ||||||
|  |       } | ||||||
|     } finally { |     } finally { | ||||||
|       notificationsProvider?.cancel(notifId); |       notificationsProvider?.cancel(notifId); | ||||||
|       if (apps[app.id] != null) { |       if (apps[app.id] != null) { | ||||||
| @@ -267,10 +304,37 @@ class AppsProvider with ChangeNotifier { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Unfortunately this 'await' does not actually wait for the APK to finish installing |   void unzipFile(String filePath, String destinationPath) { | ||||||
|   // So we only know that the install prompt was shown, but the user could still cancel w/o us knowing |     final bytes = File(filePath).readAsBytesSync(); | ||||||
|   // If appropriate criteria are met, the update (never a fresh install) happens silently  in the background |     final archive = ZipDecoder().decodeBytes(bytes); | ||||||
|   // But even then, we don't know if it actually succeeded |  | ||||||
|  |     for (final file in archive) { | ||||||
|  |       final filename = '$destinationPath/${file.name}'; | ||||||
|  |       if (file.isFile) { | ||||||
|  |         final data = file.content as List<int>; | ||||||
|  |         File(filename) | ||||||
|  |           ..createSync(recursive: true) | ||||||
|  |           ..writeAsBytesSync(data); | ||||||
|  |       } else { | ||||||
|  |         Directory(filename).create(recursive: true); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> installXApkDir(DownloadedXApkDir dir, | ||||||
|  |       {bool silent = false}) async { | ||||||
|  |     try { | ||||||
|  |       for (var apk in dir.extracted | ||||||
|  |           .listSync() | ||||||
|  |           .where((f) => f is File && f.path.toLowerCase().endsWith('.apk'))) { | ||||||
|  |         await installApk(DownloadedApk(dir.appId, apk as File), silent: silent); | ||||||
|  |       } | ||||||
|  |       dir.file.delete(); | ||||||
|  |     } finally { | ||||||
|  |       dir.extracted.delete(recursive: true); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   Future<void> installApk(DownloadedApk file, {bool silent = false}) async { |   Future<void> installApk(DownloadedApk file, {bool silent = false}) async { | ||||||
|     // TODO: Use 'silent' when/if ever possible |     // TODO: Use 'silent' when/if ever possible | ||||||
|     var newInfo = await PackageArchiveInfo.fromPath(file.file.path); |     var newInfo = await PackageArchiveInfo.fromPath(file.file.path); | ||||||
| @@ -420,9 +484,16 @@ class AppsProvider with ChangeNotifier { | |||||||
|     for (var id in appsToInstall) { |     for (var id in appsToInstall) { | ||||||
|       try { |       try { | ||||||
|         // ignore: use_build_context_synchronously |         // ignore: use_build_context_synchronously | ||||||
|         var downloadedFile = await downloadApp(apps[id]!.app, context); |         var downloadedArtifact = await downloadApp(apps[id]!.app, context); | ||||||
|         bool willBeSilent = |         DownloadedApk? downloadedFile; | ||||||
|             await canInstallSilently(apps[downloadedFile.appId]!.app); |         DownloadedXApkDir? downloadedDir; | ||||||
|  |         if (downloadedArtifact is DownloadedApk) { | ||||||
|  |           downloadedFile = downloadedArtifact; | ||||||
|  |         } else { | ||||||
|  |           downloadedDir = downloadedArtifact as DownloadedXApkDir; | ||||||
|  |         } | ||||||
|  |         bool willBeSilent = await canInstallSilently( | ||||||
|  |             apps[downloadedFile?.appId ?? downloadedDir!.appId]!.app); | ||||||
|         willBeSilent = false; // TODO: Remove this when silent updates work |         willBeSilent = false; // TODO: Remove this when silent updates work | ||||||
|         if (!(await settingsProvider?.getInstallPermission(enforce: false) ?? |         if (!(await settingsProvider?.getInstallPermission(enforce: false) ?? | ||||||
|             true)) { |             true)) { | ||||||
| @@ -432,7 +503,11 @@ class AppsProvider with ChangeNotifier { | |||||||
|           // ignore: use_build_context_synchronously |           // ignore: use_build_context_synchronously | ||||||
|           await waitForUserToReturnToForeground(context); |           await waitForUserToReturnToForeground(context); | ||||||
|         } |         } | ||||||
|  |         if (downloadedFile != null) { | ||||||
|           await installApk(downloadedFile, silent: willBeSilent); |           await installApk(downloadedFile, silent: willBeSilent); | ||||||
|  |         } else { | ||||||
|  |           await installXApkDir(downloadedDir!, silent: willBeSilent); | ||||||
|  |         } | ||||||
|         installedIds.add(id); |         installedIds.add(id); | ||||||
|       } catch (e) { |       } catch (e) { | ||||||
|         errors.add(id, e.toString()); |         errors.add(id, e.toString()); | ||||||
| @@ -734,7 +809,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|             apps[i].installedVersion = null; |             apps[i].installedVersion = null; | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|         await saveApps(apps, attemptToCorrectInstallStatus: !remove); |         await saveApps(apps, attemptToCorrectInstallStatus: false); | ||||||
|       } |       } | ||||||
|       if (remove) { |       if (remove) { | ||||||
|         await removeApps(apps.map((e) => e.id).toList()); |         await removeApps(apps.map((e) => e.id).toList()); | ||||||
|   | |||||||
| @@ -35,6 +35,7 @@ List<int> updateIntervals = [15, 30, 60, 120, 180, 360, 720, 1440, 4320, 0] | |||||||
|  |  | ||||||
| class SettingsProvider with ChangeNotifier { | class SettingsProvider with ChangeNotifier { | ||||||
|   SharedPreferences? prefs; |   SharedPreferences? prefs; | ||||||
|  |   bool justStarted = true; | ||||||
|  |  | ||||||
|   String sourceUrl = 'https://github.com/ImranR98/Obtainium'; |   String sourceUrl = 'https://github.com/ImranR98/Obtainium'; | ||||||
|  |  | ||||||
| @@ -92,6 +93,15 @@ class SettingsProvider with ChangeNotifier { | |||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   bool get checkOnStart { | ||||||
|  |     return prefs?.getBool('checkOnStart') ?? false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   set checkOnStart(bool checkOnStart) { | ||||||
|  |     prefs?.setBool('checkOnStart', checkOnStart); | ||||||
|  |     notifyListeners(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   SortColumnSettings get sortColumn { |   SortColumnSettings get sortColumn { | ||||||
|     return SortColumnSettings.values[ |     return SortColumnSettings.values[ | ||||||
|         prefs?.getInt('sortColumn') ?? SortColumnSettings.nameAuthor.index]; |         prefs?.getInt('sortColumn') ?? SortColumnSettings.nameAuthor.index]; | ||||||
| @@ -120,6 +130,14 @@ class SettingsProvider with ChangeNotifier { | |||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   bool checkJustStarted() { | ||||||
|  |     if (justStarted) { | ||||||
|  |       justStarted = false; | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   Future<bool> getInstallPermission({bool enforce = false}) async { |   Future<bool> getInstallPermission({bool enforce = false}) async { | ||||||
|     while (!(await Permission.requestInstallPackages.isGranted)) { |     while (!(await Permission.requestInstallPackages.isGranted)) { | ||||||
|       // Explicit request as InstallPlugin request sometimes bugged |       // Explicit request as InstallPlugin request sometimes bugged | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -34,6 +34,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.7" |     version: "2.0.7" | ||||||
|  |   archive: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: archive | ||||||
|  |       sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "3.3.7" | ||||||
|   args: |   args: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -82,6 +90,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.17.0" |     version: "1.17.0" | ||||||
|  |   convert: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: convert | ||||||
|  |       sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "3.1.1" | ||||||
|   cross_file: |   cross_file: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -518,6 +534,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.4" |     version: "2.1.4" | ||||||
|  |   pointycastle: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: pointycastle | ||||||
|  |       sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "3.7.3" | ||||||
|   process: |   process: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | |||||||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | # 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.13.0+164 # When changing this, update the tag in main() accordingly | version: 0.13.1+165 # When changing this, update the tag in main() accordingly | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: '>=2.18.2 <3.0.0' |   sdk: '>=2.18.2 <3.0.0' | ||||||
| @@ -63,6 +63,7 @@ dependencies: | |||||||
|   easy_localization: ^3.0.1 |   easy_localization: ^3.0.1 | ||||||
|   android_intent_plus: ^3.1.5 |   android_intent_plus: ^3.1.5 | ||||||
|   flutter_markdown: ^0.6.14 |   flutter_markdown: ^0.6.14 | ||||||
|  |   archive: ^3.3.7 | ||||||
|  |  | ||||||
|  |  | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user