diff --git a/lib/app_sources/directAPKLink.dart b/lib/app_sources/directAPKLink.dart index d17b3d7..5fc4d3a 100644 --- a/lib/app_sources/directAPKLink.dart +++ b/lib/app_sources/directAPKLink.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:obtainium/app_sources/html.dart'; +import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/providers/source_provider.dart'; @@ -8,12 +9,23 @@ class DirectAPKLink extends AppSource { DirectAPKLink() { name = tr('directAPKLink'); - additionalSourceAppSpecificSettingFormItems = html - .additionalSourceAppSpecificSettingFormItems - .where((element) => element - .where((element) => element.key == 'requestHeader') - .isNotEmpty) - .toList(); + additionalSourceAppSpecificSettingFormItems = [ + ...html.additionalSourceAppSpecificSettingFormItems + .where((element) => element + .where((element) => element.key == 'requestHeader') + .isNotEmpty) + .toList(), + [ + GeneratedFormDropdown( + 'defaultPseudoVersioningMethod', + [ + MapEntry('partialAPKHash', tr('partialAPKHash')), + MapEntry('ETag', 'ETag') + ], + label: tr('defaultPseudoVersioningMethod'), + defaultValue: 'partialAPKHash') + ] + ]; excludeCommonSettingKeys = [ 'versionExtractionRegEx', 'matchGroupToUse', @@ -57,9 +69,8 @@ class DirectAPKLink extends AppSource { additionalSettingsNew[s] = additionalSettings[s]; } } - additionalSettingsNew['defaultPseudoVersioningMethod'] = 'partialAPKHash'; additionalSettingsNew['directAPKLink'] = true; - additionalSettings['versionDetection'] = false; + additionalSettingsNew['versionDetection'] = false; return html.getLatestAPKDetails(standardUrl, additionalSettingsNew); } } diff --git a/lib/app_sources/html.dart b/lib/app_sources/html.dart index b05dc7f..6c8269c 100644 --- a/lib/app_sources/html.dart +++ b/lib/app_sources/html.dart @@ -263,7 +263,8 @@ class HTML extends AppSource { 'defaultPseudoVersioningMethod', [ MapEntry('partialAPKHash', tr('partialAPKHash')), - MapEntry('APKLinkHash', tr('APKLinkHash')) + MapEntry('APKLinkHash', tr('APKLinkHash')), + MapEntry('ETag', 'ETag') ], label: tr('defaultPseudoVersioningMethod'), defaultValue: 'partialAPKHash') @@ -356,14 +357,24 @@ class HTML extends AppSource { additionalSettings['versionExtractWholePage'] == true ? versionExtractionWholePageString : relDecoded); - version ??= additionalSettings['defaultPseudoVersioningMethod'] == - 'APKLinkHash' - ? rel.hashCode.toString() - : (await checkPartialDownloadHashDynamic(rel, - headers: await getRequestHeaders(additionalSettings, - forAPKDownload: true), - allowInsecure: additionalSettings['allowInsecure'] == true)) - .toString(); + var apkReqHeaders = + await getRequestHeaders(additionalSettings, forAPKDownload: true); + if (version == null && + additionalSettings['defaultPseudoVersioningMethod'] == 'ETag') { + version = await checkETagHeader(rel, + headers: apkReqHeaders, + allowInsecure: additionalSettings['allowInsecure'] == true); + if (version == null) { + throw NoVersionError(); + } + } + version ??= + additionalSettings['defaultPseudoVersioningMethod'] == 'APKLinkHash' + ? rel.hashCode.toString() + : (await checkPartialDownloadHashDynamic(rel, + headers: apkReqHeaders, + allowInsecure: additionalSettings['allowInsecure'] == true)) + .toString(); return APKDetails( version, [rel].map((e) { diff --git a/lib/main.dart b/lib/main.dart index 4b20795..bd13f5d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -46,6 +46,7 @@ List> supportedLocales = const [ 'Esperanto'), // https://github.com/aissat/easy_localization/issues/220#issuecomment-846035493 MapEntry(Locale('in'), 'Bahasa Indonesia'), MapEntry(Locale('ko'), '한국어'), + MapEntry(Locale('ca'), 'Català'), ]; const fallbackLocale = Locale('en'); const localeDir = 'assets/translations'; diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index 4c552c2..41d9f9c 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -575,8 +575,10 @@ class AddAppPageState extends State { Widget getSourcesListWidget() => Padding( padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.spaceBetween, + spacing: 12, children: [ GestureDetector( onTap: () { diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 940a2b6..833f1c7 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -220,6 +220,22 @@ Future checkPartialDownloadHash(String url, int bytesToGrab, return hashListOfLists(bytes); } +Future checkETagHeader(String url, + {Map? headers, bool allowInsecure = false}) async { + // Send the initial request but cancel it as soon as you have the headers + var reqHeaders = headers ?? {}; + var req = Request('GET', Uri.parse(url)); + req.headers.addAll(reqHeaders); + var client = IOClient(createHttpClient(allowInsecure)); + StreamedResponse response = await client.send(req); + var resHeaders = response.headers; + client.close(); + return resHeaders[HttpHeaders.etagHeader] + ?.replaceAll('"', '') + .hashCode + .toString(); +} + Future downloadFile(String url, String fileName, bool fileNameHasExt, Function? onProgress, String destDir, {bool useExisting = true, diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 89e6c28..f9cec27 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -3,12 +3,13 @@ import 'dart:convert'; import 'dart:io'; +import 'package:http/http.dart' as http; +import 'dart:typed_data'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:html/dom.dart'; import 'package:http/http.dart'; -import 'package:http/io_client.dart'; import 'package:obtainium/app_sources/apkmirror.dart'; import 'package:obtainium/app_sources/apkpure.dart'; import 'package:obtainium/app_sources/aptoide.dart'; @@ -566,23 +567,62 @@ abstract class AppSource { String url, Map additionalSettings, {bool followRedirects = true, Object? postBody}) async { var requestHeaders = await getRequestHeaders(additionalSettings); + if (requestHeaders != null || followRedirects == false) { - var req = Request(postBody == null ? 'GET' : 'POST', Uri.parse(url)); - req.followRedirects = followRedirects; - if (requestHeaders != null) { - req.headers.addAll(requestHeaders); + var method = postBody == null ? 'GET' : 'POST'; + var currentUrl = url; + var redirectCount = 0; + const maxRedirects = 10; + while (redirectCount < maxRedirects) { + var httpClient = + createHttpClient(additionalSettings['allowInsecure'] == true); + var request = await httpClient.openUrl(method, Uri.parse(currentUrl)); + if (requestHeaders != null) { + requestHeaders.forEach((key, value) { + request.headers.set(key, value); + }); + } + request.followRedirects = false; + if (postBody != null) { + request.headers.contentType = ContentType.json; + request.write(jsonEncode(postBody)); + } + final response = await request.close(); + + if (followRedirects && + (response.statusCode == 301 || response.statusCode == 302)) { + final location = response.headers.value(HttpHeaders.locationHeader); + if (location != null) { + currentUrl = location; + redirectCount++; + httpClient.close(); + continue; + } + } + + final bytes = (await response.fold( + BytesBuilder(), (b, d) => b..add(d))) + .toBytes(); + + final headers = {}; + response.headers.forEach((name, values) { + headers[name] = values.join(', '); + }); + + httpClient.close(); + + return http.Response.bytes( + bytes, + response.statusCode, + headers: headers, + request: http.Request(method, Uri.parse(url)), + ); } - if (postBody != null) { - req.headers[HttpHeaders.contentTypeHeader] = 'application/json'; - req.body = jsonEncode(postBody); - } - return Response.fromStream(await IOClient( - createHttpClient(additionalSettings['allowInsecure'] == true)) - .send(req)); + throw ObtainiumError('Too many redirects ($maxRedirects)'); } else { return postBody == null - ? get(Uri.parse(url)) - : post(Uri.parse(url), body: jsonEncode(postBody)); + ? http.get(Uri.parse(url)) + : http.post(Uri.parse(url), body: jsonEncode(postBody)); } } diff --git a/pubspec.lock b/pubspec.lock index 7994f21..fdbe19d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -304,10 +304,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "09b474c0c8117484b80cbebc043801ff91e05cfbd2874d512825c899e1754694" + sha256: "36a1652d99cb6bf8ccc8b9f43aded1fd60b234d23ce78af422c07f950a436ef7" url: "https://pub.dev" source: hosted - version: "9.2.3" + version: "10.0.0" fixnum: dependency: transitive description: @@ -490,10 +490,10 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5 + sha256: "634622a3a826d67cb05c0e3e576d1812c430fa98404e95b60b131775c73d76ec" url: "https://pub.dev" source: hosted - version: "0.7.6+2" + version: "0.7.7" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -1107,10 +1107,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" url_launcher_linux: dependency: transitive description: @@ -1211,10 +1211,10 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: c49a98510080378b1525132f407a92c3dcd3b7145bef04fb8137724aadcf1cf0 + sha256: c14455137ce60a68e1ccaf4e8f2dae8cebcb3465ddaa2fcfb57584fb7c5afe4d url: "https://pub.dev" source: hosted - version: "3.18.4" + version: "3.18.5" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 930c598..a2934e4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,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: 1.1.48+2305 +version: 1.1.49+2306 environment: sdk: ^3.6.0 @@ -47,7 +47,7 @@ dependencies: permission_handler: ^11.0.0 fluttertoast: ^8.0.9 device_info_plus: ^11.0.0 - file_picker: ^9.0.0 + file_picker: ^10.0.0 animations: ^2.0.4 android_package_installer: # TODO: See if PR will be accepted (dev may not be active), else remove this comment git: