From aa8d45e63659a380ea70e36e9d63a82c7d7bc4a7 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 14 Oct 2023 01:29:02 -0400 Subject: [PATCH] Make Third Party F-Droid Repos Searchable (#995) --- lib/app_sources/fdroidrepo.dart | 83 ++++++++++++++++++++++++++++-- lib/pages/add_app.dart | 2 +- lib/providers/apps_provider.dart | 8 +-- lib/providers/source_provider.dart | 17 +++--- 4 files changed, 95 insertions(+), 15 deletions(-) diff --git a/lib/app_sources/fdroidrepo.dart b/lib/app_sources/fdroidrepo.dart index 31f8435..22b8e49 100644 --- a/lib/app_sources/fdroidrepo.dart +++ b/lib/app_sources/fdroidrepo.dart @@ -7,6 +7,7 @@ import 'package:obtainium/providers/source_provider.dart'; class FDroidRepo extends AppSource { FDroidRepo() { name = tr('fdroidThirdPartyRepo'); + canSearch = true; additionalSourceAppSpecificSettingFormItems = [ [ @@ -22,12 +23,85 @@ class FDroidRepo extends AppSource { ]; } + String removeQueryParamsFromUrl(String url, {List keep = const []}) { + var uri = Uri.parse(url); + Map resultParams = {}; + uri.queryParameters.forEach((key, value) { + if (keep.contains(key)) { + resultParams[key] = value; + } + }); + url = uri.replace(queryParameters: resultParams).toString(); + if (url.endsWith('?')) { + url = url.substring(0, url.length - 1); + } + return url; + } + + @override + String sourceSpecificStandardizeURL(String url) { + var standardUri = Uri.parse(url); + var pathSegments = standardUri.pathSegments; + if (pathSegments.last == 'index.xml') { + pathSegments.removeLast(); + standardUri = standardUri.replace(path: pathSegments.join('/')); + } + return removeQueryParamsFromUrl(standardUri.toString(), keep: ['appId']); + } + + @override + Future>> search(String query, + {Map querySettings = const {}}) async { + query = removeQueryParamsFromUrl(standardizeUrl(query)); + var res = await sourceRequest('$query/index.xml'); + if (res.statusCode == 200) { + var body = parse(res.body); + Map> results = {}; + body.querySelectorAll('application').toList().forEach((app) { + String appId = app.attributes['id']!; + results['$query?appId=$appId'] = [ + app.querySelector('name')?.innerHtml ?? appId, + app.querySelector('desc')?.innerHtml ?? '' + ]; + }); + return results; + } else { + throw getObtainiumHttpError(res); + } + } + + @override + App endOfGetAppChanges(App app) { + var uri = Uri.parse(app.url); + String? appId; + if (!isTempId(app)) { + appId = app.id; + } else if (uri.queryParameters['appId'] != null) { + appId = uri.queryParameters['appId']; + } + if (appId != null) { + app.url = uri + .replace( + queryParameters: Map.fromEntries( + [...uri.queryParameters.entries, MapEntry('appId', appId)])) + .toString(); + app.additionalSettings['appIdOrName'] = appId; + app.id = appId; + } + return app; + } + @override Future getLatestAPKDetails( String standardUrl, Map additionalSettings, ) async { String? appIdOrName = additionalSettings['appIdOrName']; + var standardUri = Uri.parse(standardUrl); + if (standardUri.queryParameters['appId'] != null) { + appIdOrName = standardUri.queryParameters['appId']; + } + standardUrl = removeQueryParamsFromUrl(standardUrl); bool pickHighestVersionCode = additionalSettings['pickHighestVersionCode']; if (appIdOrName == null) { throw NoReleasesError(); @@ -41,7 +115,7 @@ class FDroidRepo extends AppSource { if (foundApps.isEmpty) { foundApps = body.querySelectorAll('application').where((element) { return element.querySelector('name')?.innerHtml.toLowerCase() == - appIdOrName.toLowerCase(); + appIdOrName!.toLowerCase(); }).toList(); } if (foundApps.isEmpty) { @@ -50,7 +124,7 @@ class FDroidRepo extends AppSource { .querySelector('name') ?.innerHtml .toLowerCase() - .contains(appIdOrName.toLowerCase()) ?? + .contains(appIdOrName!.toLowerCase()) ?? false; }).toList(); } @@ -58,8 +132,9 @@ class FDroidRepo extends AppSource { throw ObtainiumError(tr('appWithIdOrNameNotFound')); } var authorName = body.querySelector('repo')?.attributes['name'] ?? name; - var appName = - foundApps[0].querySelector('name')?.innerHtml ?? appIdOrName; + String appId = foundApps[0].attributes['id']!; + foundApps[0].querySelector('name')?.innerHtml ?? appId; + var appName = foundApps[0].querySelector('name')?.innerHtml ?? appId; var releases = foundApps[0].querySelectorAll('package'); String? latestVersion = releases[0].querySelector('version')?.innerHtml; String? added = releases[0].querySelector('added')?.innerHtml; diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index c792a2a..bbe82ad 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -153,7 +153,7 @@ class _AddAppPageState extends State { overrideSource: pickedSourceOverride, inferAppIdIfOptional: inferAppIdIfOptional); // Only download the APK here if you need to for the package ID - if (sourceProvider.isTempId(app) && + if (isTempId(app) && app.additionalSettings['trackOnly'] != true) { // ignore: use_build_context_synchronously var apkUrl = await appsProvider.confirmApkUrl(app, context); diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index b84306d..3ef8e73 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -267,10 +267,10 @@ class AppsProvider with ChangeNotifier { 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 - var isTempId = SourceProvider().isTempId(app); + var isTempIdBool = isTempId(app); if (newInfo != null) { if (app.id != newInfo.packageName) { - if (apps[app.id] != null && !isTempId && !app.allowIdChange) { + if (apps[app.id] != null && !isTempIdBool && !app.allowIdChange) { throw IDChangedError(newInfo.packageName!); } var idChangeWasAllowed = app.allowIdChange; @@ -281,10 +281,10 @@ class AppsProvider with ChangeNotifier { '${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.${downloadedFile.path.split('.').last}'); if (apps[originalAppId] != null) { await removeApps([originalAppId]); - await saveApps([app], onlyIfExists: !isTempId && !idChangeWasAllowed); + await saveApps([app], onlyIfExists: !isTempIdBool && !idChangeWasAllowed); } } - } else if (isTempId) { + } else if (isTempIdBool) { throw ObtainiumError('Could not get ID from APK'); } return downloadedFile; diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 0ac9b91..e3e1c3a 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -372,6 +372,10 @@ abstract class AppSource { return null; } + App endOfGetAppChanges(App app) { + return app; + } + Future sourceRequest(String url, {bool followRedirects = true, Map additionalSettings = @@ -541,6 +545,11 @@ intValidator(String? value, {bool positive = false}) { return null; } +bool isTempId(App app) { + // return app.id == generateTempID(app.url, app.additionalSettings); + return RegExp('^[0-9]+\$').hasMatch(app.id); +} + class SourceProvider { // Add more source classes here so they are available via the service List get sources => [ @@ -626,11 +635,6 @@ class SourceProvider { String standardUrl, Map additionalSettings) => (standardUrl + additionalSettings.toString()).hashCode.toString(); - bool isTempId(App app) { - // return app.id == generateTempID(app.url, app.additionalSettings); - return RegExp('^[0-9]+\$').hasMatch(app.id); - } - Future getApp( AppSource source, String url, Map additionalSettings, {App? currentApp, @@ -672,7 +676,7 @@ class SourceProvider { String apkVersion = apk.version.replaceAll('/', '-'); var name = currentApp != null ? currentApp.name.trim() : ''; name = name.isNotEmpty ? name : apk.names.name; - return App( + App finalApp = App( currentApp?.id ?? ((!source.appIdInferIsOptional || (source.appIdInferIsOptional && inferAppIdIfOptional)) @@ -698,6 +702,7 @@ class SourceProvider { source.appIdInferIsOptional && inferAppIdIfOptional // Optional ID inferring may be incorrect - allow correction on first install ); + return source.endOfGetAppChanges(finalApp); } // Returns errors in [results, errors] instead of throwing them