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
|
<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" />
|
||||||
|
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();
|
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(
|
||||||
|
@ -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;
|
||||||
|
14
pubspec.lock
14
pubspec.lock
@ -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:
|
||||||
|
@ -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:
|
||||||
|
Reference in New Issue
Block a user