diff --git a/lib/app_sources/html.dart b/lib/app_sources/html.dart index d9ec173..c6ec9e2 100644 --- a/lib/app_sources/html.dart +++ b/lib/app_sources/html.dart @@ -89,6 +89,13 @@ class HTML extends AppSource { overrideEligible = true; } + @override + // TODO: implement requestHeaders choice, hardcoded for now + Map? get requestHeaders => { + "User-Agent": + "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0" + }; + @override String sourceSpecificStandardizeURL(String url) { return url; diff --git a/lib/main.dart b/lib/main.dart index b9044c4..900cd41 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart'; // ignore: implementation_imports import 'package:easy_localization/src/localization.dart'; -const String currentVersion = '0.13.1'; +const String currentVersion = '0.13.2'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index bdb18e4..ac85b10 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -283,6 +283,9 @@ class _AddAppPageState extends State { } si++; } + if (res.isEmpty) { + throw ObtainiumError(tr('noResults')); + } List? selectedUrls = res.isEmpty ? [] // ignore: use_build_context_synchronously @@ -377,13 +380,15 @@ class _AddAppPageState extends State { const SizedBox( width: 16, ), - ElevatedButton( - onPressed: searchQuery.isEmpty || doingSomething - ? null - : () { - runSearch(); - }, - child: Text(tr('search'))) + searching + ? const CircularProgressIndicator() + : ElevatedButton( + onPressed: searchQuery.isEmpty || doingSomething + ? null + : () { + runSearch(); + }, + child: Text(tr('search'))) ], ); diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 8f851e5..b7585c8 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -444,7 +444,9 @@ class _AppPageState extends State { Padding( padding: const EdgeInsets.fromLTRB(0, 8, 0, 0), child: LinearProgressIndicator( - value: app!.downloadProgress! / 100)) + value: app!.downloadProgress! >= 0 + ? app.downloadProgress! / 100 + : null)) ], )); diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 5fcee6a..37fa60c 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -542,8 +542,12 @@ class AppsPageState extends State { ? SizedBox( width: 110, child: Text(tr('percentProgress', args: [ - listedApps[index].downloadProgress?.toInt().toString() ?? - '100' + listedApps[index].downloadProgress! >= 0 + ? listedApps[index] + .downloadProgress! + .toInt() + .toString() + : tr('pleaseWait') ]))) : trailingRow, onTap: () { diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 3e6f5ea..d15c5e3 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -122,25 +122,34 @@ class AppsProvider with ChangeNotifier { // Load Apps into memory (in background, this is done later instead of in the constructor) await loadApps(); // Delete any partial APKs + var cutoff = DateTime.now().subtract(const Duration(days: 7)); (await getExternalCacheDirectories()) ?.first .listSync() - .where((element) => element.path.endsWith('.apk.part')) + .where((element) => + element.path.endsWith('.part') || + element.statSync().modified.isBefore(cutoff)) .forEach((partialApk) { partialApk.delete(); }); }(); } - downloadFile(String url, String fileName, Function? onProgress, + Future downloadFile( + String url, String fileNameNoExt, Function? onProgress, {bool useExisting = true, Map? headers}) async { var destDir = (await getExternalCacheDirectories())!.first.path; var req = Request('GET', Uri.parse(url)); if (headers != null) { req.headers.addAll(headers); } - StreamedResponse response = await Client().send(req); - File downloadedFile = File('$destDir/$fileName'); + var client = Client(); + StreamedResponse response = await client.send(req); + var ext = response.headers['content-disposition']!.split('.').last; + if (ext.endsWith('"') || ext.endsWith("other")) { + ext = ext.substring(0, ext.length - 1); + } + File downloadedFile = File('$destDir/$fileNameNoExt.$ext'); if (!(downloadedFile.existsSync() && useExisting)) { File tempDownloadedFile = File('${downloadedFile.path}.part'); if (tempDownloadedFile.existsSync()) { @@ -168,12 +177,14 @@ class AppsProvider with ChangeNotifier { throw response.reasonPhrase ?? tr('unexpectedError'); } tempDownloadedFile.renameSync(downloadedFile.path); + } else { + client.close(); } return downloadedFile; } - handleAPKIDChange(App app, PackageArchiveInfo newInfo, File downloadedFile, - String downloadUrl) async { + Future 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) { @@ -184,12 +195,13 @@ class AppsProvider with ChangeNotifier { var originalAppId = app.id; app.id = newInfo.packageName; downloadedFile = downloadedFile.renameSync( - '${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.apk'); + '${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.${downloadedFile.path.split('.').last}'); if (apps[originalAppId] != null) { await removeApps([originalAppId]); await saveApps([app], onlyIfExists: !isTempId); } } + return downloadedFile; } Future downloadApp(App app, BuildContext? context) async { @@ -205,11 +217,11 @@ class AppsProvider with ChangeNotifier { .getSource(app.url, overrideSource: app.overrideSource); String downloadUrl = await source.apkUrlPrefetchModifier( app.apkUrls[app.preferredApkIndex].value, app.url); - var fileName = '${app.id}-${downloadUrl.hashCode}.apk'; var notif = DownloadNotification(app.finalName, 100); notificationsProvider?.cancel(notif.id); int? prevProg; - File downloadedFile = await downloadFile(downloadUrl, fileName, + var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}'; + var downloadedFile = await downloadFile(downloadUrl, fileNameNoExt, headers: source.requestHeaders, (double? progress) { int? prog = progress?.ceil(); if (apps[app.id] != null) { @@ -222,18 +234,20 @@ class AppsProvider with ChangeNotifier { } prevProg = prog; }); - PackageArchiveInfo? newInfo; - try { - newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path); - } catch (e) { - // Assume it's an XAPK - fileName = '${app.id}-${downloadUrl.hashCode}.xapk'; - String newPath = '${downloadedFile.parent.path}/$fileName'; - downloadedFile.renameSync(newPath); - downloadedFile = File(newPath); + // Set to 90 for remaining steps, will make null in 'finally' + if (apps[app.id] != null) { + apps[app.id]!.downloadProgress = -1; + notifyListeners(); + notif = DownloadNotification(app.finalName, -1); + notificationsProvider?.notify(notif); } + PackageArchiveInfo? newInfo; + var isAPK = downloadedFile.path.toLowerCase().endsWith('.apk'); Directory? xapkDir; - if (newInfo == null) { + if (isAPK) { + newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path); + } else { + // Assume XAPK String xapkDirPath = '${downloadedFile.path}-dir'; unzipFile(downloadedFile.path, '${downloadedFile.path}-dir'); xapkDir = Directory(xapkDirPath); @@ -243,20 +257,21 @@ class AppsProvider with ChangeNotifier { .toList(); newInfo = await PackageArchiveInfo.fromPath(apks.first.path); } - await handleAPKIDChange(app, newInfo, downloadedFile, downloadUrl); + downloadedFile = + await handleAPKIDChange(app, newInfo, downloadedFile, downloadUrl); // Delete older versions of the file if any for (var file in downloadedFile.parent.listSync()) { var fn = file.path.split('/').last; if (fn.startsWith('${app.id}-') && - fn.toLowerCase().endsWith(xapkDir == null ? '.apk' : '.xapk') && - fn != downloadedFile.path.split('/').last) { + FileSystemEntity.isFileSync(file.path) && + file.path != downloadedFile.path) { file.delete(); } } - if (xapkDir != null) { - return DownloadedXApkDir(app.id, downloadedFile, xapkDir); - } else { + if (isAPK) { return DownloadedApk(app.id, downloadedFile); + } else { + return DownloadedXApkDir(app.id, downloadedFile, xapkDir!); } } finally { notificationsProvider?.cancel(notifId); @@ -324,18 +339,23 @@ class AppsProvider with ChangeNotifier { Future installXApkDir(DownloadedXApkDir dir, {bool silent = false}) async { try { + var somethingInstalled = false; 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); + somethingInstalled = somethingInstalled || + await installApk(DownloadedApk(dir.appId, apk as File), + silent: silent); + } + if (somethingInstalled) { + dir.file.delete(); } - dir.file.delete(); } finally { dir.extracted.delete(recursive: true); } } - Future installApk(DownloadedApk file, {bool silent = false}) async { + Future installApk(DownloadedApk file, {bool silent = false}) async { // TODO: Use 'silent' when/if ever possible var newInfo = await PackageArchiveInfo.fromPath(file.file.path); AppInfo? appInfo; @@ -351,14 +371,17 @@ class AppsProvider with ChangeNotifier { } int? code = await AndroidPackageInstaller.installApk(apkFilePath: file.file.path); + bool installed = false; if (code != null && code != 0 && code != 3) { throw InstallError(code); } else if (code == 0) { + installed = true; apps[file.appId]!.app.installedVersion = apps[file.appId]!.app.latestVersion; file.file.delete(); } await saveApps([apps[file.appId]!.app]); + return installed; } void uninstallApp(String appId) async { @@ -503,10 +526,17 @@ class AppsProvider with ChangeNotifier { // ignore: use_build_context_synchronously await waitForUserToReturnToForeground(context); } - if (downloadedFile != null) { - await installApk(downloadedFile, silent: willBeSilent); - } else { - await installXApkDir(downloadedDir!, silent: willBeSilent); + apps[id]?.downloadProgress = -1; + notifyListeners(); + try { + if (downloadedFile != null) { + await installApk(downloadedFile, silent: willBeSilent); + } else { + await installXApkDir(downloadedDir!, silent: willBeSilent); + } + } finally { + apps[id]?.downloadProgress = null; + notifyListeners(); } installedIds.add(id); } catch (e) { @@ -759,11 +789,18 @@ class AppsProvider with ChangeNotifier { } Future removeApps(List appIds) async { + var apkFiles = (await getExternalCacheDirectories())?.first.listSync(); for (var appId in appIds) { File file = File('${(await getAppsDir()).path}/$appId.json'); if (file.existsSync()) { file.deleteSync(); } + apkFiles + ?.where( + (element) => element.path.split('/').last.startsWith('$appId-')) + .forEach((element) { + element.delete(); + }); if (apps.containsKey(appId)) { apps.remove(appId); } diff --git a/lib/providers/notifications_provider.dart b/lib/providers/notifications_provider.dart index f48b09a..1f39cc8 100644 --- a/lib/providers/notifications_provider.dart +++ b/lib/providers/notifications_provider.dart @@ -167,7 +167,8 @@ class NotificationsProvider { progress: progPercent ?? 0, maxProgress: 100, showProgress: progPercent != null, - onlyAlertOnce: onlyAlertOnce))); + onlyAlertOnce: onlyAlertOnce, + indeterminate: progPercent != null && progPercent < 0))); } Future notify(ObtainiumNotification notif, diff --git a/pubspec.lock b/pubspec.lock index c5513ac..32e6c98 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -426,10 +426,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4 + sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" url: "https://pub.dev" source: hosted - version: "2.0.14" + version: "2.0.15" path_provider_android: dependency: transitive description: @@ -442,10 +442,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183 + sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" path_provider_linux: dependency: transitive description: @@ -578,10 +578,10 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b" + sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" shared_preferences_android: dependency: transitive description: @@ -594,10 +594,10 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07" + sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_linux: dependency: transitive description: @@ -647,10 +647,10 @@ packages: dependency: "direct main" description: name: sqflite - sha256: acf091c6e55c50d00b30b8532b2dd23e393cf775861665ebd0f15cdd6ebfb079 + sha256: "3a82c9a216b46b88617e3714dd74227eaca20c501c4abcc213e56db26b9caa00" url: "https://pub.dev" source: hosted - version: "2.2.8+1" + version: "2.2.8+2" sqflite_common: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5d786c4..730cb10 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 # 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.13.1+165 # When changing this, update the tag in main() accordingly +version: 0.13.2+166 # When changing this, update the tag in main() accordingly environment: sdk: '>=2.18.2 <3.0.0'