Merge branch 'main' into re7gog

This commit is contained in:
Gregory Velichko
2024-04-12 13:21:52 +03:00
29 changed files with 381 additions and 149 deletions

View File

@@ -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')))
],

View File

@@ -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'),

View File

@@ -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);
}