mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-31 05:23:28 +01:00 
			
		
		
		
	Add XAPK support (incomplete - OBB not copied)
This commit is contained in:
		| @@ -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') | ||||||
|   | |||||||
| @@ -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')); | ||||||
|   | |||||||
| @@ -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); | ||||||
|         var originalAppId = app.id; |         downloadedFile = File(newPath); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |       } | ||||||
|       // Delete older versions of the APK if any |       Directory? xapkDir; | ||||||
|  |       if (newInfo == null) { | ||||||
|  |         String xapkDirPath = '${downloadedFile.path}-dir'; | ||||||
|  |         unzipFile(downloadedFile.path, '${downloadedFile.path}-dir'); | ||||||
|  |         xapkDir = Directory(xapkDirPath); | ||||||
|  |         var apks = xapkDir | ||||||
|  |             .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 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(); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       return DownloadedApk(app.id, downloadedFile); |       if (xapkDir != null) { | ||||||
|  |         return DownloadedXApkDir(app.id, downloadedFile, xapkDir); | ||||||
|  |       } else { | ||||||
|  |         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); | ||||||
|         } |         } | ||||||
|         await installApk(downloadedFile, silent: willBeSilent); |         if (downloadedFile != null) { | ||||||
|  |           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()); | ||||||
|   | |||||||
							
								
								
									
										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: | ||||||
|   | |||||||
| @@ -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