mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-10-25 11:53:45 +02:00
Add XAPK support (incomplete - OBB not copied)
This commit is contained in:
@@ -57,9 +57,9 @@ class APKPure extends AppSource {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK';
|
||||||
List<MapEntry<String, String>> apkUrls = [
|
List<MapEntry<String, String>> apkUrls = [
|
||||||
MapEntry('$appId.apk', 'https://d.$host/b/APK/$appId?version=latest')
|
MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest')
|
||||||
];
|
];
|
||||||
String author = html
|
String author = html
|
||||||
.querySelector('span.info-sdk')
|
.querySelector('span.info-sdk')
|
||||||
|
|||||||
@@ -159,9 +159,16 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
app.preferredApkIndex =
|
app.preferredApkIndex =
|
||||||
app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value);
|
app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value);
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
var downloadedApk = await appsProvider.downloadApp(
|
var downloadedArtifact = await appsProvider.downloadApp(
|
||||||
app, globalNavigatorKey.currentContext);
|
app, globalNavigatorKey.currentContext);
|
||||||
app.id = downloadedApk.appId;
|
DownloadedApk? downloadedFile;
|
||||||
|
DownloadedXApkDir? downloadedDir;
|
||||||
|
if (downloadedArtifact is DownloadedApk) {
|
||||||
|
downloadedFile = downloadedArtifact;
|
||||||
|
} else {
|
||||||
|
downloadedDir = downloadedArtifact as DownloadedXApkDir;
|
||||||
|
}
|
||||||
|
app.id = downloadedFile?.appId ?? downloadedDir!.appId;
|
||||||
}
|
}
|
||||||
if (appsProvider.apps.containsKey(app.id)) {
|
if (appsProvider.apps.containsKey(app.id)) {
|
||||||
throw ObtainiumError(tr('appAlreadyAdded'));
|
throw ObtainiumError(tr('appAlreadyAdded'));
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import 'package:flutter_fgbg/flutter_fgbg.dart';
|
|||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:android_intent_plus/android_intent.dart';
|
import 'package:android_intent_plus/android_intent.dart';
|
||||||
|
import 'package:archive/archive.dart';
|
||||||
|
|
||||||
class AppInMemory {
|
class AppInMemory {
|
||||||
late App app;
|
late App app;
|
||||||
@@ -46,6 +47,13 @@ class DownloadedApk {
|
|||||||
DownloadedApk(this.appId, this.file);
|
DownloadedApk(this.appId, this.file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DownloadedXApkDir {
|
||||||
|
String appId;
|
||||||
|
File file;
|
||||||
|
Directory extracted;
|
||||||
|
DownloadedXApkDir(this.appId, this.file, this.extracted);
|
||||||
|
}
|
||||||
|
|
||||||
List<String> generateStandardVersionRegExStrings() {
|
List<String> generateStandardVersionRegExStrings() {
|
||||||
// TODO: Look into RegEx for non-Latin characters / non-Arabic numerals
|
// TODO: Look into RegEx for non-Latin characters / non-Arabic numerals
|
||||||
var basics = [
|
var basics = [
|
||||||
@@ -164,7 +172,27 @@ class AppsProvider with ChangeNotifier {
|
|||||||
return downloadedFile;
|
return downloadedFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DownloadedApk> downloadApp(App app, BuildContext? context) async {
|
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) {
|
||||||
|
var isTempId = SourceProvider().isTempId(app);
|
||||||
|
if (apps[app.id] != null && !isTempId) {
|
||||||
|
throw IDChangedError();
|
||||||
|
}
|
||||||
|
var originalAppId = app.id;
|
||||||
|
app.id = newInfo.packageName;
|
||||||
|
downloadedFile = downloadedFile.renameSync(
|
||||||
|
'${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.apk');
|
||||||
|
if (apps[originalAppId] != null) {
|
||||||
|
await removeApps([originalAppId]);
|
||||||
|
await saveApps([app], onlyIfExists: !isTempId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Object> downloadApp(App app, BuildContext? context) async {
|
||||||
NotificationsProvider? notificationsProvider =
|
NotificationsProvider? notificationsProvider =
|
||||||
context?.read<NotificationsProvider>();
|
context?.read<NotificationsProvider>();
|
||||||
var notifId = DownloadNotification(app.finalName, 0).id;
|
var notifId = DownloadNotification(app.finalName, 0).id;
|
||||||
@@ -194,33 +222,42 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
prevProg = prog;
|
prevProg = prog;
|
||||||
});
|
});
|
||||||
// If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed
|
PackageArchiveInfo? newInfo;
|
||||||
// The former case should be handled (give the App its real ID), the latter is a security issue
|
try {
|
||||||
var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
|
newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
|
||||||
if (app.id != newInfo.packageName) {
|
} catch (e) {
|
||||||
var isTempId = SourceProvider().isTempId(app);
|
// Assume it's an XAPK
|
||||||
if (apps[app.id] != null && !isTempId) {
|
fileName = '${app.id}-${downloadUrl.hashCode}.xapk';
|
||||||
throw IDChangedError();
|
String newPath = '${downloadedFile.parent.path}/$fileName';
|
||||||
|
downloadedFile.renameSync(newPath);
|
||||||
|
downloadedFile = File(newPath);
|
||||||
}
|
}
|
||||||
var originalAppId = app.id;
|
Directory? xapkDir;
|
||||||
app.id = newInfo.packageName;
|
if (newInfo == null) {
|
||||||
downloadedFile = downloadedFile.renameSync(
|
String xapkDirPath = '${downloadedFile.path}-dir';
|
||||||
'${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.apk');
|
unzipFile(downloadedFile.path, '${downloadedFile.path}-dir');
|
||||||
if (apps[originalAppId] != null) {
|
xapkDir = Directory(xapkDirPath);
|
||||||
await removeApps([originalAppId]);
|
var apks = xapkDir
|
||||||
await saveApps([app], onlyIfExists: !isTempId);
|
.listSync()
|
||||||
|
.where((e) => e.path.toLowerCase().endsWith('.apk'))
|
||||||
|
.toList();
|
||||||
|
newInfo = await PackageArchiveInfo.fromPath(apks.first.path);
|
||||||
}
|
}
|
||||||
}
|
await handleAPKIDChange(app, newInfo, downloadedFile, downloadUrl);
|
||||||
// Delete older versions of the APK if any
|
// Delete older versions of the file if any
|
||||||
for (var file in downloadedFile.parent.listSync()) {
|
for (var file in downloadedFile.parent.listSync()) {
|
||||||
var fn = file.path.split('/').last;
|
var fn = file.path.split('/').last;
|
||||||
if (fn.startsWith('${app.id}-') &&
|
if (fn.startsWith('${app.id}-') &&
|
||||||
fn.endsWith('.apk') &&
|
fn.toLowerCase().endsWith(xapkDir == null ? '.apk' : '.xapk') &&
|
||||||
fn != downloadedFile.path.split('/').last) {
|
fn != downloadedFile.path.split('/').last) {
|
||||||
file.delete();
|
file.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (xapkDir != null) {
|
||||||
|
return DownloadedXApkDir(app.id, downloadedFile, xapkDir);
|
||||||
|
} else {
|
||||||
return DownloadedApk(app.id, downloadedFile);
|
return DownloadedApk(app.id, downloadedFile);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
notificationsProvider?.cancel(notifId);
|
notificationsProvider?.cancel(notifId);
|
||||||
if (apps[app.id] != null) {
|
if (apps[app.id] != null) {
|
||||||
@@ -267,10 +304,37 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unfortunately this 'await' does not actually wait for the APK to finish installing
|
void unzipFile(String filePath, String destinationPath) {
|
||||||
// So we only know that the install prompt was shown, but the user could still cancel w/o us knowing
|
final bytes = File(filePath).readAsBytesSync();
|
||||||
// If appropriate criteria are met, the update (never a fresh install) happens silently in the background
|
final archive = ZipDecoder().decodeBytes(bytes);
|
||||||
// But even then, we don't know if it actually succeeded
|
|
||||||
|
for (final file in archive) {
|
||||||
|
final filename = '$destinationPath/${file.name}';
|
||||||
|
if (file.isFile) {
|
||||||
|
final data = file.content as List<int>;
|
||||||
|
File(filename)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsBytesSync(data);
|
||||||
|
} else {
|
||||||
|
Directory(filename).create(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> installXApkDir(DownloadedXApkDir dir,
|
||||||
|
{bool silent = false}) async {
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
dir.file.delete();
|
||||||
|
} finally {
|
||||||
|
dir.extracted.delete(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> installApk(DownloadedApk file, {bool silent = false}) async {
|
Future<void> installApk(DownloadedApk file, {bool silent = false}) async {
|
||||||
// TODO: Use 'silent' when/if ever possible
|
// TODO: Use 'silent' when/if ever possible
|
||||||
var newInfo = await PackageArchiveInfo.fromPath(file.file.path);
|
var newInfo = await PackageArchiveInfo.fromPath(file.file.path);
|
||||||
@@ -420,9 +484,16 @@ class AppsProvider with ChangeNotifier {
|
|||||||
for (var id in appsToInstall) {
|
for (var id in appsToInstall) {
|
||||||
try {
|
try {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
var downloadedFile = await downloadApp(apps[id]!.app, context);
|
var downloadedArtifact = await downloadApp(apps[id]!.app, context);
|
||||||
bool willBeSilent =
|
DownloadedApk? downloadedFile;
|
||||||
await canInstallSilently(apps[downloadedFile.appId]!.app);
|
DownloadedXApkDir? downloadedDir;
|
||||||
|
if (downloadedArtifact is DownloadedApk) {
|
||||||
|
downloadedFile = downloadedArtifact;
|
||||||
|
} else {
|
||||||
|
downloadedDir = downloadedArtifact as DownloadedXApkDir;
|
||||||
|
}
|
||||||
|
bool willBeSilent = await canInstallSilently(
|
||||||
|
apps[downloadedFile?.appId ?? downloadedDir!.appId]!.app);
|
||||||
willBeSilent = false; // TODO: Remove this when silent updates work
|
willBeSilent = false; // TODO: Remove this when silent updates work
|
||||||
if (!(await settingsProvider?.getInstallPermission(enforce: false) ??
|
if (!(await settingsProvider?.getInstallPermission(enforce: false) ??
|
||||||
true)) {
|
true)) {
|
||||||
@@ -432,7 +503,11 @@ class AppsProvider with ChangeNotifier {
|
|||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
await waitForUserToReturnToForeground(context);
|
await waitForUserToReturnToForeground(context);
|
||||||
}
|
}
|
||||||
|
if (downloadedFile != null) {
|
||||||
await installApk(downloadedFile, silent: willBeSilent);
|
await installApk(downloadedFile, silent: willBeSilent);
|
||||||
|
} else {
|
||||||
|
await installXApkDir(downloadedDir!, silent: willBeSilent);
|
||||||
|
}
|
||||||
installedIds.add(id);
|
installedIds.add(id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errors.add(id, e.toString());
|
errors.add(id, e.toString());
|
||||||
|
|||||||
24
pubspec.lock
24
pubspec.lock
@@ -34,6 +34,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.7"
|
version: "2.0.7"
|
||||||
|
archive:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.7"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -82,6 +90,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.17.0"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
cross_file:
|
cross_file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -518,6 +534,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
pointycastle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pointycastle
|
||||||
|
sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.7.3"
|
||||||
process:
|
process:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ dependencies:
|
|||||||
easy_localization: ^3.0.1
|
easy_localization: ^3.0.1
|
||||||
android_intent_plus: ^3.1.5
|
android_intent_plus: ^3.1.5
|
||||||
flutter_markdown: ^0.6.14
|
flutter_markdown: ^0.6.14
|
||||||
|
archive: ^3.3.7
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user