Compare commits

...

4 Commits

Author SHA1 Message Date
Imran Remtulla
d063bca474 Merge pull request #506 from ImranR98/dev
Switched to synchronous install plugin (#99, #459)
2023-04-30 02:59:49 -04:00
Imran Remtulla
7c592756fe Smarter APK caching (#459) 2023-04-30 02:47:53 -04:00
Imran Remtulla
08586870fb Tweak use of attemptToCorrectInstallStatus 2023-04-30 02:28:14 -04:00
Imran Remtulla
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) height="80">](https://apt.izzysoft.de/fdroid/index/apk/dev.imranr.obtainium)
## Limitations ## 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. - 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. - 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" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter>
<action
android:name="com.android_package_installer.content.SESSION_API_PACKAGE_INSTALLED"
android:exported="false"/>
</intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
@@ -46,9 +51,18 @@
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </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> </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" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>

View File

@@ -2,4 +2,5 @@
<paths> <paths>
<external-path path="Android/data/dev.imranr.obtainium/" name="files_root" /> <external-path path="Android/data/dev.imranr.obtainium/" name="files_root" />
<external-path path="." name="external_storage_root" /> <external-path path="." name="external_storage_root" />
<external-path name="external_files" path="."/>
</paths> </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:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:obtainium/providers/logs_provider.dart'; import 'package:obtainium/providers/logs_provider.dart';
@@ -44,6 +45,11 @@ class DowngradeError extends ObtainiumError {
DowngradeError() : super(tr('cantInstallOlderVersion')); DowngradeError() : super(tr('cantInstallOlderVersion'));
} }
class InstallError extends ObtainiumError {
InstallError(int code)
: super(PackageInstallerStatus.byCode(code).name.substring(7));
}
class IDChangedError extends ObtainiumError { class IDChangedError extends ObtainiumError {
IDChangedError() : super(tr('appIdMismatch')); IDChangedError() : super(tr('appIdMismatch'));
} }

View File

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

View File

@@ -6,11 +6,11 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:android_intent_plus/flag.dart'; 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:device_info_plus/device_info_plus.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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/app_info.dart';
import 'package:installed_apps/installed_apps.dart'; import 'package:installed_apps/installed_apps.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
@@ -113,21 +113,20 @@ class AppsProvider with ChangeNotifier {
() async { () async {
// Load Apps into memory (in background, this is done later instead of in the constructor) // Load Apps into memory (in background, this is done later instead of in the constructor)
await loadApps(); await loadApps();
// Delete existing APKs // Delete any partial APKs
(await getExternalStorageDirectory()) (await getExternalCacheDirectories())
?.listSync() ?.first
.where((element) => .listSync()
element.path.endsWith('.apk') || .where((element) => element.path.endsWith('.apk.part'))
element.path.endsWith('.apk.part')) .forEach((partialApk) {
.forEach((apk) { partialApk.delete();
apk.delete();
}); });
}(); }();
} }
downloadFile(String url, String fileName, Function? onProgress, downloadFile(String url, String fileName, Function? onProgress,
{bool useExisting = true}) async { {bool useExisting = true}) async {
var destDir = (await getExternalStorageDirectory())!.path; var destDir = (await getExternalCacheDirectories())!.first.path;
StreamedResponse response = StreamedResponse response =
await Client().send(Request('GET', Uri.parse(url))); await Client().send(Request('GET', Uri.parse(url)));
File downloadedFile = File('$destDir/$fileName'); File downloadedFile = File('$destDir/$fileName');
@@ -191,15 +190,6 @@ class AppsProvider with ChangeNotifier {
} }
prevProg = prog; 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 // 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 // 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); var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
@@ -217,6 +207,15 @@ class AppsProvider with ChangeNotifier {
await saveApps([app], onlyIfExists: !isTempId); 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); return DownloadedApk(app.id, downloadedFile);
} finally { } finally {
notificationsProvider?.cancel(notifId); 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 // 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 // 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 // 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); var newInfo = await PackageArchiveInfo.fromPath(file.file.path);
AppInfo? appInfo; AppInfo? appInfo;
try { try {
@@ -281,16 +281,16 @@ class AppsProvider with ChangeNotifier {
!(await canDowngradeApps())) { !(await canDowngradeApps())) {
throw DowngradeError(); throw DowngradeError();
} }
await InstallPlugin.installApk(file.file.path, obtainiumId); int? code =
if (file.appId == obtainiumId) { await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
// Obtainium prompt should be lowest if (code != null && code != 0 && code != 3) {
await Future.delayed(const Duration(milliseconds: 500)); throw InstallError(code);
} } else if (code == 0) {
apps[file.appId]!.app.installedVersion = apps[file.appId]!.app.installedVersion =
apps[file.appId]!.app.latestVersion; apps[file.appId]!.app.latestVersion;
// Don't correct install status as installation may not be done yet file.file.delete();
await saveApps([apps[file.appId]!.app], }
attemptToCorrectInstallStatus: false); await saveApps([apps[file.appId]!.app]);
} }
void uninstallApp(String appId) async { void uninstallApp(String appId) async {
@@ -395,75 +395,43 @@ class AppsProvider with ChangeNotifier {
a.installedVersion = a.latestVersion; a.installedVersion = a.latestVersion;
return a; return a;
}).toList()); }).toList());
// Download APKs for all Apps to be installed
// Prepare to download+install Apps
MultiAppMultiError errors = MultiAppMultiError(); MultiAppMultiError errors = MultiAppMultiError();
List<DownloadedApk?> downloadedFiles = List<String> installedIds = [];
await Future.wait(appsToInstall.map((id) async {
try {
return await downloadApp(apps[id]!.app, context);
} 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) // Move Obtainium to the end of the line (let all other apps update first)
// TODO: Remove this when silent updates work String? temp;
regularInstalls.addAll(silentUpdates); appsToInstall.removeWhere((element) {
bool res = element == obtainiumId || element == obtainiumTempId;
// 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) { if (res) {
temp = element; temp = element;
} }
return res; return res;
}); });
if (temp != null) { if (temp != null) {
items = [temp!, ...items]; appsToInstall = [...appsToInstall, temp!];
}
return items;
} }
silentUpdates = moveObtainiumToStart(silentUpdates); for (var id in appsToInstall) {
regularInstalls = moveObtainiumToStart(regularInstalls); try {
// 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) ?? if (!(await settingsProvider?.getInstallPermission(enforce: false) ??
true)) { true)) {
throw ObtainiumError(tr('cancelled')); throw ObtainiumError(tr('cancelled'));
} }
if (!willBeSilent && context != null) {
// // 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 // ignore: use_build_context_synchronously
await waitForUserToReturnToForeground(context); await waitForUserToReturnToForeground(context);
for (var i in regularInstalls) {
try {
await installApk(i);
} catch (e) {
errors.add(i.appId, e.toString());
} }
await installApk(downloadedFile, silent: willBeSilent);
installedIds.add(id);
} catch (e) {
errors.add(id, e.toString());
} }
} }
@@ -473,7 +441,7 @@ class AppsProvider with ChangeNotifier {
NotificationsProvider().cancel(UpdateNotification([]).id); NotificationsProvider().cancel(UpdateNotification([]).id);
return downloadedFiles.map((e) => e!.appId).toList(); return installedIds;
} }
Future<Directory> getAppsDir() async { Future<Directory> getAppsDir() async {
@@ -762,7 +730,7 @@ class AppsProvider with ChangeNotifier {
apps[i].installedVersion = null; apps[i].installedVersion = null;
} }
} }
await saveApps(apps, attemptToCorrectInstallStatus: false); await saveApps(apps, attemptToCorrectInstallStatus: !remove);
} }
if (remove) { if (remove) {
await removeApps(apps.map((e) => e.id).toList()); await removeApps(apps.map((e) => e.id).toList());

View File

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

View File

@@ -17,6 +17,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.9" 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: animations:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -293,14 +302,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" 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: installed_apps:
dependency: "direct main" dependency: "direct main"
description: 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 # 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 # 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. # 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: environment:
sdk: '>=2.18.2 <3.0.0' sdk: '>=2.18.2 <3.0.0'
@@ -51,7 +51,10 @@ dependencies:
device_info_plus: ^8.0.0 device_info_plus: ^8.0.0
file_picker: ^5.2.10 file_picker: ^5.2.10
animations: ^2.0.4 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 share_plus: ^6.0.1
installed_apps: ^1.3.1 installed_apps: ^1.3.1
package_archive_info: ^0.1.0 package_archive_info: ^0.1.0