mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-08-07 15:50:16 +02:00
Compare commits
4 Commits
v0.12.0-be
...
v0.12.1-be
Author | SHA1 | Date | |
---|---|---|---|
|
d063bca474 | ||
|
7c592756fe | ||
|
08586870fb | ||
|
8b123acdcd |
@@ -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.
|
||||||
|
|
||||||
|
@@ -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"/>
|
||||||
|
@@ -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>
|
@@ -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'));
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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());
|
||||||
|
@@ -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));
|
||||||
|
17
pubspec.lock
17
pubspec.lock
@@ -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:
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user