Enable version correction in background (replace plugin)

This commit is contained in:
Imran Remtulla
2023-08-20 16:03:41 -04:00
parent ce89d456e1
commit 7c41692d5f
6 changed files with 78 additions and 121 deletions

View File

@@ -71,4 +71,5 @@
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"/> android:maxSdkVersion="29"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
</manifest> </manifest>

View File

@@ -153,10 +153,10 @@ class _AppPageState extends State<AppPage> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SizedBox(height: 125), const SizedBox(height: 125),
app?.installedInfo != null app?.icon != null
? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ ? Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Image.memory( Image.memory(
app!.installedInfo!.icon!, app!.icon!,
height: 150, height: 150,
gaplessPlayback: true, gaplessPlayback: true,
) )

View File

@@ -393,9 +393,9 @@ class AppsPageState extends State<AppsPage> {
} }
getAppIcon(int appIndex) { getAppIcon(int appIndex) {
return listedApps[appIndex].installedInfo != null return listedApps[appIndex].icon != null
? Image.memory( ? Image.memory(
listedApps[appIndex].installedInfo!.icon!, listedApps[appIndex].icon!,
gaplessPlayback: true, gaplessPlayback: true,
) )
: Row( : Row(

View File

@@ -12,15 +12,12 @@ 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:installed_apps/app_info.dart';
import 'package:installed_apps/installed_apps.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/components/generated_form_modal.dart'; import 'package:obtainium/components/generated_form_modal.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/logs_provider.dart'; import 'package:obtainium/providers/logs_provider.dart';
import 'package:obtainium/providers/notifications_provider.dart'; import 'package:obtainium/providers/notifications_provider.dart';
import 'package:obtainium/providers/settings_provider.dart'; import 'package:obtainium/providers/settings_provider.dart';
import 'package:package_archive_info/package_archive_info.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -35,13 +32,14 @@ final pm = AndroidPackageManager();
class AppInMemory { class AppInMemory {
late App app; late App app;
double? downloadProgress; double? downloadProgress;
AppInfo? installedInfo; PackageInfo? installedInfo;
Uint8List? icon;
AppInMemory(this.app, this.downloadProgress, this.installedInfo); AppInMemory(this.app, this.downloadProgress, this.installedInfo, this.icon);
AppInMemory deepCopy() => AppInMemory deepCopy() =>
AppInMemory(app.deepCopy(), downloadProgress, installedInfo); AppInMemory(app.deepCopy(), downloadProgress, installedInfo, icon);
String get name => app.overrideName ?? installedInfo?.name ?? app.finalName; String get name => app.overrideName ?? app.finalName;
} }
class DownloadedApk { class DownloadedApk {
@@ -218,19 +216,19 @@ class AppsProvider with ChangeNotifier {
return downloadedFile; return downloadedFile;
} }
Future<File> handleAPKIDChange(App app, PackageArchiveInfo newInfo, Future<File> handleAPKIDChange(App app, PackageInfo newInfo,
File downloadedFile, String downloadUrl) async { File downloadedFile, String downloadUrl) async {
// 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
if (app.id != newInfo.packageName) { if (app.id != newInfo.packageName) {
var isTempId = SourceProvider().isTempId(app); var isTempId = SourceProvider().isTempId(app);
if (apps[app.id] != null && !isTempId && !app.allowIdChange) { if (apps[app.id] != null && !isTempId && !app.allowIdChange) {
throw IDChangedError(newInfo.packageName); throw IDChangedError(newInfo.packageName!);
} }
var idChangeWasAllowed = app.allowIdChange; var idChangeWasAllowed = app.allowIdChange;
app.allowIdChange = false; app.allowIdChange = false;
var originalAppId = app.id; var originalAppId = app.id;
app.id = newInfo.packageName; app.id = newInfo.packageName!;
downloadedFile = downloadedFile.renameSync( downloadedFile = downloadedFile.renameSync(
'${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.${downloadedFile.path.split('.').last}'); '${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.${downloadedFile.path.split('.').last}');
if (apps[originalAppId] != null) { if (apps[originalAppId] != null) {
@@ -279,11 +277,12 @@ class AppsProvider with ChangeNotifier {
notif = DownloadNotification(app.finalName, -1); notif = DownloadNotification(app.finalName, -1);
notificationsProvider?.notify(notif); notificationsProvider?.notify(notif);
} }
PackageArchiveInfo? newInfo; PackageInfo? newInfo;
var isAPK = downloadedFile.path.toLowerCase().endsWith('.apk'); var isAPK = downloadedFile.path.toLowerCase().endsWith('.apk');
Directory? xapkDir; Directory? xapkDir;
if (isAPK) { if (isAPK) {
newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path); newInfo = await pm.getPackageArchiveInfo(
archiveFilePath: downloadedFile.path);
} else { } else {
// Assume XAPK // Assume XAPK
String xapkDirPath = '${downloadedFile.path}-dir'; String xapkDirPath = '${downloadedFile.path}-dir';
@@ -293,10 +292,11 @@ class AppsProvider with ChangeNotifier {
.listSync() .listSync()
.where((e) => e.path.toLowerCase().endsWith('.apk')) .where((e) => e.path.toLowerCase().endsWith('.apk'))
.toList(); .toList();
newInfo = await PackageArchiveInfo.fromPath(apks.first.path); newInfo =
await pm.getPackageArchiveInfo(archiveFilePath: apks.first.path);
} }
downloadedFile = downloadedFile =
await handleAPKIDChange(app, newInfo, downloadedFile, downloadUrl); await handleAPKIDChange(app, newInfo!, downloadedFile, downloadUrl);
// Delete older versions of the file if any // Delete older versions of the file if any
for (var file in downloadedFile.parent.listSync()) { for (var file in downloadedFile.parent.listSync()) {
var fn = file.path.split('/').last; var fn = file.path.split('/').last;
@@ -344,14 +344,8 @@ class AppsProvider with ChangeNotifier {
// If we did not install the app (or it isn't installed), silent install is not possible // If we did not install the app (or it isn't installed), silent install is not possible
return false; return false;
} }
int? targetSDK; int? targetSDK =
try { (await getInstalledInfo(app.id))?.applicationInfo?.targetSdkVersion;
targetSDK = (await pm.getPackageInfo(packageName: app.id))
?.applicationInfo
?.targetSdkVersion;
} catch (e) {
// Weird if you get here - ignore
}
// The OS must also be new enough and the APK should target a new enough API // The OS must also be new enough and the APK should target a new enough API
return osInfo.version.sdkInt >= 30 && return osInfo.version.sdkInt >= 30 &&
@@ -371,14 +365,8 @@ class AppsProvider with ChangeNotifier {
} }
} }
Future<bool> canDowngradeApps() async { Future<bool> canDowngradeApps() async =>
try { (await getInstalledInfo('com.berdik.letmedowngrade')) != null;
await InstalledApps.getAppInfo('com.berdik.letmedowngrade');
return true;
} catch (e) {
return false;
}
}
Future<void> unzipFile(String filePath, String destinationPath) async { Future<void> unzipFile(String filePath, String destinationPath) async {
await ZipFile.extractToDirectory( await ZipFile.extractToDirectory(
@@ -419,15 +407,11 @@ class AppsProvider with ChangeNotifier {
} }
Future<bool> installApk(DownloadedApk file) async { Future<bool> installApk(DownloadedApk file) async {
var newInfo = await PackageArchiveInfo.fromPath(file.file.path); var newInfo =
AppInfo? appInfo; await pm.getPackageArchiveInfo(archiveFilePath: file.file.path);
try { PackageInfo? appInfo = await getInstalledInfo(apps[file.appId]!.app.id);
appInfo = await InstalledApps.getAppInfo(apps[file.appId]!.app.id);
} catch (e) {
// OK
}
if (appInfo != null && if (appInfo != null &&
int.parse(newInfo.buildNumber) < appInfo.versionCode! && newInfo!.versionCode! < appInfo.versionCode! &&
!(await canDowngradeApps())) { !(await canDowngradeApps())) {
throw DowngradeError(); throw DowngradeError();
} }
@@ -638,27 +622,17 @@ class AppsProvider with ChangeNotifier {
return appsDir; return appsDir;
} }
Future<AppInfo?> getInstalledInfo(String? packageName) async { Future<PackageInfo?> getInstalledInfo(String? packageName) async {
if (packageName != null) { if (packageName != null) {
try { try {
return await InstalledApps.getAppInfo(packageName); return await pm.getPackageInfo(packageName: packageName);
} catch (e) { } catch (e) {
// OK print(e); // OK
} }
} }
return null; return null;
} }
Future<bool> doesInstalledAppsPluginWork() async {
bool res = false;
try {
res = (await InstalledApps.getAppInfo(obtainiumId)).versionName != null;
} catch (e) {
//
}
return res;
}
bool isVersionDetectionPossible(AppInMemory? app) { bool isVersionDetectionPossible(AppInMemory? app) {
return app?.app.additionalSettings['trackOnly'] != true && return app?.app.additionalSettings['trackOnly'] != true &&
app?.app.additionalSettings['versionDetection'] != app?.app.additionalSettings['versionDetection'] !=
@@ -672,7 +646,8 @@ class AppsProvider with ChangeNotifier {
// Given an App and it's on-device info... // Given an App and it's on-device info...
// Reconcile unexpected differences between its reported installed version, real installed version, and reported latest version // Reconcile unexpected differences between its reported installed version, real installed version, and reported latest version
App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) { App? getCorrectedInstallStatusAppIfPossible(
App app, PackageInfo? installedInfo) {
var modded = false; var modded = false;
var trackOnly = app.additionalSettings['trackOnly'] == true; var trackOnly = app.additionalSettings['trackOnly'] == true;
var noVersionDetection = app.additionalSettings['versionDetection'] != var noVersionDetection = app.additionalSettings['versionDetection'] !=
@@ -718,7 +693,8 @@ class AppsProvider with ChangeNotifier {
if (installedInfo != null && if (installedInfo != null &&
app.additionalSettings['versionDetection'] == app.additionalSettings['versionDetection'] ==
'standardVersionDetection' && 'standardVersionDetection' &&
!isVersionDetectionPossible(AppInMemory(app, null, installedInfo))) { !isVersionDetectionPossible(
AppInMemory(app, null, installedInfo, null))) {
app.additionalSettings['versionDetection'] = 'noVersionDetection'; app.additionalSettings['versionDetection'] = 'noVersionDetection';
logs.add('Could not reconcile version formats for: ${app.id}'); logs.add('Could not reconcile version formats for: ${app.id}');
modded = true; modded = true;
@@ -802,9 +778,9 @@ class AppsProvider with ChangeNotifier {
sp.getSource(app.url, overrideSource: app.overrideSource); sp.getSource(app.url, overrideSource: app.overrideSource);
apps.update( apps.update(
app.id, app.id,
(value) => (value) => AppInMemory(
AppInMemory(app, value.downloadProgress, value.installedInfo), app, value.downloadProgress, value.installedInfo, value.icon),
ifAbsent: () => AppInMemory(app, null, null)); ifAbsent: () => AppInMemory(app, null, null, null));
} catch (e) { } catch (e) {
errors.add([app.id, app.finalName, e.toString()]); errors.add([app.id, app.finalName, e.toString()]);
} }
@@ -817,34 +793,39 @@ class AppsProvider with ChangeNotifier {
AppsRemovedNotification(errors.map((e) => [e[1], e[2]]).toList())); AppsRemovedNotification(errors.map((e) => [e[1], e[2]]).toList()));
} }
if (await doesInstalledAppsPluginWork()) { for (var app in apps.values) {
for (var app in apps.values) { // Get install status and other OS info for each App (slow)
// Check install status for each App (slow) apps[app.app.id]?.installedInfo = await getInstalledInfo(app.app.id);
apps[app.app.id]?.installedInfo = await getInstalledInfo(app.app.id); apps[app.app.id]?.icon =
notifyListeners(); await apps[app.app.id]?.installedInfo?.applicationInfo?.getAppIcon();
apps[app.app.id]?.app.name = await (apps[app.app.id]
?.installedInfo
?.applicationInfo
?.getAppLabel()) ??
app.name;
notifyListeners();
}
// Reconcile version differences
List<App> modifiedApps = [];
for (var app in apps.values) {
var moddedApp =
getCorrectedInstallStatusAppIfPossible(app.app, app.installedInfo);
if (moddedApp != null) {
modifiedApps.add(moddedApp);
} }
// Reconcile version differences }
List<App> modifiedApps = []; if (modifiedApps.isNotEmpty) {
for (var app in apps.values) { await saveApps(modifiedApps, attemptToCorrectInstallStatus: false);
var moddedApp = var removedAppIds = modifiedApps
getCorrectedInstallStatusAppIfPossible(app.app, app.installedInfo); .where((a) => a.installedVersion == null)
if (moddedApp != null) { .map((e) => e.id)
modifiedApps.add(moddedApp); .toList();
} // After reconciliation, delete externally uninstalled Apps if needed
} if (removedAppIds.isNotEmpty) {
if (modifiedApps.isNotEmpty) { var settingsProvider = SettingsProvider();
await saveApps(modifiedApps, attemptToCorrectInstallStatus: false); await settingsProvider.initializeSettings();
var removedAppIds = modifiedApps if (settingsProvider.removeOnExternalUninstall) {
.where((a) => a.installedVersion == null) await removeApps(removedAppIds);
.map((e) => e.id)
.toList();
// After reconciliation, delete externally uninstalled Apps if needed
if (removedAppIds.isNotEmpty) {
var settingsProvider = SettingsProvider();
await settingsProvider.initializeSettings();
if (settingsProvider.removeOnExternalUninstall) {
await removeApps(removedAppIds);
}
} }
} }
} }
@@ -856,12 +837,12 @@ class AppsProvider with ChangeNotifier {
Future<void> saveApps(List<App> apps, Future<void> saveApps(List<App> apps,
{bool attemptToCorrectInstallStatus = true, {bool attemptToCorrectInstallStatus = true,
bool onlyIfExists = true}) async { bool onlyIfExists = true}) async {
attemptToCorrectInstallStatus = attemptToCorrectInstallStatus = attemptToCorrectInstallStatus;
attemptToCorrectInstallStatus && (await doesInstalledAppsPluginWork());
for (var a in apps) { for (var a in apps) {
var app = a.deepCopy(); var app = a.deepCopy();
AppInfo? info = await getInstalledInfo(app.id); PackageInfo? info = await getInstalledInfo(app.id);
app.name = info?.name ?? app.name; var icon = await info?.applicationInfo?.getAppIcon();
app.name = await (info?.applicationInfo?.getAppLabel()) ?? app.name;
if (attemptToCorrectInstallStatus) { if (attemptToCorrectInstallStatus) {
app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app; app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app;
} }
@@ -870,9 +851,10 @@ class AppsProvider with ChangeNotifier {
.writeAsStringSync(jsonEncode(app.toJson())); .writeAsStringSync(jsonEncode(app.toJson()));
} }
try { try {
this.apps.update( this.apps.update(app.id,
app.id, (value) => AppInMemory(app, value.downloadProgress, info), (value) => AppInMemory(app, value.downloadProgress, info, icon),
ifAbsent: onlyIfExists ? null : () => AppInMemory(app, null, info)); ifAbsent:
onlyIfExists ? null : () => AppInMemory(app, null, info, icon));
} catch (e) { } catch (e) {
if (e is! ArgumentError || e.name != 'key') { if (e is! ArgumentError || e.name != 'key') {
rethrow; rethrow;

View File

@@ -31,7 +31,7 @@ packages:
description: description:
path: "." path: "."
ref: master ref: master
resolved-ref: "6e68991ef9c6232695abce2eef345d3cca2f52ac" resolved-ref: c7c2f992a9dc452393c94d96cdf2b1f5a5ce7c80
url: "https://github.com/ImranR98/android_package_manager" url: "https://github.com/ImranR98/android_package_manager"
source: git source: git
version: "0.5.4" version: "0.5.4"
@@ -375,14 +375,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.17" version: "4.0.17"
installed_apps:
dependency: "direct main"
description:
name: installed_apps
sha256: "145af8eb6e4e7c830e9888d6de0573ae5c139e8e0742a3e67316e1db21ab6fe0"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
intl: intl:
dependency: transitive dependency: transitive
description: description:
@@ -463,22 +455,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
package_archive_info:
dependency: "direct main"
description:
name: package_archive_info
sha256: "8f671a29b79d15f192e5e2b0dab9d0bad66b9ee93fb58d4e0afdb62f91a259be"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
package_info:
dependency: transitive
description:
name: package_info
sha256: "6c07d9d82c69e16afeeeeb6866fe43985a20b3b50df243091bfc4a4ad2b03b75"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
path: path:
dependency: transitive dependency: transitive
description: description:

View File

@@ -60,8 +60,6 @@ dependencies:
url: https://github.com/ImranR98/android_package_manager url: https://github.com/ImranR98/android_package_manager
ref: master ref: master
share_plus: ^7.0.0 share_plus: ^7.0.0
installed_apps: ^1.3.1 # TODO: Remove when android_package_manager supports getting icon as UInt8List and versionCode
package_archive_info: ^0.1.0 # TODO: Remove when android_package_manager supports getting versionCode
android_alarm_manager_plus: ^3.0.0 android_alarm_manager_plus: ^3.0.0
sqflite: ^2.2.0+3 sqflite: ^2.2.0+3
easy_localization: ^3.0.1 easy_localization: ^3.0.1