mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-31 21:00:15 +02:00
Merge branch 'main' into re7gog
This commit is contained in:
@@ -360,7 +360,7 @@ class AppsProvider with ChangeNotifier {
|
||||
foregroundStream = FGBGEvents.stream.asBroadcastStream();
|
||||
foregroundSubscription = foregroundStream?.listen((event) async {
|
||||
isForeground = event == FGBGType.foreground;
|
||||
if (isForeground) await loadApps();
|
||||
if (isForeground) loadApps();
|
||||
});
|
||||
() async {
|
||||
await settingsProvider.initializeSettings();
|
||||
@@ -698,23 +698,28 @@ class AppsProvider with ChangeNotifier {
|
||||
await intent.launch();
|
||||
}
|
||||
|
||||
Future<MapEntry<String, String>?> confirmApkUrl(
|
||||
App app, BuildContext? context) async {
|
||||
Future<MapEntry<String, String>?> confirmAppFileUrl(
|
||||
App app, BuildContext? context, bool pickAnyAsset) async {
|
||||
var urlsToSelectFrom = app.apkUrls;
|
||||
if (pickAnyAsset) {
|
||||
urlsToSelectFrom = [...urlsToSelectFrom, ...app.otherAssetUrls];
|
||||
}
|
||||
// If the App has more than one APK, the user should pick one (if context provided)
|
||||
MapEntry<String, String>? apkUrl =
|
||||
app.apkUrls[app.preferredApkIndex >= 0 ? app.preferredApkIndex : 0];
|
||||
MapEntry<String, String>? appFileUrl = urlsToSelectFrom[
|
||||
app.preferredApkIndex >= 0 ? app.preferredApkIndex : 0];
|
||||
// get device supported architecture
|
||||
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
||||
|
||||
if (app.apkUrls.length > 1 && context != null) {
|
||||
if (urlsToSelectFrom.length > 1 && context != null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
apkUrl = await showDialog(
|
||||
appFileUrl = await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return APKPicker(
|
||||
return AppFilePicker(
|
||||
app: app,
|
||||
initVal: apkUrl,
|
||||
initVal: appFileUrl,
|
||||
archs: archs,
|
||||
pickAnyAsset: pickAnyAsset,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -724,8 +729,8 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
// If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
|
||||
if (apkUrl != null &&
|
||||
getHost(apkUrl.value) != getHost(app.url) &&
|
||||
if (appFileUrl != null &&
|
||||
getHost(appFileUrl.value) != getHost(app.url) &&
|
||||
context != null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (!(settingsProvider.hideAPKOriginWarning) &&
|
||||
@@ -734,13 +739,13 @@ class AppsProvider with ChangeNotifier {
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return APKOriginWarningDialog(
|
||||
sourceUrl: app.url, apkUrl: apkUrl!.value);
|
||||
sourceUrl: app.url, apkUrl: appFileUrl!.value);
|
||||
}) !=
|
||||
true) {
|
||||
apkUrl = null;
|
||||
appFileUrl = null;
|
||||
}
|
||||
}
|
||||
return apkUrl;
|
||||
return appFileUrl;
|
||||
}
|
||||
|
||||
// Given a list of AppIds, uses stored info about the apps to download APKs and install them
|
||||
@@ -767,7 +772,7 @@ class AppsProvider with ChangeNotifier {
|
||||
var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == true;
|
||||
if (!trackOnly) {
|
||||
// ignore: use_build_context_synchronously
|
||||
apkUrl = await confirmApkUrl(apps[id]!.app, context);
|
||||
apkUrl = await confirmAppFileUrl(apps[id]!.app, context, false);
|
||||
}
|
||||
if (apkUrl != null) {
|
||||
int urlInd = apps[id]!
|
||||
@@ -898,6 +903,85 @@ class AppsProvider with ChangeNotifier {
|
||||
return installedIds;
|
||||
}
|
||||
|
||||
Future<List<String>> downloadAppAssets(
|
||||
List<String> appIds, BuildContext context,
|
||||
{bool forceParallelDownloads = false}) async {
|
||||
NotificationsProvider notificationsProvider =
|
||||
context.read<NotificationsProvider>();
|
||||
List<MapEntry<MapEntry<String, String>, App>> filesToDownload = [];
|
||||
for (var id in appIds) {
|
||||
if (apps[id] == null) {
|
||||
throw ObtainiumError(tr('appNotFound'));
|
||||
}
|
||||
MapEntry<String, String>? fileUrl;
|
||||
if (apps[id]!.app.apkUrls.isNotEmpty ||
|
||||
apps[id]!.app.otherAssetUrls.isNotEmpty) {
|
||||
// ignore: use_build_context_synchronously
|
||||
fileUrl = await confirmAppFileUrl(apps[id]!.app, context, true);
|
||||
}
|
||||
if (fileUrl != null) {
|
||||
filesToDownload.add(MapEntry(fileUrl, apps[id]!.app));
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare to download+install Apps
|
||||
MultiAppMultiError errors = MultiAppMultiError();
|
||||
List<String> downloadedIds = [];
|
||||
|
||||
Future<void> downloadFn(MapEntry<String, String> fileUrl, App app) async {
|
||||
try {
|
||||
var exportDir = await settingsProvider.getExportDir();
|
||||
String downloadPath = '/storage/emulated/0/Download';
|
||||
bool downloadsAccessible = false;
|
||||
try {
|
||||
downloadsAccessible = Directory(downloadPath).existsSync();
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
if (!downloadsAccessible && exportDir != null) {
|
||||
downloadPath = exportDir.path;
|
||||
}
|
||||
await downloadFile(
|
||||
fileUrl.value,
|
||||
fileUrl.key
|
||||
.split('.')
|
||||
.reversed
|
||||
.toList()
|
||||
.sublist(1)
|
||||
.reversed
|
||||
.join('.'), (double? progress) {
|
||||
notificationsProvider
|
||||
.notify(DownloadNotification(fileUrl.key, progress?.ceil() ?? 0));
|
||||
}, downloadPath,
|
||||
headers: await SourceProvider()
|
||||
.getSource(app.url, overrideSource: app.overrideSource)
|
||||
.getRequestHeaders(app.additionalSettings,
|
||||
forAPKDownload:
|
||||
fileUrl.key.endsWith('.apk') ? true : false),
|
||||
useExisting: false);
|
||||
notificationsProvider
|
||||
.notify(DownloadedNotification(fileUrl.key, fileUrl.value));
|
||||
} catch (e) {
|
||||
errors.add(fileUrl.key, e);
|
||||
} finally {
|
||||
notificationsProvider.cancel(DownloadNotification(fileUrl.key, 0).id);
|
||||
}
|
||||
}
|
||||
|
||||
if (forceParallelDownloads || !settingsProvider.parallelDownloads) {
|
||||
for (var urlWithApp in filesToDownload) {
|
||||
await downloadFn(urlWithApp.key, urlWithApp.value);
|
||||
}
|
||||
} else {
|
||||
await Future.wait(filesToDownload
|
||||
.map((urlWithApp) => downloadFn(urlWithApp.key, urlWithApp.value)));
|
||||
}
|
||||
if (errors.idsByErrorString.isNotEmpty) {
|
||||
throw errors;
|
||||
}
|
||||
return downloadedIds;
|
||||
}
|
||||
|
||||
Future<Directory> getAppsDir() async {
|
||||
Directory appsDir =
|
||||
Directory('${(await getExternalStorageDirectory())!.path}/app_data');
|
||||
@@ -1469,38 +1553,49 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
class APKPicker extends StatefulWidget {
|
||||
const APKPicker({super.key, required this.app, this.initVal, this.archs});
|
||||
class AppFilePicker extends StatefulWidget {
|
||||
const AppFilePicker(
|
||||
{super.key,
|
||||
required this.app,
|
||||
this.initVal,
|
||||
this.archs,
|
||||
this.pickAnyAsset = false});
|
||||
|
||||
final App app;
|
||||
final MapEntry<String, String>? initVal;
|
||||
final List<String>? archs;
|
||||
final bool pickAnyAsset;
|
||||
|
||||
@override
|
||||
State<APKPicker> createState() => _APKPickerState();
|
||||
State<AppFilePicker> createState() => _AppFilePickerState();
|
||||
}
|
||||
|
||||
class _APKPickerState extends State<APKPicker> {
|
||||
MapEntry<String, String>? apkUrl;
|
||||
class _AppFilePickerState extends State<AppFilePicker> {
|
||||
MapEntry<String, String>? fileUrl;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
apkUrl ??= widget.initVal;
|
||||
fileUrl ??= widget.initVal;
|
||||
var urlsToSelectFrom = widget.app.apkUrls;
|
||||
if (widget.pickAnyAsset) {
|
||||
urlsToSelectFrom = [...urlsToSelectFrom, ...widget.app.otherAssetUrls];
|
||||
}
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: Text(tr('pickAnAPK')),
|
||||
title: Text(widget.pickAnyAsset
|
||||
? tr('selectX', args: [tr('releaseAsset').toLowerCase()])
|
||||
: tr('pickAnAPK')),
|
||||
content: Column(children: [
|
||||
Text(tr('appHasMoreThanOnePackage', args: [widget.app.finalName])),
|
||||
const SizedBox(height: 16),
|
||||
...widget.app.apkUrls.map(
|
||||
...urlsToSelectFrom.map(
|
||||
(u) => RadioListTile<String>(
|
||||
title: Text(u.key),
|
||||
value: u.value,
|
||||
groupValue: apkUrl!.value,
|
||||
groupValue: fileUrl!.value,
|
||||
onChanged: (String? val) {
|
||||
setState(() {
|
||||
apkUrl =
|
||||
widget.app.apkUrls.where((e) => e.value == val).first;
|
||||
fileUrl = urlsToSelectFrom.where((e) => e.value == val).first;
|
||||
});
|
||||
}),
|
||||
),
|
||||
@@ -1527,7 +1622,7 @@ class _APKPickerState extends State<APKPicker> {
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
Navigator.of(context).pop(apkUrl);
|
||||
Navigator.of(context).pop(fileUrl);
|
||||
},
|
||||
child: Text(tr('continue')))
|
||||
],
|
||||
|
@@ -120,6 +120,18 @@ class DownloadNotification extends ObtainiumNotification {
|
||||
progPercent: progPercent);
|
||||
}
|
||||
|
||||
class DownloadedNotification extends ObtainiumNotification {
|
||||
DownloadedNotification(String fileName, String downloadUrl)
|
||||
: super(
|
||||
downloadUrl.hashCode,
|
||||
tr('downloadedX', args: [fileName]),
|
||||
'',
|
||||
'FILE_DOWNLOADED',
|
||||
tr('downloadedXNotifChannel', args: [tr('app')]),
|
||||
tr('downloadedX', args: [tr('app')]),
|
||||
Importance.defaultImportance);
|
||||
}
|
||||
|
||||
final completeInstallationNotification = ObtainiumNotification(
|
||||
1,
|
||||
tr('completeAppInstallation'),
|
||||
|
@@ -47,9 +47,10 @@ class APKDetails {
|
||||
late AppNames names;
|
||||
late DateTime? releaseDate;
|
||||
late String? changeLog;
|
||||
late List<MapEntry<String, String>> allAssetUrls;
|
||||
|
||||
APKDetails(this.version, this.apkUrls, this.names,
|
||||
{this.releaseDate, this.changeLog});
|
||||
{this.releaseDate, this.changeLog, this.allAssetUrls = const []});
|
||||
}
|
||||
|
||||
stringMapListTo2DList(List<MapEntry<String, String>> mapList) =>
|
||||
@@ -223,6 +224,7 @@ class App {
|
||||
String? installedVersion;
|
||||
late String latestVersion;
|
||||
List<MapEntry<String, String>> apkUrls = [];
|
||||
List<MapEntry<String, String>> otherAssetUrls = [];
|
||||
late int preferredApkIndex;
|
||||
late Map<String, dynamic> additionalSettings;
|
||||
late DateTime? lastUpdateCheck;
|
||||
@@ -248,7 +250,8 @@ class App {
|
||||
this.releaseDate,
|
||||
this.changeLog,
|
||||
this.overrideSource,
|
||||
this.allowIdChange = false});
|
||||
this.allowIdChange = false,
|
||||
this.otherAssetUrls = const []});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -280,41 +283,44 @@ class App {
|
||||
changeLog: changeLog,
|
||||
releaseDate: releaseDate,
|
||||
overrideSource: overrideSource,
|
||||
allowIdChange: allowIdChange);
|
||||
allowIdChange: allowIdChange,
|
||||
otherAssetUrls: otherAssetUrls);
|
||||
|
||||
factory App.fromJson(Map<String, dynamic> json) {
|
||||
json = appJSONCompatibilityModifiers(json);
|
||||
return App(
|
||||
json['id'] as String,
|
||||
json['url'] as String,
|
||||
json['author'] as String,
|
||||
json['name'] as String,
|
||||
json['installedVersion'] == null
|
||||
? null
|
||||
: json['installedVersion'] as String,
|
||||
(json['latestVersion'] ?? tr('unknown')) as String,
|
||||
assumed2DlistToStringMapList(jsonDecode(
|
||||
(json['apkUrls'] ?? '[["placeholder", "placeholder"]]'))),
|
||||
(json['preferredApkIndex'] ?? -1) as int,
|
||||
jsonDecode(json['additionalSettings']) as Map<String, dynamic>,
|
||||
json['lastUpdateCheck'] == null
|
||||
? null
|
||||
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
|
||||
json['pinned'] ?? false,
|
||||
categories: json['categories'] != null
|
||||
? (json['categories'] as List<dynamic>)
|
||||
.map((e) => e.toString())
|
||||
.toList()
|
||||
: json['category'] != null
|
||||
? [json['category'] as String]
|
||||
: [],
|
||||
releaseDate: json['releaseDate'] == null
|
||||
? null
|
||||
: DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']),
|
||||
changeLog:
|
||||
json['changeLog'] == null ? null : json['changeLog'] as String,
|
||||
overrideSource: json['overrideSource'],
|
||||
allowIdChange: json['allowIdChange'] ?? false);
|
||||
json['id'] as String,
|
||||
json['url'] as String,
|
||||
json['author'] as String,
|
||||
json['name'] as String,
|
||||
json['installedVersion'] == null
|
||||
? null
|
||||
: json['installedVersion'] as String,
|
||||
(json['latestVersion'] ?? tr('unknown')) as String,
|
||||
assumed2DlistToStringMapList(
|
||||
jsonDecode((json['apkUrls'] ?? '[["placeholder", "placeholder"]]'))),
|
||||
(json['preferredApkIndex'] ?? -1) as int,
|
||||
jsonDecode(json['additionalSettings']) as Map<String, dynamic>,
|
||||
json['lastUpdateCheck'] == null
|
||||
? null
|
||||
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
|
||||
json['pinned'] ?? false,
|
||||
categories: json['categories'] != null
|
||||
? (json['categories'] as List<dynamic>)
|
||||
.map((e) => e.toString())
|
||||
.toList()
|
||||
: json['category'] != null
|
||||
? [json['category'] as String]
|
||||
: [],
|
||||
releaseDate: json['releaseDate'] == null
|
||||
? null
|
||||
: DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']),
|
||||
changeLog: json['changeLog'] == null ? null : json['changeLog'] as String,
|
||||
overrideSource: json['overrideSource'],
|
||||
allowIdChange: json['allowIdChange'] ?? false,
|
||||
otherAssetUrls: assumed2DlistToStringMapList(
|
||||
jsonDecode((json['otherAssetUrls'] ?? '[]'))),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
@@ -325,6 +331,7 @@ class App {
|
||||
'installedVersion': installedVersion,
|
||||
'latestVersion': latestVersion,
|
||||
'apkUrls': jsonEncode(stringMapListTo2DList(apkUrls)),
|
||||
'otherAssetUrls': jsonEncode(stringMapListTo2DList(otherAssetUrls)),
|
||||
'preferredApkIndex': preferredApkIndex,
|
||||
'additionalSettings': jsonEncode(additionalSettings),
|
||||
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
|
||||
@@ -892,8 +899,10 @@ class SourceProvider {
|
||||
allowIdChange: currentApp?.allowIdChange ??
|
||||
trackOnly ||
|
||||
(source.appIdInferIsOptional &&
|
||||
inferAppIdIfOptional) // Optional ID inferring may be incorrect - allow correction on first install
|
||||
);
|
||||
inferAppIdIfOptional), // Optional ID inferring may be incorrect - allow correction on first install
|
||||
otherAssetUrls: apk.allAssetUrls
|
||||
.where((a) => apk.apkUrls.indexWhere((p) => a.key == p.key) < 0)
|
||||
.toList());
|
||||
return source.endOfGetAppChanges(finalApp);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user