Various bugfixes + prep for multiple apk support

This commit is contained in:
Imran Remtulla
2022-08-26 21:36:52 -04:00
parent ecb1e7d367
commit a6f290eb59
5 changed files with 66 additions and 42 deletions

View File

@@ -9,7 +9,7 @@ import 'package:provider/provider.dart';
import 'package:workmanager/workmanager.dart'; import 'package:workmanager/workmanager.dart';
import 'package:dynamic_color/dynamic_color.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 'v0.1.3-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
@pragma('vm:entry-point') @pragma('vm:entry-point')
@@ -101,9 +101,8 @@ class MyApp extends StatelessWidget {
'https://github.com/ImranR98/Obtainium', 'https://github.com/ImranR98/Obtainium',
'ImranR98', 'ImranR98',
'Obtainium', 'Obtainium',
CURRENT_RELEASE_TAG, currentReleaseTag,
CURRENT_RELEASE_TAG, currentReleaseTag, []));
''));
} }
appsProvider.deleteSavedAPKs(); appsProvider.deleteSavedAPKs();
appsProvider.checkUpdates(); appsProvider.checkUpdates();

View File

@@ -48,7 +48,7 @@ class _AppPageState extends State<AppPage> {
? () { ? () {
appsProvider appsProvider
.downloadAndInstallLatestApp( .downloadAndInstallLatestApp(
app!.app.id); app!.app.id, context);
} }
: null, : null,
child: Text(app?.app.installedVersion == null child: Text(app?.app.installedVersion == null

View File

@@ -26,7 +26,7 @@ class _AppsPageState extends State<AppsPage> {
? null ? null
: () { : () {
for (var e in existingUpdateAppIds) { for (var e in existingUpdateAppIds) {
appsProvider.downloadAndInstallLatestApp(e); appsProvider.downloadAndInstallLatestApp(e, context);
} }
}, },
icon: const Icon(Icons.update), icon: const Icon(Icons.update),

View File

@@ -68,12 +68,13 @@ class AppsProvider with ChangeNotifier {
} }
// Given a App (assumed valid), initiate an APK download (will trigger install callback when complete) // Given a App (assumed valid), initiate an APK download (will trigger install callback when complete)
Future<void> downloadAndInstallLatestApp(String appId) async { Future<void> downloadAndInstallLatestApp(
String appId, BuildContext context) async {
if (apps[appId] == null) { if (apps[appId] == null) {
throw 'App not found'; throw 'App not found';
} }
StreamedResponse response = StreamedResponse response = await Client()
await Client().send(Request('GET', Uri.parse(apps[appId]!.app.apkUrl))); .send(Request('GET', Uri.parse(apps[appId]!.app.apkUrls[0])));
File downloadFile = File downloadFile =
File('${(await getExternalStorageDirectory())!.path}/$appId.apk'); File('${(await getExternalStorageDirectory())!.path}/$appId.apk');
if (downloadFile.existsSync()) { if (downloadFile.existsSync()) {
@@ -244,14 +245,14 @@ class AppsProvider with ChangeNotifier {
path = exportDir!.path; path = exportDir!.path;
} }
File export = File( File export = File(
'${exportDir!.path}/obtainium-export-${DateTime.now().millisecondsSinceEpoch}.json'); '${exportDir.path}/obtainium-export-${DateTime.now().millisecondsSinceEpoch}.json');
export.writeAsStringSync( export.writeAsStringSync(
jsonEncode(apps.values.map((e) => e.app.toJson()).toList())); jsonEncode(apps.values.map((e) => e.app.toJson()).toList()));
return path; return path;
} }
Future<int> importApps(String appsJSON) async { Future<int> 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) { // if (result != null) {
// String appsJSON = File(result.files.single.path!).readAsStringSync(); // String appsJSON = File(result.files.single.path!).readAsStringSync();

View File

@@ -1,6 +1,9 @@
// Exposes functions related to interacting with App sources and retrieving App info // Exposes functions related to interacting with App sources and retrieving App info
// Stateless - not a provider // Stateless - not a provider
import 'dart:convert';
import 'package:html/dom.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
@@ -15,9 +18,9 @@ class AppNames {
class APKDetails { class APKDetails {
late String version; late String version;
late String downloadUrl; late List<String> apkUrls;
APKDetails(this.version, this.downloadUrl); APKDetails(this.version, this.apkUrls);
} }
// App Source abstract class (diff. implementations for GitHub, GitLab, etc.) // App Source abstract class (diff. implementations for GitHub, GitLab, etc.)
@@ -35,6 +38,17 @@ escapeRegEx(String s) {
}); });
} }
List<String> 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 // App class
class App { class App {
@@ -44,13 +58,13 @@ class App {
late String name; late String name;
String? installedVersion; String? installedVersion;
late String latestVersion; late String latestVersion;
late String apkUrl; List<String> apkUrls = [];
App(this.id, this.url, this.author, this.name, this.installedVersion, App(this.id, this.url, this.author, this.name, this.installedVersion,
this.latestVersion, this.apkUrl); this.latestVersion, this.apkUrls);
@override @override
String toString() { 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<String, dynamic> json) => App( factory App.fromJson(Map<String, dynamic> json) => App(
@@ -62,7 +76,7 @@ class App {
? null ? null
: json['installedVersion'] as String, : json['installedVersion'] as String,
json['latestVersion'] as String, json['latestVersion'] as String,
json['apkUrl'] as String); List<String>.from(jsonDecode(json['apkUrls'])));
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'id': id, 'id': id,
@@ -71,7 +85,7 @@ class App {
'name': name, 'name': name,
'installedVersion': installedVersion, 'installedVersion': installedVersion,
'latestVersion': latestVersion, 'latestVersion': latestVersion,
'apkUrl': apkUrl, 'apkUrls': jsonEncode(apkUrls),
}; };
} }
@@ -98,23 +112,31 @@ class GitHub implements AppSource {
if (res.statusCode == 200) { if (res.statusCode == 200) {
var standardUri = Uri.parse(standardUrl); var standardUri = Uri.parse(standardUrl);
var parsedHtml = parse(res.body); var parsedHtml = parse(res.body);
var apkUrlList = parsedHtml.querySelectorAll('a').where((element) { var apkUrlList = getLinksFromParsedHTML(
if (element.attributes['href'] == null) return false; parsedHtml,
return RegExp( RegExp(
'^${escapeRegEx(standardUri.path)}/releases/download/[^/]+/[^/]+\\.apk\$', '^${escapeRegEx(standardUri.path)}/releases/download/[^/]+/[^/]+\\.apk\$',
caseSensitive: false) caseSensitive: false),
.hasMatch(element.attributes['href']!); standardUri.origin);
}).toList(); if (apkUrlList.isEmpty) {
throw 'No APK found';
}
String getTag(String url) {
List<String> parts = url.split('/');
return parts[parts.length - 2];
}
String latestTag = getTag(apkUrlList[0]);
String? version = parsedHtml String? version = parsedHtml
.querySelector('.octicon-tag') .querySelector('.octicon-tag')
?.nextElementSibling ?.nextElementSibling
?.innerHtml ?.innerHtml
.trim(); .trim();
if (apkUrlList.isEmpty || version == null) { if (version == null) {
throw 'No APK found'; throw 'Could not determine latest release version';
} }
return APKDetails( return APKDetails(version,
version, '${standardUri.origin}${apkUrlList[0].attributes['href']!}'); apkUrlList.where((element) => getTag(element) == latestTag).toList());
} else { } else {
throw 'Unable to fetch release info'; throw 'Unable to fetch release info';
} }
@@ -152,21 +174,23 @@ class GitLab implements AppSource {
var entry = parsedHtml.querySelector('entry'); var entry = parsedHtml.querySelector('entry');
var entryContent = var entryContent =
parse(parseFragment(entry!.querySelector('content')!.innerHtml).text); parse(parseFragment(entry!.querySelector('content')!.innerHtml).text);
var apkUrlList = entryContent.querySelectorAll('a').where((element) { var apkUrlList = getLinksFromParsedHTML(
if (element.attributes['href'] == null) return false; entryContent,
return RegExp( RegExp(
'^${escapeRegEx(standardUri.path)}/uploads/[^/]+/[^/]+\\.apk\$', '^${escapeRegEx(standardUri.path)}/uploads/[^/]+/[^/]+\\.apk\$',
caseSensitive: false) caseSensitive: false),
.hasMatch(element.attributes['href']!); standardUri.origin);
}).toList(); if (apkUrlList.isEmpty) {
throw 'No APK found';
}
var entryId = entry.querySelector('id')?.innerHtml; var entryId = entry.querySelector('id')?.innerHtml;
var version = var version =
entryId == null ? null : Uri.parse(entryId).pathSegments.last; entryId == null ? null : Uri.parse(entryId).pathSegments.last;
if (apkUrlList.isEmpty || version == null) { if (version == null) {
throw 'No APK found'; throw 'Could not determine latest release version';
} }
return APKDetails( return APKDetails(version, apkUrlList);
version, '${standardUri.origin}${apkUrlList[0].attributes['href']!}');
} else { } else {
throw 'Unable to fetch release info'; throw 'Unable to fetch release info';
} }
@@ -203,12 +227,12 @@ class SourceService {
AppNames names = source.getAppNames(standardUrl); AppNames names = source.getAppNames(standardUrl);
APKDetails apk = await source.getLatestAPKDetails(standardUrl); APKDetails apk = await source.getLatestAPKDetails(standardUrl);
return App( return App(
'${names.author}_${names.name}_${source.sourceId}', '${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.sourceId}',
standardUrl, standardUrl,
names.author[0].toUpperCase() + names.author.substring(1), names.author[0].toUpperCase() + names.author.substring(1),
names.name[0].toUpperCase() + names.name.substring(1), names.name[0].toUpperCase() + names.name.substring(1),
null, null,
apk.version, apk.version,
apk.downloadUrl); apk.apkUrls);
} }
} }