diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index ccee49f..1062122 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -30,6 +30,16 @@
+
+
+
+
diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..670a26f
--- /dev/null
+++ b/android/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/lib/pages/app.dart b/lib/pages/app.dart
index 7cf0fc4..49cacbd 100644
--- a/lib/pages/app.dart
+++ b/lib/pages/app.dart
@@ -25,8 +25,12 @@ class _AppPageState extends State {
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 {
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(
diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart
index 64ff3a3..ca35304 100644
--- a/lib/providers/apps_provider.dart
+++ b/lib/providers/apps_provider.dart
@@ -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 canInstallSilently(App app) async {
+ var osInfo = await DeviceInfoPlugin().androidInfo;
+ return app.installedVersion != null &&
+ osInfo.version.sdkInt! >= 30 &&
+ osInfo.version.release!.compareTo('12') >= 0;
+ }
+
+ Future askUserToReturnToForeground(BuildContext context) async {
+ NotificationsProvider notificationsProvider =
+ context.read();
+ 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 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 downloadAndInstallLatestApp(
List appIds, BuildContext context) async {
- NotificationsProvider notificationsProvider =
- context.read();
Map 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 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 silentUpdates = [];
+ List 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;
diff --git a/pubspec.lock b/pubspec.lock
index 64faa17..0255f80 100644
--- a/pubspec.lock
+++ b/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:
diff --git a/pubspec.yaml b/pubspec.yaml
index 3d1c191..919ce6f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -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: