Add support for zipped APKs - GitHub/HTML only for now (#983)

This commit is contained in:
Imran Remtulla
2025-10-11 18:29:59 -04:00
parent 7cb9ca128b
commit 8bdc0f8d12
32 changed files with 144 additions and 32 deletions

View File

@@ -18,6 +18,7 @@ class GitHub extends AppSource {
appIdInferIsOptional = true;
showReleaseDateAsVersionToggle = true;
this.hostChanged = hostChanged;
allowIncludeZips = true;
sourceConfigSettingFormItems = [
GeneratedFormTextField(
@@ -370,6 +371,7 @@ class GitHub extends AppSource {
additionalSettings['useLatestAssetDateAsReleaseDate'] == true;
String sortMethod =
additionalSettings['sortMethodChoice'] ?? 'smartname-datefallback';
bool includeZips = additionalSettings['includeZips'] == true;
dynamic latestRelease;
if (verifyLatestTag) {
var temp = requestUrl.split('?');
@@ -402,7 +404,8 @@ class GitHub extends AppSource {
findReleaseAssetUrls(dynamic release) =>
(release['assets'] as List<dynamic>?)?.map((e) {
var url = !e['name'].toString().toLowerCase().endsWith('.apk')
var ext = e['name'].toString().toLowerCase().split('.').last;
var url = !(ext == 'apk' || (includeZips && ext == 'zip'))
? (e['browser_download_url'] ?? e['url'])
: (e['url'] ?? e['browser_download_url']);
url = undoGHProxyMod(url, sourceConfigSettingValues);
@@ -542,14 +545,13 @@ class GitHub extends AppSource {
List<MapEntry<String, String>> allAssetUrls = allAssetsWithUrls
.map((e) => e['final_url'] as MapEntry<String, String>)
.toList();
var apkAssetsWithUrls = allAssetsWithUrls
.where(
(element) => (element['final_url'] as MapEntry<String, String>)
.key
.toLowerCase()
.endsWith('.apk'),
)
.toList();
var apkAssetsWithUrls = allAssetsWithUrls.where((element) {
var ext = (element['final_url'] as MapEntry<String, String>).key
.toLowerCase()
.split('.')
.last;
return ext == 'apk' || (includeZips && ext == 'zip');
}).toList();
var filteredApkUrls = filterApks(
apkAssetsWithUrls

View File

@@ -203,11 +203,11 @@ class AddAppPageState extends State<AddAppPage> {
notificationsProvider: notificationsProvider,
);
DownloadedApk? downloadedFile;
DownloadedXApkDir? downloadedDir;
DownloadedDir? downloadedDir;
if (downloadedArtifact is DownloadedApk) {
downloadedFile = downloadedArtifact;
} else {
downloadedDir = downloadedArtifact as DownloadedXApkDir;
downloadedDir = downloadedArtifact as DownloadedDir;
}
app.id = downloadedFile?.appId ?? downloadedDir!.appId;
}

View File

@@ -62,11 +62,14 @@ class DownloadedApk {
DownloadedApk(this.appId, this.file);
}
class DownloadedXApkDir {
enum DownloadedDirType { XAPK, ZIP }
class DownloadedDir {
String appId;
File file;
Directory extracted;
DownloadedXApkDir(this.appId, this.file, this.extracted);
DownloadedDirType type;
DownloadedDir(this.appId, this.file, this.extracted, this.type);
}
List<String> generateStandardVersionRegExStrings() {
@@ -664,17 +667,18 @@ class AppsProvider with ChangeNotifier {
}
PackageInfo? newInfo;
var isAPK = downloadedFile.path.toLowerCase().endsWith('.apk');
Directory? xapkDir;
var isXAPK = downloadedFile.path.toLowerCase().endsWith('.xapk');
Directory? apkDir;
if (isAPK) {
newInfo = await pm.getPackageArchiveInfo(
archiveFilePath: downloadedFile.path,
);
} else {
// Assume XAPK
String xapkDirPath = '${downloadedFile.path}-dir';
// Assume XAPK or ZIP
String apkDirPath = '${downloadedFile.path}-dir';
await unzipFile(downloadedFile.path, '${downloadedFile.path}-dir');
xapkDir = Directory(xapkDirPath);
var apks = xapkDir
apkDir = Directory(apkDirPath);
var apks = apkDir
.listSync()
.where((e) => e.path.toLowerCase().endsWith('.apk'))
.toList();
@@ -691,6 +695,22 @@ class AppsProvider with ChangeNotifier {
apks = [temp!, ...apks];
}
if (app.additionalSettings['zippedApkFilterRegEx']?.isNotEmpty ==
true) {
var reg = RegExp(app.additionalSettings['zippedApkFilterRegEx']);
apks.removeWhere((apk) {
var shouldDelete = !reg.hasMatch(apk.uri.pathSegments.last);
if (shouldDelete) {
apk.delete();
}
return shouldDelete;
});
}
if (apks.isEmpty) {
throw NoAPKError();
}
for (var i = 0; i < apks.length; i++) {
try {
newInfo = await pm.getPackageArchiveInfo(
@@ -728,7 +748,12 @@ class AppsProvider with ChangeNotifier {
if (isAPK) {
return DownloadedApk(app.id, downloadedFile);
} else {
return DownloadedXApkDir(app.id, downloadedFile, xapkDir!);
return DownloadedDir(
app.id,
downloadedFile,
apkDir!,
isXAPK ? DownloadedDirType.XAPK : DownloadedDirType.ZIP,
);
}
} finally {
notificationsProvider?.cancel(notifId);
@@ -826,15 +851,16 @@ class AppsProvider with ChangeNotifier {
);
}
Future<bool> installXApkDir(
DownloadedXApkDir dir,
Future<bool> installApkDir(
DownloadedDir dir,
BuildContext? firstTimeWithContext, {
bool needsBGWorkaround = false,
bool shizukuPretendToBeGooglePlay = false,
}) async {
// We don't know which APKs in an XAPK are supported by the user's device
// We don't know which APKs in an XAPK or ZIP are supported by the user's device
// So we try installing all of them and assume success if at least one installed
// If 0 APKs installed, throw the first install error encountered
// Obviously this approach is naive and is undesirable in many cases, needs to be improved
var somethingInstalled = false;
try {
MultiAppMultiError errors = MultiAppMultiError();
@@ -863,7 +889,7 @@ class AppsProvider with ChangeNotifier {
}
try {
await installApk(
var wasInstalled = await installApk(
DownloadedApk(dir.appId, APKFiles[0]),
firstTimeWithContext,
needsBGWorkaround: needsBGWorkaround,
@@ -872,10 +898,10 @@ class AppsProvider with ChangeNotifier {
1,
).map((a) => DownloadedApk(dir.appId, a)).toList(),
);
somethingInstalled = true;
somethingInstalled = somethingInstalled || wasInstalled;
dir.file.delete(recursive: true);
} catch (e) {
logs.add('Could not install APKs from XAPK: ${e.toString()}');
logs.add('Could not install APKs from ${dir.type}: ${e.toString()}');
errors.add(dir.appId, e, appName: apps[dir.appId]?.name);
}
if (errors.idsByErrorString.isNotEmpty) {
@@ -1148,7 +1174,7 @@ class AppsProvider with ChangeNotifier {
String id,
bool willBeSilent,
DownloadedApk? downloadedFile,
DownloadedXApkDir? downloadedDir,
DownloadedDir? downloadedDir,
) async {
apps[id]?.downloadProgress = -1;
notifyListeners();
@@ -1183,14 +1209,14 @@ class AppsProvider with ChangeNotifier {
} else {
if (needBGWorkaround) {
// ignore: use_build_context_synchronously
installXApkDir(
installApkDir(
downloadedDir!,
contextIfNewInstall,
needsBGWorkaround: true,
);
} else {
// ignore: use_build_context_synchronously
sayInstalled = await installXApkDir(
sayInstalled = await installApkDir(
downloadedDir!,
contextIfNewInstall,
shizukuPretendToBeGooglePlay: shizukuPretendToBeGooglePlay,
@@ -1227,7 +1253,7 @@ class AppsProvider with ChangeNotifier {
}) async {
bool willBeSilent = false;
DownloadedApk? downloadedFile;
DownloadedXApkDir? downloadedDir;
DownloadedDir? downloadedDir;
try {
var downloadedArtifact =
// ignore: use_build_context_synchronously
@@ -1240,7 +1266,7 @@ class AppsProvider with ChangeNotifier {
if (downloadedArtifact is DownloadedApk) {
downloadedFile = downloadedArtifact;
} else {
downloadedDir = downloadedArtifact as DownloadedXApkDir;
downloadedDir = downloadedArtifact as DownloadedDir;
}
id = downloadedFile?.appId ?? downloadedDir!.appId;
willBeSilent = await canInstallSilently(apps[id]!.app);
@@ -1292,7 +1318,7 @@ class AppsProvider with ChangeNotifier {
res['id'] as String,
res['willBeSilent'] as bool,
res['downloadedFile'] as DownloadedApk?,
res['downloadedDir'] as DownloadedXApkDir?,
res['downloadedDir'] as DownloadedDir?,
);
} catch (e) {
var id = res['id'] as String;

View File

@@ -636,6 +636,7 @@ abstract class AppSource {
bool versionDetectionDisallowed = false;
List<String> excludeCommonSettingKeys = [];
bool urlsAlwaysHaveExtension = false;
bool allowIncludeZips = false;
AppSource() {
name = runtimeType.toString();
@@ -834,7 +835,7 @@ abstract class AppSource {
],
];
// Previous 2 variables combined into one at runtime for convenient usage
// Previous 2 variables combined into one at runtime for convenient usage + additional processing
List<List<GeneratedFormItem>> get combinedAppSpecificSettingFormItems {
if (showReleaseDateAsVersionToggle == true) {
if (additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
@@ -878,6 +879,32 @@ abstract class AppSource {
)
.where((e) => e.isNotEmpty)
.toList();
var moreConditionalItems = [];
if (allowIncludeZips) {
moreConditionalItems.addAll([
[
GeneratedFormSwitch(
'includeZips',
label: tr('includeZips'),
defaultValue: false,
),
],
[
GeneratedFormTextField(
'zippedApkFilterRegEx',
label: tr('zippedApkFilterRegEx'),
required: false,
additionalValidators: [
(value) {
return regExValidator(value);
},
],
),
],
]);
}
if (versionDetectionDisallowed) {
overrideAdditionalAppSpecificSourceAgnosticSettingSwitch(
'versionDetection',
@@ -893,6 +920,7 @@ abstract class AppSource {
return [
...additionalSourceAppSpecificSettingFormItems,
...additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly,
...moreConditionalItems,
];
}