Bugfixes + started work on silent udates

This commit is contained in:
Imran Remtulla
2022-09-24 15:00:47 -04:00
parent 22dd8253a9
commit b65c6e1d41
6 changed files with 185 additions and 119 deletions

View File

@ -30,6 +30,16 @@
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> 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> </application>
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

View 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>

View File

@ -25,8 +25,12 @@ class _AppPageState extends State<AppPage> {
var sourceProvider = SourceProvider(); var sourceProvider = SourceProvider();
AppInMemory? app = appsProvider.apps[widget.appId]; AppInMemory? app = appsProvider.apps[widget.appId];
var source = app != null ? sourceProvider.getSource(app.app.url) : null; var source = app != null ? sourceProvider.getSource(app.app.url) : null;
if (app?.app.installedVersion != null) { if (!appsProvider.areDownloadsRunning()) {
appsProvider.getUpdate(app!.app.id); appsProvider.getUpdate(app!.app.id).catchError((e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString())),
);
});
} }
return Scaffold( return Scaffold(
appBar: settingsProvider.showAppWebpage ? AppBar() : null, appBar: settingsProvider.showAppWebpage ? AppBar() : null,
@ -96,104 +100,112 @@ class _AppPageState extends State<AppPage> {
children: [ children: [
if (app?.app.installedVersion != app?.app.latestVersion) if (app?.app.installedVersion != app?.app.latestVersion)
IconButton( IconButton(
onPressed: () { onPressed: app?.downloadProgress != null
showDialog( ? null
context: context, : () {
builder: (BuildContext ctx) { showDialog(
return AlertDialog( context: context,
title: Text( builder: (BuildContext ctx) {
'App Already ${app?.app.installedVersion == null ? 'Installed' : 'Updated'}?'), return AlertDialog(
actions: [ title: Text(
TextButton( 'App Already ${app?.app.installedVersion == null ? 'Installed' : 'Updated'}?'),
onPressed: () { actions: [
Navigator.of(context).pop(); TextButton(
}, onPressed: () {
child: const Text('No')), Navigator.of(context)
TextButton( .pop();
onPressed: () { },
HapticFeedback.selectionClick(); child: const Text('No')),
var updatedApp = app?.app; TextButton(
if (updatedApp != null) { onPressed: () {
updatedApp.installedVersion = HapticFeedback
updatedApp.latestVersion; .selectionClick();
appsProvider var updatedApp = app?.app;
.saveApp(updatedApp); if (updatedApp != null) {
} updatedApp
Navigator.of(context).pop(); .installedVersion =
}, updatedApp
child: const Text( .latestVersion;
'Yes, Mark as Installed')) appsProvider.saveApp(
], updatedApp);
); }
}); Navigator.of(context)
}, .pop();
},
child: const Text(
'Yes, Mark as Installed'))
],
);
});
},
tooltip: 'Mark as Installed', tooltip: 'Mark as Installed',
icon: const Icon(Icons.done)) icon: const Icon(Icons.done))
else else
IconButton( IconButton(
onPressed: () { onPressed: app?.downloadProgress != null
showDialog( ? null
context: context, : () {
builder: (BuildContext ctx) { showDialog(
return AlertDialog( context: context,
title: const Text('App Not Installed?'), builder: (BuildContext ctx) {
actions: [ return AlertDialog(
TextButton( title: const Text(
onPressed: () { 'App Not Installed?'),
Navigator.of(context).pop(); actions: [
}, TextButton(
child: const Text('No')), onPressed: () {
TextButton( Navigator.of(context)
onPressed: () { .pop();
HapticFeedback.selectionClick(); },
var updatedApp = app?.app; child: const Text('No')),
if (updatedApp != null) { TextButton(
updatedApp.installedVersion = onPressed: () {
null; HapticFeedback
appsProvider .selectionClick();
.saveApp(updatedApp); var updatedApp = app?.app;
} if (updatedApp != null) {
Navigator.of(context).pop(); updatedApp
}, .installedVersion =
child: const Text( null;
'Yes, Mark as Not Installed')) appsProvider.saveApp(
], updatedApp);
); }
}); Navigator.of(context)
}, .pop();
},
child: const Text(
'Yes, Mark as Not Installed'))
],
);
});
},
tooltip: 'Mark as Not Installed', tooltip: 'Mark as Not Installed',
icon: const Icon(Icons.no_cell_outlined)), icon: const Icon(Icons.no_cell_outlined)),
if (source != null && if (source != null &&
source.additionalDataFormItems.isNotEmpty) source.additionalDataFormItems.isNotEmpty)
IconButton( IconButton(
onPressed: () { onPressed: app?.downloadProgress != null
showDialog( ? null
context: context, : () {
builder: (BuildContext ctx) { showDialog(
return GeneratedFormModal( context: context,
title: 'Additional Options', builder: (BuildContext ctx) {
items: source.additionalDataFormItems, return GeneratedFormModal(
defaultValues: app != null title: 'Additional Options',
? app.app.additionalData items: source
: source.additionalDataDefaults); .additionalDataFormItems,
}).then((values) { defaultValues: app != null
if (app != null && values != null) { ? app.app.additionalData
var changedApp = app.app; : source
changedApp.additionalData = values; .additionalDataDefaults);
sourceProvider }).then((values) {
.getApp(source, changedApp.url, if (app != null && values != null) {
changedApp.additionalData) var changedApp = app.app;
.then((finalChangedApp) { changedApp.additionalData = values;
appsProvider.saveApp(finalChangedApp); appsProvider.saveApp(changedApp);
}).catchError((e) { }
ScaffoldMessenger.of(context) });
.showSnackBar( },
SnackBar(content: Text(e.toString())),
);
});
}
});
},
icon: const Icon(Icons.settings)), icon: const Icon(Icons.settings)),
const SizedBox(width: 16.0), const SizedBox(width: 16.0),
Expanded( Expanded(

View File

@ -5,6 +5,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:obtainium/providers/notifications_provider.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:flutter_fgbg/flutter_fgbg.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
import 'package:http/http.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 { class AppInMemory {
late App app; late App app;
@ -96,21 +97,55 @@ class AppsProvider with ChangeNotifier {
.where((element) => element.downloadProgress != null) .where((element) => element.downloadProgress != null)
.isNotEmpty; .isNotEmpty;
// Given an AppId, uses stored info about the app to download an APK (with user input if needed) and install it Future<bool> canInstallSilently(App app) async {
// Installs can only be done in the foreground, so a notification is sent to get the user's attention if needed 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 // Returns upon successful download, regardless of installation result
Future<bool> downloadAndInstallLatestApp( Future<bool> downloadAndInstallLatestApp(
List<String> appIds, BuildContext context) async { List<String> appIds, BuildContext context) async {
NotificationsProvider notificationsProvider =
context.read<NotificationsProvider>();
Map<String, String> appsToInstall = {}; Map<String, String> appsToInstall = {};
for (var id in appIds) { for (var id in appIds) {
if (apps[id] == null) { if (apps[id] == null) {
throw 'App not found'; throw 'App not found';
} }
// If the App has more than one APK, the user should pick one // If the App has more than one APK, the user should pick one
String? apkUrl = apps[id]!.app.apkUrls[apps[id]!.app.preferredApkIndex]; String? apkUrl = apps[id]!.app.apkUrls[apps[id]!.app.preferredApkIndex];
if (apps[id]!.app.apkUrls.length > 1) { if (apps[id]!.app.apkUrls.length > 1) {
// ignore: use_build_context_synchronously
await askUserToReturnToForeground(context);
apkUrl = await showDialog( apkUrl = await showDialog(
context: context, context: context,
builder: (BuildContext ctx) { 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 the picked APK comes from an origin different from the source, get user confirmation
if (apkUrl != null && if (apkUrl != null &&
Uri.parse(apkUrl).origin != Uri.parse(apps[id]!.app.url).origin) { Uri.parse(apkUrl).origin != Uri.parse(apps[id]!.app.url).origin) {
// ignore: use_build_context_synchronously
await askUserToReturnToForeground(context);
if (await showDialog( if (await showDialog(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
@ -143,23 +180,25 @@ class AppsProvider with ChangeNotifier {
List<ApkFile> downloadedFiles = await Future.wait(appsToInstall.entries List<ApkFile> downloadedFiles = await Future.wait(appsToInstall.entries
.map((entry) => downloadApp(entry.value, entry.key))); .map((entry) => downloadApp(entry.value, entry.key)));
if (!isForeground) { List<ApkFile> silentUpdates = [];
await notificationsProvider.notify(completeInstallationNotification, List<ApkFile> regularInstalls = [];
cancelExisting: true); for (var f in downloadedFiles) {
await FGBGEvents.stream.first == FGBGType.foreground; bool willBeSilent = await canInstallSilently(apps[f.appId]!.app);
await notificationsProvider.cancel(completeInstallationNotification.id); if (willBeSilent) {
// We need to wait for the App to come to the foreground to install it silentUpdates.add(f);
// Can't try to call install plugin in a background isolate (may not have worked anyways) because of: } else {
// https://github.com/flutter/flutter/issues/13937 regularInstalls.add(f);
}
} }
// Unfortunately this 'await' does not actually wait for the APK to finish installing for (var u in silentUpdates) {
// So we only know that the install prompt was shown, but the user could still cancel w/o us knowing await installApk(u);
// 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'); for (var i in regularInstalls) {
apps[f.appId]!.app.installedVersion = apps[f.appId]!.app.latestVersion; // ignore: use_build_context_synchronously
await saveApp(apps[f.appId]!.app); await askUserToReturnToForeground(context);
await installApk(i);
} }
return downloadedFiles.isNotEmpty; return downloadedFiles.isNotEmpty;

View File

@ -188,6 +188,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.0" 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: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -275,13 +282,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.0" 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: js:
dependency: transitive dependency: transitive
description: description:

View File

@ -44,7 +44,6 @@ dependencies:
webview_flutter: ^3.0.4 webview_flutter: ^3.0.4
workmanager: ^0.5.0 workmanager: ^0.5.0
dynamic_color: ^1.5.4 dynamic_color: ^1.5.4
install_plugin_v2: ^1.0.0 # Try replacing this
html: ^0.15.0 html: ^0.15.0
shared_preferences: ^2.0.15 shared_preferences: ^2.0.15
url_launcher: ^6.1.5 url_launcher: ^6.1.5
@ -53,6 +52,7 @@ dependencies:
device_info_plus: ^4.1.2 device_info_plus: ^4.1.2
file_picker: ^5.1.0 file_picker: ^5.1.0
animations: ^2.0.4 animations: ^2.0.4
flutter_install_app: ^1.3.0
dev_dependencies: dev_dependencies: