mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-08-19 05:00:21 +02:00
Cleanup (#98)
This commit is contained in:
@@ -5,7 +5,6 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
@@ -25,15 +24,15 @@ import 'package:http/http.dart';
|
||||
class AppInMemory {
|
||||
late App app;
|
||||
double? downloadProgress;
|
||||
AppInfo? installedInfo; // Also indicates that an App is installed
|
||||
AppInfo? installedInfo;
|
||||
|
||||
AppInMemory(this.app, this.downloadProgress, this.installedInfo);
|
||||
}
|
||||
|
||||
class DownloadedApp {
|
||||
class DownloadedApk {
|
||||
String appId;
|
||||
File file;
|
||||
DownloadedApp(this.appId, this.file);
|
||||
DownloadedApk(this.appId, this.file);
|
||||
}
|
||||
|
||||
class AppsProvider with ChangeNotifier {
|
||||
@@ -41,124 +40,112 @@ class AppsProvider with ChangeNotifier {
|
||||
Map<String, AppInMemory> apps = {};
|
||||
bool loadingApps = false;
|
||||
bool gettingUpdates = false;
|
||||
bool forBGTask = false;
|
||||
|
||||
// Variables to keep track of the app foreground status (installs can't run in the background)
|
||||
bool isForeground = true;
|
||||
late Stream<FGBGType>? foregroundStream;
|
||||
late StreamSubscription<FGBGType>? foregroundSubscription;
|
||||
|
||||
AppsProvider(
|
||||
{bool shouldLoadApps = false,
|
||||
bool shouldCheckUpdatesAfterLoad = false,
|
||||
bool shouldDeleteAPKs = false}) {
|
||||
if (shouldLoadApps) {
|
||||
AppsProvider({this.forBGTask = false}) {
|
||||
// Many setup tasks should only be done in the foreground isolate
|
||||
if (!forBGTask) {
|
||||
// Subscribe to changes in the app foreground status
|
||||
foregroundStream = FGBGEvents.stream.asBroadcastStream();
|
||||
foregroundSubscription = foregroundStream?.listen((event) async {
|
||||
isForeground = event == FGBGType.foreground;
|
||||
if (isForeground) await loadApps();
|
||||
});
|
||||
loadApps().then((_) {
|
||||
if (shouldDeleteAPKs) {
|
||||
deleteSavedAPKs();
|
||||
}
|
||||
if (shouldCheckUpdatesAfterLoad) {
|
||||
checkUpdates();
|
||||
}
|
||||
});
|
||||
() async {
|
||||
// Load Apps into memory (in background, this is done later instead of in the constructor)
|
||||
await loadApps();
|
||||
// Delete existing APKs
|
||||
(await getExternalStorageDirectory())
|
||||
?.listSync()
|
||||
.where((element) => element.path.endsWith('.apk'))
|
||||
.forEach((apk) {
|
||||
apk.delete();
|
||||
});
|
||||
}();
|
||||
}
|
||||
}
|
||||
|
||||
downloadApk(String apkUrl, String fileName, Function? onProgress,
|
||||
Function? urlModifier) async {
|
||||
bool useExistingIfExists =
|
||||
false; // This should be an function argument, but isn't because of the partially downloaded APK issue
|
||||
downloadFile(String url, String fileName, Function? onProgress) async {
|
||||
var destDir = (await getExternalStorageDirectory())!.path;
|
||||
if (urlModifier != null) {
|
||||
apkUrl = await urlModifier(apkUrl);
|
||||
}
|
||||
StreamedResponse response =
|
||||
await Client().send(Request('GET', Uri.parse(apkUrl)));
|
||||
File downloadFile = File('$destDir/$fileName.apk');
|
||||
var alreadyExists = downloadFile.existsSync();
|
||||
if (!alreadyExists || !useExistingIfExists) {
|
||||
if (alreadyExists) {
|
||||
downloadFile.deleteSync();
|
||||
}
|
||||
await Client().send(Request('GET', Uri.parse(url)));
|
||||
File downloadedFile = File('$destDir/$fileName');
|
||||
|
||||
var length = response.contentLength;
|
||||
var received = 0;
|
||||
double? progress;
|
||||
var sink = downloadFile.openWrite();
|
||||
if (downloadedFile.existsSync()) {
|
||||
downloadedFile.deleteSync();
|
||||
}
|
||||
var length = response.contentLength;
|
||||
var received = 0;
|
||||
double? progress;
|
||||
var sink = downloadedFile.openWrite();
|
||||
|
||||
await response.stream.map((s) {
|
||||
received += s.length;
|
||||
progress = (length != null ? received / length * 100 : 30);
|
||||
if (onProgress != null) {
|
||||
onProgress(progress);
|
||||
}
|
||||
return s;
|
||||
}).pipe(sink);
|
||||
|
||||
await sink.close();
|
||||
progress = null;
|
||||
await response.stream.map((s) {
|
||||
received += s.length;
|
||||
progress = (length != null ? received / length * 100 : 30);
|
||||
if (onProgress != null) {
|
||||
onProgress(progress);
|
||||
}
|
||||
return s;
|
||||
}).pipe(sink);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
downloadFile.deleteSync();
|
||||
throw response.reasonPhrase ?? 'Unknown Error';
|
||||
}
|
||||
await sink.close();
|
||||
progress = null;
|
||||
if (onProgress != null) {
|
||||
onProgress(progress);
|
||||
}
|
||||
return downloadFile;
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
downloadedFile.deleteSync();
|
||||
throw response.reasonPhrase ?? 'Unknown Error';
|
||||
}
|
||||
return downloadedFile;
|
||||
}
|
||||
|
||||
// Downloads the App (preferred URL) and returns an ApkFile object
|
||||
// If the app was already saved, updates it's download progress % in memory
|
||||
// But also works for Apps that are not saved
|
||||
Future<DownloadedApp> downloadApp(App app,
|
||||
{bool showOccasionalProgressToast = false}) async {
|
||||
Future<DownloadedApk> downloadApp(App app) async {
|
||||
var fileName =
|
||||
'${app.id}-${app.latestVersion}-${app.preferredApkIndex}.apk';
|
||||
String downloadUrl = await SourceProvider()
|
||||
.getSource(app.url)
|
||||
.apkUrlPrefetchModifier(app.url);
|
||||
int? prevProg;
|
||||
var fileName = '${app.id}-${app.latestVersion}-${app.preferredApkIndex}';
|
||||
File downloadFile = await downloadApk(app.apkUrls[app.preferredApkIndex],
|
||||
'${app.id}-${app.latestVersion}-${app.preferredApkIndex}',
|
||||
(double? progress) {
|
||||
File downloadedFile =
|
||||
await downloadFile(downloadUrl, fileName, (double? progress) {
|
||||
int? prog = progress?.ceil();
|
||||
if (apps[app.id] != null) {
|
||||
apps[app.id]!.downloadProgress = progress;
|
||||
}
|
||||
int? prog = progress?.ceil();
|
||||
if (showOccasionalProgressToast &&
|
||||
(prog == 25 || prog == 50 || prog == 75) &&
|
||||
prevProg != prog) {
|
||||
notifyListeners();
|
||||
} else if ((prog == 25 || prog == 50 || prog == 75) && prevProg != prog) {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Progress: $prog%', toastLength: Toast.LENGTH_SHORT);
|
||||
}
|
||||
prevProg = prog;
|
||||
notifyListeners();
|
||||
}, SourceProvider().getSource(app.url).apkUrlPrefetchModifier);
|
||||
});
|
||||
// Delete older versions of the APK if any
|
||||
for (var file in downloadFile.parent.listSync()) {
|
||||
for (var file in downloadedFile.parent.listSync()) {
|
||||
var fn = file.path.split('/').last;
|
||||
if (fn.startsWith('${app.id}-') &&
|
||||
fn.endsWith('.apk') &&
|
||||
fn != '$fileName.apk') {
|
||||
fn != fileName) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
// If the ID has changed (as it should on first download), replace it
|
||||
var newInfo = await PackageArchiveInfo.fromPath(downloadFile.path);
|
||||
// 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
|
||||
var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
|
||||
if (app.id != newInfo.packageName) {
|
||||
var originalAppId = app.id;
|
||||
app.id = newInfo.packageName;
|
||||
downloadFile = downloadFile.renameSync(
|
||||
'${downloadFile.parent.path}/${app.id}-${app.latestVersion}-${app.preferredApkIndex}.apk');
|
||||
if (apps[originalAppId] != null) {
|
||||
await removeApps([originalAppId]);
|
||||
await saveApps([app]);
|
||||
if (apps[app.id] != null) {
|
||||
throw IDChangedError();
|
||||
}
|
||||
app.id = newInfo.packageName;
|
||||
downloadedFile = downloadedFile.renameSync(
|
||||
'${downloadedFile.parent.path}/${app.id}-${app.latestVersion}-${app.preferredApkIndex}.apk');
|
||||
}
|
||||
return DownloadedApp(app.id, downloadFile);
|
||||
return DownloadedApk(app.id, downloadedFile);
|
||||
}
|
||||
|
||||
bool areDownloadsRunning() => apps.values
|
||||
@@ -166,24 +153,26 @@ class AppsProvider with ChangeNotifier {
|
||||
.isNotEmpty;
|
||||
|
||||
Future<bool> canInstallSilently(App app) async {
|
||||
// TODO: This is unreliable - try to get from OS in the future
|
||||
var osInfo = await DeviceInfoPlugin().androidInfo;
|
||||
return app.installedVersion != null &&
|
||||
osInfo.version.sdkInt >= 30 &&
|
||||
osInfo.version.release.compareTo('12') >= 0;
|
||||
return false;
|
||||
// TODO: Uncomment the below once silentupdates are ever figured out
|
||||
// // TODO: This is unreliable - try to get from OS in the future
|
||||
// if (app.apkUrls.length > 1) {
|
||||
// return false;
|
||||
// }
|
||||
// var osInfo = await DeviceInfoPlugin().androidInfo;
|
||||
// return app.installedVersion != null &&
|
||||
// osInfo.version.sdkInt >= 30 &&
|
||||
// osInfo.version.release.compareTo('12') >= 0;
|
||||
}
|
||||
|
||||
Future<void> askUserToReturnToForeground(BuildContext context,
|
||||
{bool waitForFG = false}) async {
|
||||
Future<void> waitForUserToReturnToForeground(BuildContext context) async {
|
||||
NotificationsProvider notificationsProvider =
|
||||
context.read<NotificationsProvider>();
|
||||
if (!isForeground) {
|
||||
await notificationsProvider.notify(completeInstallationNotification,
|
||||
cancelExisting: true);
|
||||
if (waitForFG) {
|
||||
await FGBGEvents.stream.first == FGBGType.foreground;
|
||||
await notificationsProvider.cancel(completeInstallationNotification.id);
|
||||
}
|
||||
while (await FGBGEvents.stream.first != FGBGType.foreground) {}
|
||||
await notificationsProvider.cancel(completeInstallationNotification.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +180,7 @@ class AppsProvider with ChangeNotifier {
|
||||
// So we only know that the install prompt was shown, but the user could still cancel w/o us knowing
|
||||
// If appropriate criteria are met, the update (never a fresh install) happens silently in the background
|
||||
// But even then, we don't know if it actually succeeded
|
||||
Future<void> installApk(DownloadedApp file) async {
|
||||
Future<void> installApk(DownloadedApk file) async {
|
||||
var newInfo = await PackageArchiveInfo.fromPath(file.file.path);
|
||||
AppInfo? appInfo;
|
||||
try {
|
||||
@@ -201,7 +190,7 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
if (appInfo != null &&
|
||||
int.parse(newInfo.buildNumber) < appInfo.versionCode!) {
|
||||
throw 'Can\'t install an older version';
|
||||
throw DowngradeError();
|
||||
}
|
||||
if (appInfo == null ||
|
||||
int.parse(newInfo.buildNumber) > appInfo.versionCode!) {
|
||||
@@ -210,10 +199,11 @@ class AppsProvider with ChangeNotifier {
|
||||
apps[file.appId]!.app.installedVersion =
|
||||
apps[file.appId]!.app.latestVersion;
|
||||
// Don't correct install status as installation may not be done yet
|
||||
await saveApps([apps[file.appId]!.app], shouldCorrectInstallStatus: false);
|
||||
await saveApps([apps[file.appId]!.app],
|
||||
attemptToCorrectInstallStatus: false);
|
||||
}
|
||||
|
||||
Future<String?> selectApkUrl(App app, BuildContext? context) async {
|
||||
Future<String?> confirmApkUrl(App app, BuildContext? context) async {
|
||||
// If the App has more than one APK, the user should pick one (if context provided)
|
||||
String? apkUrl = app.apkUrls[app.preferredApkIndex];
|
||||
if (app.apkUrls.length > 1 && context != null) {
|
||||
@@ -240,15 +230,6 @@ class AppsProvider with ChangeNotifier {
|
||||
return apkUrl;
|
||||
}
|
||||
|
||||
Map<String, List<String>> addToErrorMap(
|
||||
Map<String, List<String>> errors, String appId, String error) {
|
||||
var tempIds = errors.remove(error);
|
||||
tempIds ??= [];
|
||||
tempIds.add(appId);
|
||||
errors.putIfAbsent(error, () => tempIds!);
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Given a list of AppIds, uses stored info about the apps to download APKs and install them
|
||||
// If the APKs can be installed silently, they are
|
||||
// If no BuildContext is provided, apps that require user interaction are ignored
|
||||
@@ -257,42 +238,41 @@ class AppsProvider with ChangeNotifier {
|
||||
Future<List<String>> downloadAndInstallLatestApps(
|
||||
List<String> appIds, BuildContext? context) async {
|
||||
List<String> appsToInstall = [];
|
||||
// For all specified Apps, filter out those for which:
|
||||
// 1. A URL cannot be picked
|
||||
// 2. That cannot be installed silently (IF no buildContext was given for interactive install)
|
||||
for (var id in appIds) {
|
||||
if (apps[id] == null) {
|
||||
throw 'App not found';
|
||||
throw ObtainiumError('App not found');
|
||||
}
|
||||
|
||||
String? apkUrl = await selectApkUrl(apps[id]!.app, context);
|
||||
|
||||
String? apkUrl = await confirmApkUrl(apps[id]!.app, context);
|
||||
if (apkUrl != null) {
|
||||
int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl);
|
||||
if (urlInd != apps[id]!.app.preferredApkIndex) {
|
||||
apps[id]!.app.preferredApkIndex = urlInd;
|
||||
await saveApps([apps[id]!.app]);
|
||||
}
|
||||
if (context != null ||
|
||||
(await canInstallSilently(apps[id]!.app) &&
|
||||
apps[id]!.app.apkUrls.length == 1)) {
|
||||
if (context != null || await canInstallSilently(apps[id]!.app)) {
|
||||
appsToInstall.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<String, List<String>> errors = {};
|
||||
|
||||
List<DownloadedApp?> downloadedFiles =
|
||||
// Download APKs for all Apps to be installed
|
||||
MultiAppMultiError errors = MultiAppMultiError();
|
||||
List<DownloadedApk?> downloadedFiles =
|
||||
await Future.wait(appsToInstall.map((id) async {
|
||||
try {
|
||||
return await downloadApp(apps[id]!.app);
|
||||
} catch (e) {
|
||||
addToErrorMap(errors, id, e.toString());
|
||||
errors.add(id, e.toString());
|
||||
}
|
||||
return null;
|
||||
}));
|
||||
downloadedFiles =
|
||||
downloadedFiles.where((element) => element != null).toList();
|
||||
|
||||
List<DownloadedApp> silentUpdates = [];
|
||||
List<DownloadedApp> regularInstalls = [];
|
||||
// Separate the Apps to install into silent and regular lists
|
||||
List<DownloadedApk> silentUpdates = [];
|
||||
List<DownloadedApk> regularInstalls = [];
|
||||
for (var f in downloadedFiles) {
|
||||
bool willBeSilent = await canInstallSilently(apps[f!.appId]!.app);
|
||||
if (willBeSilent) {
|
||||
@@ -302,10 +282,13 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// Move everything to the regular install list (since silent updates don't currently work) - TODO
|
||||
regularInstalls.addAll(silentUpdates);
|
||||
|
||||
// If Obtainium is being installed, it should be the last one
|
||||
List<DownloadedApp> moveObtainiumToStart(List<DownloadedApp> items) {
|
||||
List<DownloadedApk> moveObtainiumToStart(List<DownloadedApk> items) {
|
||||
String obtainiumId = 'imranr98_obtainium_${GitHub().host}';
|
||||
DownloadedApp? temp;
|
||||
DownloadedApk? temp;
|
||||
items.removeWhere((element) {
|
||||
bool res = element.appId == obtainiumId;
|
||||
if (res) {
|
||||
@@ -319,37 +302,29 @@ class AppsProvider with ChangeNotifier {
|
||||
return items;
|
||||
}
|
||||
|
||||
// TODO: Remove below line if silentupdates are ever figured out
|
||||
regularInstalls.addAll(silentUpdates);
|
||||
|
||||
silentUpdates = moveObtainiumToStart(silentUpdates);
|
||||
regularInstalls = moveObtainiumToStart(regularInstalls);
|
||||
|
||||
// TODO: Uncomment below if silentupdates are ever figured out
|
||||
// // Install silent updates (uncomment when it works - TODO)
|
||||
// for (var u in silentUpdates) {
|
||||
// await installApk(u, silent: true); // Would need to add silent option
|
||||
// }
|
||||
|
||||
if (context != null) {
|
||||
if (regularInstalls.isNotEmpty) {
|
||||
// ignore: use_build_context_synchronously
|
||||
await askUserToReturnToForeground(context, waitForFG: true);
|
||||
}
|
||||
// Do regular installs
|
||||
if (regularInstalls.isNotEmpty && context != null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
await waitForUserToReturnToForeground(context);
|
||||
for (var i in regularInstalls) {
|
||||
try {
|
||||
await installApk(i);
|
||||
} catch (e) {
|
||||
addToErrorMap(errors, i.appId, e.toString());
|
||||
errors.add(i.appId, e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.isNotEmpty) {
|
||||
String finalError = '';
|
||||
for (var e in errors.keys) {
|
||||
finalError +=
|
||||
'$e ${errors[e]!.map((e) => apps[e]!.app.name).toString()}. ';
|
||||
}
|
||||
throw finalError;
|
||||
|
||||
if (errors.content.isNotEmpty) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
return downloadedFiles.map((e) => e!.appId).toList();
|
||||
@@ -364,40 +339,6 @@ class AppsProvider with ChangeNotifier {
|
||||
return appsDir;
|
||||
}
|
||||
|
||||
// Delete all stored APKs except those likely to still be needed
|
||||
Future<void> deleteSavedAPKs() async {
|
||||
List<FileSystemEntity>? apks = (await getExternalStorageDirectory())
|
||||
?.listSync()
|
||||
.where((element) => element.path.endsWith('.apk'))
|
||||
.toList();
|
||||
if (apks != null && apks.isNotEmpty) {
|
||||
for (var apk in apks) {
|
||||
var shouldDelete = true;
|
||||
var temp = apk.path.split('/').last;
|
||||
temp = temp.substring(0, temp.length - 4);
|
||||
var fn = temp.split('-');
|
||||
if (fn.length == 3) {
|
||||
var possibleId = fn[0];
|
||||
var possibleVersion = fn[1];
|
||||
var possibleApkUrlIndex = fn[2];
|
||||
if (apps[possibleId] != null) {
|
||||
if (apps[possibleId] != null &&
|
||||
apps[possibleId]?.app != null &&
|
||||
apps[possibleId]!.app.installedVersion !=
|
||||
apps[possibleId]!.app.latestVersion &&
|
||||
apps[possibleId]!.app.latestVersion == possibleVersion &&
|
||||
apps[possibleId]!.app.preferredApkIndex.toString() ==
|
||||
possibleApkUrlIndex) {
|
||||
shouldDelete = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDelete) apk.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<AppInfo?> getInstalledInfo(String? packageName) async {
|
||||
if (packageName != null) {
|
||||
try {
|
||||
@@ -409,24 +350,37 @@ class AppsProvider with ChangeNotifier {
|
||||
return null;
|
||||
}
|
||||
|
||||
String standardizeVersionString(String versionString) {
|
||||
return versionString.characters
|
||||
.where((p0) => ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.']
|
||||
.contains(p0))
|
||||
.join('');
|
||||
}
|
||||
|
||||
// If the App says it is installed by installedInfo is null, set it to not installed
|
||||
// If the App says it is installed but installedInfo is null, set it to not installed
|
||||
// If the App says is is not installed but installedInfo exists, try to set it to installed as latest version...
|
||||
// ...if the latestVersion seems to match the version in installedInfo (not guaranteed)
|
||||
App? correctInstallStatus(App app, AppInfo? installedInfo) {
|
||||
// If that fails, just set it to the actual version string (all we can do at that point)
|
||||
// Don't save changes, just return the object if changes were made (else null)
|
||||
// If in a background isolate, return null straight away as the required plugin won't work anyways
|
||||
App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) {
|
||||
if (forBGTask) {
|
||||
return null; // Can't correct in the background isolate
|
||||
}
|
||||
var modded = false;
|
||||
if (installedInfo == null && app.installedVersion != null) {
|
||||
app.installedVersion = null;
|
||||
modded = true;
|
||||
}
|
||||
if (installedInfo != null && app.installedVersion == null) {
|
||||
if (standardizeVersionString(app.latestVersion) ==
|
||||
if (app.latestVersion.characters
|
||||
.where((p0) => [
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'.'
|
||||
].contains(p0))
|
||||
.join('') ==
|
||||
installedInfo.versionName) {
|
||||
app.installedVersion = app.latestVersion;
|
||||
} else {
|
||||
@@ -437,7 +391,7 @@ class AppsProvider with ChangeNotifier {
|
||||
return modded ? app : null;
|
||||
}
|
||||
|
||||
Future<void> loadApps({shouldCorrectInstallStatus = true}) async {
|
||||
Future<void> loadApps() async {
|
||||
while (loadingApps) {
|
||||
await Future.delayed(const Duration(microseconds: 1));
|
||||
}
|
||||
@@ -468,28 +422,26 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
loadingApps = false;
|
||||
notifyListeners();
|
||||
// For any that are not installed (by ID == package name), set to not installed if needed
|
||||
if (shouldCorrectInstallStatus) {
|
||||
List<App> modifiedApps = [];
|
||||
for (var app in apps.values) {
|
||||
var moddedApp = correctInstallStatus(app.app, app.installedInfo);
|
||||
if (moddedApp != null) {
|
||||
modifiedApps.add(moddedApp);
|
||||
}
|
||||
}
|
||||
if (modifiedApps.isNotEmpty) {
|
||||
await saveApps(modifiedApps, shouldCorrectInstallStatus: false);
|
||||
List<App> modifiedApps = [];
|
||||
for (var app in apps.values) {
|
||||
var moddedApp =
|
||||
getCorrectedInstallStatusAppIfPossible(app.app, app.installedInfo);
|
||||
if (moddedApp != null) {
|
||||
modifiedApps.add(moddedApp);
|
||||
}
|
||||
}
|
||||
if (modifiedApps.isNotEmpty) {
|
||||
await saveApps(modifiedApps);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveApps(List<App> apps,
|
||||
{bool shouldCorrectInstallStatus = true}) async {
|
||||
{bool attemptToCorrectInstallStatus = true}) async {
|
||||
for (var app in apps) {
|
||||
AppInfo? info = await getInstalledInfo(app.id);
|
||||
app.name = info?.name ?? app.name;
|
||||
if (shouldCorrectInstallStatus) {
|
||||
app = correctInstallStatus(app, info) ?? app;
|
||||
if (attemptToCorrectInstallStatus) {
|
||||
app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app;
|
||||
}
|
||||
File('${(await getAppsDir()).path}/${app.id}.json')
|
||||
.writeAsStringSync(jsonEncode(app.toJson()));
|
||||
@@ -515,15 +467,7 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
bool checkAppObjectForUpdate(App app) {
|
||||
if (!apps.containsKey(app.id)) {
|
||||
throw 'App not found';
|
||||
}
|
||||
return app.latestVersion != apps[app.id]?.app.installedVersion;
|
||||
}
|
||||
|
||||
Future<App?> getUpdate(String appId,
|
||||
{bool shouldCorrectInstallStatus = true}) async {
|
||||
Future<App?> checkUpdate(String appId) async {
|
||||
App? currentApp = apps[appId]!.app;
|
||||
SourceProvider sourceProvider = SourceProvider();
|
||||
App newApp = await sourceProvider.getApp(
|
||||
@@ -536,51 +480,39 @@ class AppsProvider with ChangeNotifier {
|
||||
if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
|
||||
newApp.preferredApkIndex = currentApp.preferredApkIndex;
|
||||
}
|
||||
await saveApps([newApp],
|
||||
shouldCorrectInstallStatus: shouldCorrectInstallStatus);
|
||||
await saveApps([newApp]);
|
||||
return newApp.latestVersion != currentApp.latestVersion ? newApp : null;
|
||||
}
|
||||
|
||||
Future<List<App>> checkUpdates(
|
||||
{DateTime? ignoreAfter,
|
||||
bool immediatelyThrowRateLimitError = false,
|
||||
bool shouldCorrectInstallStatus = true,
|
||||
bool immediatelyThrowSocketError = false}) async {
|
||||
{DateTime? ignoreAppsCheckedAfter,
|
||||
bool throwErrorsForRetry = false}) async {
|
||||
List<App> updates = [];
|
||||
Map<String, List<String>> errors = {};
|
||||
MultiAppMultiError errors = MultiAppMultiError();
|
||||
if (!gettingUpdates) {
|
||||
gettingUpdates = true;
|
||||
|
||||
try {
|
||||
List<String> appIds = apps.keys.toList();
|
||||
if (ignoreAfter != null) {
|
||||
appIds = appIds
|
||||
.where((id) =>
|
||||
apps[id]!.app.lastUpdateCheck == null ||
|
||||
apps[id]!.app.lastUpdateCheck!.isBefore(ignoreAfter))
|
||||
.toList();
|
||||
}
|
||||
List<String> appIds = apps.values
|
||||
.where((app) =>
|
||||
app.app.lastUpdateCheck == null ||
|
||||
ignoreAppsCheckedAfter == null ||
|
||||
app.app.lastUpdateCheck!.isBefore(ignoreAppsCheckedAfter))
|
||||
.map((e) => e.app.id)
|
||||
.toList();
|
||||
appIds.sort((a, b) => (apps[a]!.app.lastUpdateCheck ??
|
||||
DateTime.fromMicrosecondsSinceEpoch(0))
|
||||
.compareTo(apps[b]!.app.lastUpdateCheck ??
|
||||
DateTime.fromMicrosecondsSinceEpoch(0)));
|
||||
|
||||
for (int i = 0; i < appIds.length; i++) {
|
||||
App? newApp;
|
||||
try {
|
||||
newApp = await getUpdate(appIds[i],
|
||||
shouldCorrectInstallStatus: shouldCorrectInstallStatus);
|
||||
newApp = await checkUpdate(appIds[i]);
|
||||
} catch (e) {
|
||||
if (e is RateLimitError && immediatelyThrowRateLimitError) {
|
||||
if ((e is RateLimitError || e is SocketException) &&
|
||||
throwErrorsForRetry) {
|
||||
rethrow;
|
||||
}
|
||||
if (e is SocketException && immediatelyThrowSocketError) {
|
||||
rethrow;
|
||||
}
|
||||
var tempIds = errors.remove(e.toString());
|
||||
tempIds ??= [];
|
||||
tempIds.add(appIds[i]);
|
||||
errors.putIfAbsent(e.toString(), () => tempIds!);
|
||||
errors.add(appIds[i], e.toString());
|
||||
}
|
||||
if (newApp != null) {
|
||||
updates.add(newApp);
|
||||
@@ -590,18 +522,13 @@ class AppsProvider with ChangeNotifier {
|
||||
gettingUpdates = false;
|
||||
}
|
||||
}
|
||||
if (errors.isNotEmpty) {
|
||||
String finalError = '';
|
||||
for (var e in errors.keys) {
|
||||
finalError +=
|
||||
'$e ${errors[e]!.map((e) => apps[e]!.app.name).toString()}. ';
|
||||
}
|
||||
throw finalError;
|
||||
if (errors.content.isNotEmpty) {
|
||||
throw errors;
|
||||
}
|
||||
return updates;
|
||||
}
|
||||
|
||||
List<String> getExistingUpdates(
|
||||
List<String> findExistingUpdates(
|
||||
{bool installedOnly = false, bool nonInstalledOnly = false}) {
|
||||
List<String> updateAppIds = [];
|
||||
List<String> appIds = apps.keys.toList();
|
||||
@@ -635,7 +562,6 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<int> importApps(String appsJSON) async {
|
||||
// File picker does not work in android 13, so the user must paste the JSON directly into Obtainium to import Apps
|
||||
List<App> importedApps = (jsonDecode(appsJSON) as List<dynamic>)
|
||||
.map((e) => App.fromJson(e))
|
||||
.toList();
|
||||
|
@@ -12,6 +12,7 @@ import 'package:obtainium/app_sources/mullvad.dart';
|
||||
import 'package:obtainium/app_sources/signal.dart';
|
||||
import 'package:obtainium/app_sources/sourceforge.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/mass_app_sources/githubstars.dart';
|
||||
|
||||
class AppNames {
|
||||
@@ -90,12 +91,7 @@ class App {
|
||||
};
|
||||
}
|
||||
|
||||
escapeRegEx(String s) {
|
||||
return s.replaceAllMapped(RegExp(r'[.*+?^${}()|[\]\\]'), (x) {
|
||||
return '\\${x[0]}';
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure the input is starts with HTTPS and has no WWW
|
||||
preStandardizeUrl(String url) {
|
||||
if (url.toLowerCase().indexOf('http://') != 0 &&
|
||||
url.toLowerCase().indexOf('https://') != 0) {
|
||||
@@ -145,7 +141,7 @@ abstract class AppSource {
|
||||
Future<String> apkUrlPrefetchModifier(String apkUrl);
|
||||
}
|
||||
|
||||
abstract class MassAppSource {
|
||||
abstract class MassAppUrlSource {
|
||||
late String name;
|
||||
late List<String> requiredArgs;
|
||||
Future<List<String>> getUrls(List<String> args);
|
||||
@@ -164,8 +160,8 @@ class SourceProvider {
|
||||
// APKMirror()
|
||||
];
|
||||
|
||||
// Add more mass source classes here so they are available via the service
|
||||
List<MassAppSource> massSources = [GitHubStars()];
|
||||
// Add more mass url source classes here so they are available via the service
|
||||
List<MassAppUrlSource> massUrlSources = [GitHubStars()];
|
||||
|
||||
AppSource getSource(String url) {
|
||||
url = preStandardizeUrl(url);
|
||||
@@ -177,12 +173,12 @@ class SourceProvider {
|
||||
}
|
||||
}
|
||||
if (source == null) {
|
||||
throw 'URL does not match a known source';
|
||||
throw UnsupportedURLError();
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
bool doesSourceHaveRequiredAdditionalData(AppSource source) {
|
||||
bool ifSourceAppsRequireAdditionalData(AppSource source) {
|
||||
for (var row in source.additionalDataFormItems) {
|
||||
for (var element in row) {
|
||||
if (element.required) {
|
||||
@@ -217,8 +213,7 @@ class SourceProvider {
|
||||
DateTime.now());
|
||||
}
|
||||
|
||||
/// Returns a length 2 list, where the first element is a list of Apps and
|
||||
/// the second is a Map<String, dynamic> of URLs and errors
|
||||
// Returns errors in [results, errors] instead of throwing them
|
||||
Future<List<dynamic>> getApps(List<String> urls,
|
||||
{List<String> ignoreUrls = const []}) async {
|
||||
List<App> apps = [];
|
||||
|
Reference in New Issue
Block a user