Compare commits

...

4 Commits

Author SHA1 Message Date
d063bca474 Merge pull request #506 from ImranR98/dev
Switched to synchronous install plugin (#99, #459)
2023-04-30 02:59:49 -04:00
7c592756fe Smarter APK caching (#459) 2023-04-30 02:47:53 -04:00
08586870fb Tweak use of attemptToCorrectInstallStatus 2023-04-30 02:28:14 -04:00
8b123acdcd Switched to synchronous install plugin 2023-04-30 02:23:53 -04:00
9 changed files with 101 additions and 110 deletions

View File

@ -34,7 +34,6 @@ Currently supported App sources:
height="80">](https://apt.izzysoft.de/fdroid/index/apk/dev.imranr.obtainium)
## Limitations
- App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected.
- Auto (unattended) updates are unsupported due to a lack of any capable Flutter plugin.
- For some sources, data is gathered using Web scraping and can easily break due to changes in website design. In such cases, more reliable methods may be unavailable.

View File

@ -25,6 +25,11 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action
android:name="com.android_package_installer.content.SESSION_API_PACKAGE_INSTALLED"
android:exported="false"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
@ -46,9 +51,18 @@
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="dev.imranr.obtainium"
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" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>

View File

@ -2,4 +2,5 @@
<paths>
<external-path path="Android/data/dev.imranr.obtainium/" name="files_root" />
<external-path path="." name="external_storage_root" />
<external-path name="external_files" path="."/>
</paths>

View File

@ -1,3 +1,4 @@
import 'package:android_package_installer/android_package_installer.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:obtainium/providers/logs_provider.dart';
@ -44,6 +45,11 @@ class DowngradeError extends ObtainiumError {
DowngradeError() : super(tr('cantInstallOlderVersion'));
}
class InstallError extends ObtainiumError {
InstallError(int code)
: super(PackageInstallerStatus.byCode(code).name.substring(7));
}
class IDChangedError extends ObtainiumError {
IDChangedError() : super(tr('appIdMismatch'));
}

View File

@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
// ignore: implementation_imports
import 'package:easy_localization/src/localization.dart';
const String currentVersion = '0.12.0';
const String currentVersion = '0.12.1';
const String currentReleaseTag =
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES

View File

@ -6,11 +6,11 @@ import 'dart:convert';
import 'dart:io';
import 'package:android_intent_plus/flag.dart';
import 'package:android_package_installer/android_package_installer.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:install_plugin_v2/install_plugin_v2.dart';
import 'package:installed_apps/app_info.dart';
import 'package:installed_apps/installed_apps.dart';
import 'package:obtainium/components/generated_form.dart';
@ -113,21 +113,20 @@ class AppsProvider with ChangeNotifier {
() 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') ||
element.path.endsWith('.apk.part'))
.forEach((apk) {
apk.delete();
// Delete any partial APKs
(await getExternalCacheDirectories())
?.first
.listSync()
.where((element) => element.path.endsWith('.apk.part'))
.forEach((partialApk) {
partialApk.delete();
});
}();
}
downloadFile(String url, String fileName, Function? onProgress,
{bool useExisting = true}) async {
var destDir = (await getExternalStorageDirectory())!.path;
var destDir = (await getExternalCacheDirectories())!.first.path;
StreamedResponse response =
await Client().send(Request('GET', Uri.parse(url)));
File downloadedFile = File('$destDir/$fileName');
@ -191,15 +190,6 @@ class AppsProvider with ChangeNotifier {
}
prevProg = prog;
});
// Delete older versions of the APK if any
for (var file in downloadedFile.parent.listSync()) {
var fn = file.path.split('/').last;
if (fn.startsWith('${app.id}-') &&
fn.endsWith('.apk') &&
fn != fileName) {
file.delete();
}
}
// 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);
@ -217,6 +207,15 @@ class AppsProvider with ChangeNotifier {
await saveApps([app], onlyIfExists: !isTempId);
}
}
// Delete older versions of the APK if any
for (var file in downloadedFile.parent.listSync()) {
var fn = file.path.split('/').last;
if (fn.startsWith('${app.id}-') &&
fn.endsWith('.apk') &&
fn != fileName) {
file.delete();
}
}
return DownloadedApk(app.id, downloadedFile);
} finally {
notificationsProvider?.cancel(notifId);
@ -268,7 +267,8 @@ 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(DownloadedApk file) async {
Future<void> installApk(DownloadedApk file, {bool silent = false}) async {
// TODO: Use 'silent' when/if ever possible
var newInfo = await PackageArchiveInfo.fromPath(file.file.path);
AppInfo? appInfo;
try {
@ -281,16 +281,16 @@ class AppsProvider with ChangeNotifier {
!(await canDowngradeApps())) {
throw DowngradeError();
}
await InstallPlugin.installApk(file.file.path, obtainiumId);
if (file.appId == obtainiumId) {
// Obtainium prompt should be lowest
await Future.delayed(const Duration(milliseconds: 500));
int? code =
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
if (code != null && code != 0 && code != 3) {
throw InstallError(code);
} else if (code == 0) {
apps[file.appId]!.app.installedVersion =
apps[file.appId]!.app.latestVersion;
file.file.delete();
}
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],
attemptToCorrectInstallStatus: false);
await saveApps([apps[file.appId]!.app]);
}
void uninstallApp(String appId) async {
@ -395,76 +395,44 @@ class AppsProvider with ChangeNotifier {
a.installedVersion = a.latestVersion;
return a;
}).toList());
// Download APKs for all Apps to be installed
// Prepare to download+install Apps
MultiAppMultiError errors = MultiAppMultiError();
List<DownloadedApk?> downloadedFiles =
await Future.wait(appsToInstall.map((id) async {
List<String> installedIds = [];
// Move Obtainium to the end of the line (let all other apps update first)
String? temp;
appsToInstall.removeWhere((element) {
bool res = element == obtainiumId || element == obtainiumTempId;
if (res) {
temp = element;
}
return res;
});
if (temp != null) {
appsToInstall = [...appsToInstall, temp!];
}
for (var id in appsToInstall) {
try {
return await downloadApp(apps[id]!.app, context);
// ignore: use_build_context_synchronously
var downloadedFile = await downloadApp(apps[id]!.app, context);
bool willBeSilent =
await canInstallSilently(apps[downloadedFile.appId]!.app);
willBeSilent = false; // TODO: Remove this when silent updates work
if (!(await settingsProvider?.getInstallPermission(enforce: false) ??
true)) {
throw ObtainiumError(tr('cancelled'));
}
if (!willBeSilent && context != null) {
// ignore: use_build_context_synchronously
await waitForUserToReturnToForeground(context);
}
await installApk(downloadedFile, silent: willBeSilent);
installedIds.add(id);
} catch (e) {
errors.add(id, e.toString());
}
return null;
}));
downloadedFiles =
downloadedFiles.where((element) => element != null).toList();
// 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) {
silentUpdates.add(f);
} else {
regularInstalls.add(f);
}
}
// Move everything to the regular install list (since silent updates don't currently work)
// TODO: Remove this when silent updates work
regularInstalls.addAll(silentUpdates);
// If Obtainium is being installed, it should be the last one
List<DownloadedApk> moveObtainiumToStart(List<DownloadedApk> items) {
DownloadedApk? temp;
items.removeWhere((element) {
bool res =
element.appId == obtainiumId || element.appId == obtainiumTempId;
if (res) {
temp = element;
}
return res;
});
if (temp != null) {
items = [temp!, ...items];
}
return items;
}
silentUpdates = moveObtainiumToStart(silentUpdates);
regularInstalls = moveObtainiumToStart(regularInstalls);
if (!(await settingsProvider?.getInstallPermission(enforce: false) ??
true)) {
throw ObtainiumError(tr('cancelled'));
}
// // Install silent updates (uncomment when it works - TODO)
// for (var u in silentUpdates) {
// await installApk(u, silent: true); // Would need to add silent option
// }
// 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) {
errors.add(i.appId, e.toString());
}
}
}
if (errors.content.isNotEmpty) {
@ -473,7 +441,7 @@ class AppsProvider with ChangeNotifier {
NotificationsProvider().cancel(UpdateNotification([]).id);
return downloadedFiles.map((e) => e!.appId).toList();
return installedIds;
}
Future<Directory> getAppsDir() async {
@ -762,7 +730,7 @@ class AppsProvider with ChangeNotifier {
apps[i].installedVersion = null;
}
}
await saveApps(apps, attemptToCorrectInstallStatus: false);
await saveApps(apps, attemptToCorrectInstallStatus: !remove);
}
if (remove) {
await removeApps(apps.map((e) => e.id).toList());

View File

@ -206,8 +206,7 @@ class SettingsProvider with ChangeNotifier {
.map((e) => e as App)
.toList();
if (changedApps.isNotEmpty) {
appsProvider.saveApps(changedApps,
attemptToCorrectInstallStatus: false);
appsProvider.saveApps(changedApps);
}
}
prefs?.setString('categories', jsonEncode(cats));

View File

@ -17,6 +17,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.9"
android_package_installer:
dependency: "direct main"
description:
path: "."
ref: main
resolved-ref: f09c79eee5be3c60b04760143eb954a13fdd07f1
url: "https://github.com/ImranR98/android_package_installer"
source: git
version: "0.0.1"
animations:
dependency: "direct main"
description:
@ -293,14 +302,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
install_plugin_v2:
dependency: "direct main"
description:
name: install_plugin_v2
sha256: d6b014637e7a53839e9c5a254f9fd9bb8866392c6db1f16184ce17818cc2d979
url: "https://pub.dev"
source: hosted
version: "1.0.0"
installed_apps:
dependency: "direct main"
description:

View File

@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.12.0+160 # When changing this, update the tag in main() accordingly
version: 0.12.1+161 # When changing this, update the tag in main() accordingly
environment:
sdk: '>=2.18.2 <3.0.0'
@ -51,7 +51,10 @@ dependencies:
device_info_plus: ^8.0.0
file_picker: ^5.2.10
animations: ^2.0.4
install_plugin_v2: ^1.0.0
android_package_installer:
git:
url: https://github.com/ImranR98/android_package_installer
ref: main
share_plus: ^6.0.1
installed_apps: ^1.3.1
package_archive_info: ^0.1.0