diff --git a/lib/main.dart b/lib/main.dart index 4d2bb12..7de6103 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,7 +9,7 @@ import 'package:provider/provider.dart'; import 'package:workmanager/workmanager.dart'; import 'package:dynamic_color/dynamic_color.dart'; -const String CURRENT_RELEASE_TAG = +const String currentReleaseTag = 'v0.1.3-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES @pragma('vm:entry-point') @@ -101,9 +101,8 @@ class MyApp extends StatelessWidget { 'https://github.com/ImranR98/Obtainium', 'ImranR98', 'Obtainium', - CURRENT_RELEASE_TAG, - CURRENT_RELEASE_TAG, - '')); + currentReleaseTag, + currentReleaseTag, [])); } appsProvider.deleteSavedAPKs(); appsProvider.checkUpdates(); diff --git a/lib/pages/app.dart b/lib/pages/app.dart index b922c11..f938549 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -48,7 +48,7 @@ class _AppPageState extends State { ? () { appsProvider .downloadAndInstallLatestApp( - app!.app.id); + app!.app.id, context); } : null, child: Text(app?.app.installedVersion == null diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 0ad433a..59790be 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -26,7 +26,7 @@ class _AppsPageState extends State { ? null : () { for (var e in existingUpdateAppIds) { - appsProvider.downloadAndInstallLatestApp(e); + appsProvider.downloadAndInstallLatestApp(e, context); } }, icon: const Icon(Icons.update), diff --git a/lib/services/apps_provider.dart b/lib/services/apps_provider.dart index 6380291..aa497a3 100644 --- a/lib/services/apps_provider.dart +++ b/lib/services/apps_provider.dart @@ -68,12 +68,13 @@ class AppsProvider with ChangeNotifier { } // Given a App (assumed valid), initiate an APK download (will trigger install callback when complete) - Future downloadAndInstallLatestApp(String appId) async { + Future downloadAndInstallLatestApp( + String appId, BuildContext context) async { if (apps[appId] == null) { throw 'App not found'; } - StreamedResponse response = - await Client().send(Request('GET', Uri.parse(apps[appId]!.app.apkUrl))); + StreamedResponse response = await Client() + .send(Request('GET', Uri.parse(apps[appId]!.app.apkUrls[0]))); File downloadFile = File('${(await getExternalStorageDirectory())!.path}/$appId.apk'); if (downloadFile.existsSync()) { @@ -244,14 +245,14 @@ class AppsProvider with ChangeNotifier { path = exportDir!.path; } File export = File( - '${exportDir!.path}/obtainium-export-${DateTime.now().millisecondsSinceEpoch}.json'); + '${exportDir.path}/obtainium-export-${DateTime.now().millisecondsSinceEpoch}.json'); export.writeAsStringSync( jsonEncode(apps.values.map((e) => e.app.toJson()).toList())); return path; } Future importApps(String appsJSON) async { - // FilePickerResult? result = await FilePicker.platform.pickFiles(); + // FilePickerResult? result = await FilePicker.platform.pickFiles(); // Does not work on Android 13 // if (result != null) { // String appsJSON = File(result.files.single.path!).readAsStringSync(); diff --git a/lib/services/source_service.dart b/lib/services/source_service.dart index 9f08b51..4fb0d76 100644 --- a/lib/services/source_service.dart +++ b/lib/services/source_service.dart @@ -1,6 +1,9 @@ // Exposes functions related to interacting with App sources and retrieving App info // Stateless - not a provider +import 'dart:convert'; + +import 'package:html/dom.dart'; import 'package:http/http.dart'; import 'package:html/parser.dart'; @@ -15,9 +18,9 @@ class AppNames { class APKDetails { late String version; - late String downloadUrl; + late List apkUrls; - APKDetails(this.version, this.downloadUrl); + APKDetails(this.version, this.apkUrls); } // App Source abstract class (diff. implementations for GitHub, GitLab, etc.) @@ -35,6 +38,17 @@ escapeRegEx(String s) { }); } +List getLinksFromParsedHTML( + Document dom, RegExp hrefPattern, String prependToLinks) => + dom + .querySelectorAll('a') + .where((element) { + if (element.attributes['href'] == null) return false; + return hrefPattern.hasMatch(element.attributes['href']!); + }) + .map((e) => '$prependToLinks${e.attributes['href']!}') + .toList(); + // App class class App { @@ -44,13 +58,13 @@ class App { late String name; String? installedVersion; late String latestVersion; - late String apkUrl; + List apkUrls = []; App(this.id, this.url, this.author, this.name, this.installedVersion, - this.latestVersion, this.apkUrl); + this.latestVersion, this.apkUrls); @override String toString() { - return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrl'; + return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls'; } factory App.fromJson(Map json) => App( @@ -62,7 +76,7 @@ class App { ? null : json['installedVersion'] as String, json['latestVersion'] as String, - json['apkUrl'] as String); + List.from(jsonDecode(json['apkUrls']))); Map toJson() => { 'id': id, @@ -71,7 +85,7 @@ class App { 'name': name, 'installedVersion': installedVersion, 'latestVersion': latestVersion, - 'apkUrl': apkUrl, + 'apkUrls': jsonEncode(apkUrls), }; } @@ -98,23 +112,31 @@ class GitHub implements AppSource { if (res.statusCode == 200) { var standardUri = Uri.parse(standardUrl); var parsedHtml = parse(res.body); - var apkUrlList = parsedHtml.querySelectorAll('a').where((element) { - if (element.attributes['href'] == null) return false; - return RegExp( - '^${escapeRegEx(standardUri.path)}/releases/download/[^/]+/[^/]+\\.apk\$', - caseSensitive: false) - .hasMatch(element.attributes['href']!); - }).toList(); + var apkUrlList = getLinksFromParsedHTML( + parsedHtml, + RegExp( + '^${escapeRegEx(standardUri.path)}/releases/download/[^/]+/[^/]+\\.apk\$', + caseSensitive: false), + standardUri.origin); + if (apkUrlList.isEmpty) { + throw 'No APK found'; + } + String getTag(String url) { + List parts = url.split('/'); + return parts[parts.length - 2]; + } + + String latestTag = getTag(apkUrlList[0]); String? version = parsedHtml .querySelector('.octicon-tag') ?.nextElementSibling ?.innerHtml .trim(); - if (apkUrlList.isEmpty || version == null) { - throw 'No APK found'; + if (version == null) { + throw 'Could not determine latest release version'; } - return APKDetails( - version, '${standardUri.origin}${apkUrlList[0].attributes['href']!}'); + return APKDetails(version, + apkUrlList.where((element) => getTag(element) == latestTag).toList()); } else { throw 'Unable to fetch release info'; } @@ -152,21 +174,23 @@ class GitLab implements AppSource { var entry = parsedHtml.querySelector('entry'); var entryContent = parse(parseFragment(entry!.querySelector('content')!.innerHtml).text); - var apkUrlList = entryContent.querySelectorAll('a').where((element) { - if (element.attributes['href'] == null) return false; - return RegExp( - '^${escapeRegEx(standardUri.path)}/uploads/[^/]+/[^/]+\\.apk\$', - caseSensitive: false) - .hasMatch(element.attributes['href']!); - }).toList(); + var apkUrlList = getLinksFromParsedHTML( + entryContent, + RegExp( + '^${escapeRegEx(standardUri.path)}/uploads/[^/]+/[^/]+\\.apk\$', + caseSensitive: false), + standardUri.origin); + if (apkUrlList.isEmpty) { + throw 'No APK found'; + } + var entryId = entry.querySelector('id')?.innerHtml; var version = entryId == null ? null : Uri.parse(entryId).pathSegments.last; - if (apkUrlList.isEmpty || version == null) { - throw 'No APK found'; + if (version == null) { + throw 'Could not determine latest release version'; } - return APKDetails( - version, '${standardUri.origin}${apkUrlList[0].attributes['href']!}'); + return APKDetails(version, apkUrlList); } else { throw 'Unable to fetch release info'; } @@ -203,12 +227,12 @@ class SourceService { AppNames names = source.getAppNames(standardUrl); APKDetails apk = await source.getLatestAPKDetails(standardUrl); return App( - '${names.author}_${names.name}_${source.sourceId}', + '${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.sourceId}', standardUrl, names.author[0].toUpperCase() + names.author.substring(1), names.name[0].toUpperCase() + names.name.substring(1), null, apk.version, - apk.downloadUrl); + apk.apkUrls); } }