mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 13:26:43 +02:00
Bugfixes + started work on silent udates
This commit is contained in:
@ -30,6 +30,16 @@
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
5
android/app/src/main/res/xml/file_paths.xml
Normal file
5
android/app/src/main/res/xml/file_paths.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-path path="Android/data/dev.imranr.obtainium/" name="files_root" />
|
||||
<external-path path="." name="external_storage_root" />
|
||||
</paths>
|
@ -25,8 +25,12 @@ class _AppPageState extends State<AppPage> {
|
||||
var sourceProvider = SourceProvider();
|
||||
AppInMemory? app = appsProvider.apps[widget.appId];
|
||||
var source = app != null ? sourceProvider.getSource(app.app.url) : null;
|
||||
if (app?.app.installedVersion != null) {
|
||||
appsProvider.getUpdate(app!.app.id);
|
||||
if (!appsProvider.areDownloadsRunning()) {
|
||||
appsProvider.getUpdate(app!.app.id).catchError((e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(e.toString())),
|
||||
);
|
||||
});
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: settingsProvider.showAppWebpage ? AppBar() : null,
|
||||
@ -96,104 +100,112 @@ class _AppPageState extends State<AppPage> {
|
||||
children: [
|
||||
if (app?.app.installedVersion != app?.app.latestVersion)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'App Already ${app?.app.installedVersion == null ? 'Installed' : 'Updated'}?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('No')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
var updatedApp = app?.app;
|
||||
if (updatedApp != null) {
|
||||
updatedApp.installedVersion =
|
||||
updatedApp.latestVersion;
|
||||
appsProvider
|
||||
.saveApp(updatedApp);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text(
|
||||
'Yes, Mark as Installed'))
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
onPressed: app?.downloadProgress != null
|
||||
? null
|
||||
: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'App Already ${app?.app.installedVersion == null ? 'Installed' : 'Updated'}?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
.pop();
|
||||
},
|
||||
child: const Text('No')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback
|
||||
.selectionClick();
|
||||
var updatedApp = app?.app;
|
||||
if (updatedApp != null) {
|
||||
updatedApp
|
||||
.installedVersion =
|
||||
updatedApp
|
||||
.latestVersion;
|
||||
appsProvider.saveApp(
|
||||
updatedApp);
|
||||
}
|
||||
Navigator.of(context)
|
||||
.pop();
|
||||
},
|
||||
child: const Text(
|
||||
'Yes, Mark as Installed'))
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
tooltip: 'Mark as Installed',
|
||||
icon: const Icon(Icons.done))
|
||||
else
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return AlertDialog(
|
||||
title: const Text('App Not Installed?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('No')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
var updatedApp = app?.app;
|
||||
if (updatedApp != null) {
|
||||
updatedApp.installedVersion =
|
||||
null;
|
||||
appsProvider
|
||||
.saveApp(updatedApp);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text(
|
||||
'Yes, Mark as Not Installed'))
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
onPressed: app?.downloadProgress != null
|
||||
? null
|
||||
: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return AlertDialog(
|
||||
title: const Text(
|
||||
'App Not Installed?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
.pop();
|
||||
},
|
||||
child: const Text('No')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback
|
||||
.selectionClick();
|
||||
var updatedApp = app?.app;
|
||||
if (updatedApp != null) {
|
||||
updatedApp
|
||||
.installedVersion =
|
||||
null;
|
||||
appsProvider.saveApp(
|
||||
updatedApp);
|
||||
}
|
||||
Navigator.of(context)
|
||||
.pop();
|
||||
},
|
||||
child: const Text(
|
||||
'Yes, Mark as Not Installed'))
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
tooltip: 'Mark as Not Installed',
|
||||
icon: const Icon(Icons.no_cell_outlined)),
|
||||
if (source != null &&
|
||||
source.additionalDataFormItems.isNotEmpty)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: 'Additional Options',
|
||||
items: source.additionalDataFormItems,
|
||||
defaultValues: app != null
|
||||
? app.app.additionalData
|
||||
: source.additionalDataDefaults);
|
||||
}).then((values) {
|
||||
if (app != null && values != null) {
|
||||
var changedApp = app.app;
|
||||
changedApp.additionalData = values;
|
||||
sourceProvider
|
||||
.getApp(source, changedApp.url,
|
||||
changedApp.additionalData)
|
||||
.then((finalChangedApp) {
|
||||
appsProvider.saveApp(finalChangedApp);
|
||||
}).catchError((e) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
SnackBar(content: Text(e.toString())),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
onPressed: app?.downloadProgress != null
|
||||
? null
|
||||
: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: 'Additional Options',
|
||||
items: source
|
||||
.additionalDataFormItems,
|
||||
defaultValues: app != null
|
||||
? app.app.additionalData
|
||||
: source
|
||||
.additionalDataDefaults);
|
||||
}).then((values) {
|
||||
if (app != null && values != null) {
|
||||
var changedApp = app.app;
|
||||
changedApp.additionalData = values;
|
||||
appsProvider.saveApp(changedApp);
|
||||
}
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.settings)),
|
||||
const SizedBox(width: 16.0),
|
||||
Expanded(
|
||||
|
@ -5,6 +5,7 @@ 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:obtainium/providers/notifications_provider.dart';
|
||||
@ -13,7 +14,7 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:flutter_fgbg/flutter_fgbg.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:install_plugin_v2/install_plugin_v2.dart';
|
||||
import 'package:flutter_install_app/flutter_install_app.dart';
|
||||
|
||||
class AppInMemory {
|
||||
late App app;
|
||||
@ -96,21 +97,55 @@ class AppsProvider with ChangeNotifier {
|
||||
.where((element) => element.downloadProgress != null)
|
||||
.isNotEmpty;
|
||||
|
||||
// Given an AppId, uses stored info about the app to download an APK (with user input if needed) and install it
|
||||
// Installs can only be done in the foreground, so a notification is sent to get the user's attention if needed
|
||||
Future<bool> canInstallSilently(App app) async {
|
||||
var osInfo = await DeviceInfoPlugin().androidInfo;
|
||||
return app.installedVersion != null &&
|
||||
osInfo.version.sdkInt! >= 30 &&
|
||||
osInfo.version.release!.compareTo('12') >= 0;
|
||||
}
|
||||
|
||||
Future<void> askUserToReturnToForeground(BuildContext context) async {
|
||||
NotificationsProvider notificationsProvider =
|
||||
context.read<NotificationsProvider>();
|
||||
if (!isForeground) {
|
||||
await notificationsProvider.notify(completeInstallationNotification,
|
||||
cancelExisting: true);
|
||||
await FGBGEvents.stream.first == FGBGType.foreground;
|
||||
await notificationsProvider.cancel(completeInstallationNotification.id);
|
||||
// We need to wait for the App to come to the foreground to install it
|
||||
// Can't try to call install plugin in a background isolate (may not have worked anyways) because of:
|
||||
// https://github.com/flutter/flutter/issues/13937
|
||||
}
|
||||
}
|
||||
|
||||
// Unfortunately this 'await' does not actually wait for the APK to finish installing
|
||||
// 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(ApkFile file) async {
|
||||
await AppInstaller.installApk(file.file.path, actionRequired: false);
|
||||
apps[file.appId]!.app.installedVersion =
|
||||
apps[file.appId]!.app.latestVersion;
|
||||
await saveApp(apps[file.appId]!.app);
|
||||
}
|
||||
|
||||
// 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 user input is needed and the App is in the background, a notification is sent to get the user's attention
|
||||
// Returns upon successful download, regardless of installation result
|
||||
Future<bool> downloadAndInstallLatestApp(
|
||||
List<String> appIds, BuildContext context) async {
|
||||
NotificationsProvider notificationsProvider =
|
||||
context.read<NotificationsProvider>();
|
||||
Map<String, String> appsToInstall = {};
|
||||
for (var id in appIds) {
|
||||
if (apps[id] == null) {
|
||||
throw 'App not found';
|
||||
}
|
||||
|
||||
// If the App has more than one APK, the user should pick one
|
||||
String? apkUrl = apps[id]!.app.apkUrls[apps[id]!.app.preferredApkIndex];
|
||||
if (apps[id]!.app.apkUrls.length > 1) {
|
||||
// ignore: use_build_context_synchronously
|
||||
await askUserToReturnToForeground(context);
|
||||
apkUrl = await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
@ -120,6 +155,8 @@ class AppsProvider with ChangeNotifier {
|
||||
// If the picked APK comes from an origin different from the source, get user confirmation
|
||||
if (apkUrl != null &&
|
||||
Uri.parse(apkUrl).origin != Uri.parse(apps[id]!.app.url).origin) {
|
||||
// ignore: use_build_context_synchronously
|
||||
await askUserToReturnToForeground(context);
|
||||
if (await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
@ -143,23 +180,25 @@ class AppsProvider with ChangeNotifier {
|
||||
List<ApkFile> downloadedFiles = await Future.wait(appsToInstall.entries
|
||||
.map((entry) => downloadApp(entry.value, entry.key)));
|
||||
|
||||
if (!isForeground) {
|
||||
await notificationsProvider.notify(completeInstallationNotification,
|
||||
cancelExisting: true);
|
||||
await FGBGEvents.stream.first == FGBGType.foreground;
|
||||
await notificationsProvider.cancel(completeInstallationNotification.id);
|
||||
// We need to wait for the App to come to the foreground to install it
|
||||
// Can't try to call install plugin in a background isolate (may not have worked anyways) because of:
|
||||
// https://github.com/flutter/flutter/issues/13937
|
||||
List<ApkFile> silentUpdates = [];
|
||||
List<ApkFile> regularInstalls = [];
|
||||
for (var f in downloadedFiles) {
|
||||
bool willBeSilent = await canInstallSilently(apps[f.appId]!.app);
|
||||
if (willBeSilent) {
|
||||
silentUpdates.add(f);
|
||||
} else {
|
||||
regularInstalls.add(f);
|
||||
}
|
||||
}
|
||||
|
||||
// Unfortunately this 'await' does not actually wait for the APK to finish installing
|
||||
// So we only know that the install prompt was shown, but the user could still cancel w/o us knowing
|
||||
// This also does not use the 'session-based' installer API, so background/silent updates are impossible
|
||||
for (var f in downloadedFiles) {
|
||||
await InstallPlugin.installApk(f.file.path, 'dev.imranr.obtainium');
|
||||
apps[f.appId]!.app.installedVersion = apps[f.appId]!.app.latestVersion;
|
||||
await saveApp(apps[f.appId]!.app);
|
||||
for (var u in silentUpdates) {
|
||||
await installApk(u);
|
||||
}
|
||||
|
||||
for (var i in regularInstalls) {
|
||||
// ignore: use_build_context_synchronously
|
||||
await askUserToReturnToForeground(context);
|
||||
await installApk(i);
|
||||
}
|
||||
|
||||
return downloadedFiles.isNotEmpty;
|
||||
|
14
pubspec.lock
14
pubspec.lock
@ -188,6 +188,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
flutter_install_app:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_install_app
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -275,13 +282,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
install_plugin_v2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: install_plugin_v2
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -44,7 +44,6 @@ dependencies:
|
||||
webview_flutter: ^3.0.4
|
||||
workmanager: ^0.5.0
|
||||
dynamic_color: ^1.5.4
|
||||
install_plugin_v2: ^1.0.0 # Try replacing this
|
||||
html: ^0.15.0
|
||||
shared_preferences: ^2.0.15
|
||||
url_launcher: ^6.1.5
|
||||
@ -53,6 +52,7 @@ dependencies:
|
||||
device_info_plus: ^4.1.2
|
||||
file_picker: ^5.1.0
|
||||
animations: ^2.0.4
|
||||
flutter_install_app: ^1.3.0
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
|
Reference in New Issue
Block a user