diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 7fade03b..782ef780 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -32,17 +32,17 @@
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme" />
-
-
+
+
-
-
+
+
@@ -55,5 +55,22 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt
index 10ff238e..6fe5438e 100644
--- a/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt
+++ b/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt
@@ -1,11 +1,16 @@
package app.revanced.manager.flutter
+import android.app.PendingIntent
import android.app.SearchManager
import android.content.Intent
+import android.content.pm.PackageInstaller
+import android.os.Build
import android.os.Handler
import android.os.Looper
import app.revanced.manager.flutter.utils.Aapt
import app.revanced.manager.flutter.utils.aligning.ZipAligner
+import app.revanced.manager.flutter.utils.packageInstaller.InstallerReceiver
+import app.revanced.manager.flutter.utils.packageInstaller.UninstallerReceiver
import app.revanced.manager.flutter.utils.signing.Signer
import app.revanced.manager.flutter.utils.zip.ZipFile
import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
@@ -184,12 +189,24 @@ class MainActivity : FlutterActivity() {
}.toString().let(result::success)
}
+ "installApk" -> {
+ val apkPath = call.argument("apkPath")!!
+ PackageInstallerManager.result = result
+ installApk(apkPath)
+ }
+
+ "uninstallApp" -> {
+ val packageName = call.argument("packageName")!!
+ uninstallApp(packageName)
+ PackageInstallerManager.result = result
+ }
+
else -> result.notImplemented()
}
}
}
- fun openBrowser(query: String?) {
+ private fun openBrowser(query: String?) {
val intent = Intent(Intent.ACTION_WEB_SEARCH).apply {
putExtra(SearchManager.QUERY, query)
}
@@ -407,4 +424,44 @@ class MainActivity : FlutterActivity() {
handler.post { result.success(null) }
}.start()
}
+
+ private fun installApk(apkPath: String) {
+ val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller
+ val sessionParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
+ val sessionId: Int = packageInstaller.createSession(sessionParams)
+ val session: PackageInstaller.Session = packageInstaller.openSession(sessionId)
+ session.use { activeSession ->
+ val sessionOutputStream = activeSession.openWrite(applicationContext.packageName, 0, -1)
+ sessionOutputStream.use { outputStream ->
+ val apkFile = File(apkPath)
+ apkFile.inputStream().use { inputStream ->
+ inputStream.copyTo(outputStream)
+ }
+ }
+ }
+ val receiverIntent = Intent(applicationContext, InstallerReceiver::class.java).apply {
+ action = "APP_INSTALL_ACTION"
+ }
+ val receiverPendingIntent = PendingIntent.getBroadcast(context, sessionId, receiverIntent, PackageInstallerManager.flags)
+ session.commit(receiverPendingIntent.intentSender)
+ session.close()
+ }
+
+ private fun uninstallApp(packageName: String) {
+ val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller
+ val receiverIntent = Intent(applicationContext, UninstallerReceiver::class.java).apply {
+ action = "APP_UNINSTALL_ACTION"
+ }
+ val receiverPendingIntent = PendingIntent.getBroadcast(context, 0, receiverIntent, PackageInstallerManager.flags)
+ packageInstaller.uninstall(packageName, receiverPendingIntent.intentSender)
+ }
+
+ object PackageInstallerManager {
+ var result: MethodChannel.Result? = null
+ val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+ }
}
diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/packageInstaller/InstallerReceiver.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/packageInstaller/InstallerReceiver.kt
new file mode 100644
index 00000000..d14a9daa
--- /dev/null
+++ b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/packageInstaller/InstallerReceiver.kt
@@ -0,0 +1,32 @@
+package app.revanced.manager.flutter.utils.packageInstaller
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageInstaller
+import app.revanced.manager.flutter.MainActivity
+
+class InstallerReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
+ PackageInstaller.STATUS_PENDING_USER_ACTION -> {
+ val confirmationIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT)
+ if (confirmationIntent != null) {
+ context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
+ }
+ }
+
+ else -> {
+ val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
+ val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
+ val otherPackageName = intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME)
+ MainActivity.PackageInstallerManager.result!!.success(mapOf(
+ "status" to status,
+ "packageName" to packageName,
+ "message" to message,
+ "otherPackageName" to otherPackageName
+ ))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/packageInstaller/UninstallerReceiver.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/packageInstaller/UninstallerReceiver.kt
new file mode 100644
index 00000000..84dec3cc
--- /dev/null
+++ b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/packageInstaller/UninstallerReceiver.kt
@@ -0,0 +1,24 @@
+package app.revanced.manager.flutter.utils.packageInstaller
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageInstaller
+import app.revanced.manager.flutter.MainActivity
+
+class UninstallerReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
+ PackageInstaller.STATUS_PENDING_USER_ACTION -> {
+ val confirmationIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT)
+ if (confirmationIntent != null) {
+ context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
+ }
+ }
+
+ else -> {
+ MainActivity.PackageInstallerManager.result!!.success(status)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/assets/i18n/en_US.json b/assets/i18n/en_US.json
index 511cd9c4..dde2237a 100644
--- a/assets/i18n/en_US.json
+++ b/assets/i18n/en_US.json
@@ -170,6 +170,8 @@
"installRootType": "Mount",
"installNonRootType": "Normal",
+ "warning": "Disable auto updates after installing the app to avoid unexpected issues.",
+
"pressBackAgain": "Press back again to cancel",
"openButton": "Open",
"shareButton": "Share file",
@@ -327,5 +329,34 @@
"integrationsContributors": "Integrations contributors",
"cliContributors": "CLI contributors",
"managerContributors": "Manager contributors"
+ },
+ "installErrorDialog": {
+ "mount_version_mismatch": "Version mismatch",
+ "mount_no_root": "No root access",
+ "mount_missing_installation": "Installation not found",
+
+ "status_failure_blocked": "Installation blocked",
+ "install_failed_verification_failure": "Verification failed",
+ "status_failure_invalid": "Installation invalid",
+ "install_failed_version_downgrade": "Can't downgrade",
+ "status_failure_conflict": "Installation conflict",
+ "status_failure_storage": "Installation storage issue",
+ "status_failure_incompatible": "Installation incompatible",
+ "status_failure_timeout": "Installation timeout",
+ "status_unknown": "Installation failed",
+
+ "mount_version_mismatch_description": "The installation failed due to the installed app being a different version than the patched app.\n\nInstall the version of the app you are mounting and try again.",
+ "mount_no_root_description": "The installation failed due to root access not being granted.\n\nGrant root access to ReVanced Manager and try again.",
+ "mount_missing_installation_description": "The installation failed due to the unpatched app not being installed on this device.\n\nInstall the app and try again.",
+
+ "status_failure_timeout_description": "The installation took too long to finish.\n\nWould you like to try again?",
+ "status_failure_storage_description": "The installation failed due to insufficient storage.\n\nFree up some space and try again.",
+ "status_failure_invalid_description": "The installation failed due to the patched app being invalid.\n\nUninstall the app and try again?",
+ "status_failure_incompatible_description": "The app is incompatible with this device.\n\nContact the developer of the app and ask for support.",
+ "status_failure_conflict_description": "The installation was prevented by an existing installation of the app.\n\nUninstall the app and try again?",
+ "status_failure_blocked_description": "The installation was blocked by {packageName}.\n\nAdjust your security settings and try again.",
+ "install_failed_verification_failure_description": "The installation failed due to a verification issue.\n\nAdjust your security settings and try again.",
+ "install_failed_version_downgrade_description": "The installation failed due to the patched app being a lower version than the installed app.\n\nUninstall the app and try again?",
+ "status_unknown_description": "The installation failed due to an unknown reason. Please try again."
}
}
diff --git a/lib/main.dart b/lib/main.dart
index b38cb4c7..5b8df919 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -8,6 +8,7 @@ import 'package:revanced_manager/services/download_manager.dart';
import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/revanced_api.dart';
+import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -24,6 +25,13 @@ Future main() async {
final String repoUrl = locator().getRepoUrl();
locator().initialize(repoUrl);
tz.initializeTimeZones();
+
+ // TODO(aAbed): remove in the future, keep it for now during migration.
+ final rootAPI = RootAPI();
+ if (await rootAPI.hasRootPermissions()) {
+ await rootAPI.removeOrphanedFiles();
+ }
+
prefs = await SharedPreferences.getInstance();
runApp(const MyApp());
diff --git a/lib/services/download_manager.dart b/lib/services/download_manager.dart
index 4c0919b9..1aa9fc2b 100644
--- a/lib/services/download_manager.dart
+++ b/lib/services/download_manager.dart
@@ -72,4 +72,3 @@ class DownloadManager {
);
}
}
-
diff --git a/lib/services/patcher_api.dart b/lib/services/patcher_api.dart
index f79d02e5..a22c610b 100644
--- a/lib/services/patcher_api.dart
+++ b/lib/services/patcher_api.dart
@@ -3,22 +3,24 @@ import 'dart:io';
import 'package:collection/collection.dart';
import 'package:device_apps/device_apps.dart';
import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
+import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:injectable/injectable.dart';
-import 'package:install_plugin/install_plugin.dart';
import 'package:path_provider/path_provider.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/root_api.dart';
+import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:share_plus/share_plus.dart';
@lazySingleton
class PatcherAPI {
static const patcherChannel =
- MethodChannel('app.revanced.manager.flutter/patcher');
+ MethodChannel('app.revanced.manager.flutter/patcher');
final ManagerAPI _managerAPI = locator();
final RootAPI _rootAPI = RootAPI();
late Directory _dataDir;
@@ -79,7 +81,8 @@ class PatcherAPI {
}
Future> getFilteredInstalledApps(
- bool showUniversalPatches,) async {
+ bool showUniversalPatches,
+ ) async {
final List filteredApps = [];
final bool allAppsIncluded =
_universalPatches.isNotEmpty && showUniversalPatches;
@@ -121,11 +124,11 @@ class PatcherAPI {
final List patches = _patches
.where(
(patch) =>
- patch.compatiblePackages.isEmpty ||
- !patch.name.contains('settings') &&
- patch.compatiblePackages
- .any((pack) => pack.name == packageName),
- )
+ patch.compatiblePackages.isEmpty ||
+ !patch.name.contains('settings') &&
+ patch.compatiblePackages
+ .any((pack) => pack.name == packageName),
+ )
.toList();
if (!_managerAPI.areUniversalPatchesEnabled()) {
filteredPatches[packageName] = patches
@@ -137,22 +140,27 @@ class PatcherAPI {
return filteredPatches[packageName];
}
- Future> getAppliedPatches(List appliedPatches,) async {
+ Future> getAppliedPatches(
+ List appliedPatches,
+ ) async {
return _patches
.where((patch) => appliedPatches.contains(patch.name))
.toList();
}
- Future runPatcher(String packageName,
- String apkFilePath,
- List selectedPatches,) async {
+ Future runPatcher(
+ String packageName,
+ String apkFilePath,
+ List selectedPatches,
+ ) async {
final File? integrationsFile = await _managerAPI.downloadIntegrations();
final Map> options = {};
for (final patch in selectedPatches) {
if (patch.options.isNotEmpty) {
final Map patchOptions = {};
for (final option in patch.options) {
- final patchOption = _managerAPI.getPatchOption(packageName, patch.name, option.key);
+ final patchOption =
+ _managerAPI.getPatchOption(packageName, patch.name, option.key);
if (patchOption != null) {
patchOptions[patchOption.key] = patchOption.value;
}
@@ -194,133 +202,308 @@ class PatcherAPI {
}
}
}
-}
+ }
-Future stopPatcher() async {
- try {
- await patcherChannel.invokeMethod('stopPatcher');
- } on Exception catch (e) {
- if (kDebugMode) {
- print(e);
+ Future stopPatcher() async {
+ try {
+ await patcherChannel.invokeMethod('stopPatcher');
+ } on Exception catch (e) {
+ if (kDebugMode) {
+ print(e);
+ }
}
}
-}
-Future installPatchedFile(PatchedApplication patchedApp) async {
- if (outFile != null) {
- try {
- if (patchedApp.isRooted) {
- final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
- if (hasRootPermissions) {
- return _rootAPI.installApp(
- patchedApp.packageName,
- patchedApp.apkFilePath,
- outFile!.path,
- );
+ Future installPatchedFile(
+ BuildContext context,
+ PatchedApplication patchedApp,
+ ) async {
+ if (outFile != null) {
+ _managerAPI.ctx = context;
+ try {
+ if (patchedApp.isRooted) {
+ final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
+ final packageVersion = await DeviceApps.getApp(patchedApp.packageName)
+ .then((app) => app?.versionName);
+ if (!hasRootPermissions) {
+ installErrorDialog(1);
+ } else if (packageVersion == null) {
+ installErrorDialog(1.2);
+ } else if (packageVersion == patchedApp.version) {
+ return await _rootAPI.installApp(
+ patchedApp.packageName,
+ patchedApp.apkFilePath,
+ outFile!.path,
+ )
+ ? 0
+ : 1;
+ } else {
+ installErrorDialog(1.1);
+ }
+ } else {
+ if (await _rootAPI.hasRootPermissions()) {
+ await _rootAPI.unmount(patchedApp.packageName);
+ }
+ if (context.mounted) {
+ return await installApk(
+ context,
+ outFile!.path,
+ );
+ }
}
+ } on Exception catch (e) {
+ if (kDebugMode) {
+ print(e);
+ }
+ }
+ }
+ return 1;
+ }
+
+ Future installApk(
+ BuildContext context,
+ String apkPath,
+ ) async {
+ try {
+ final status = await patcherChannel.invokeMethod('installApk', {
+ 'apkPath': apkPath,
+ });
+ final int statusCode = status['status'];
+ final String message = status['message'];
+ final bool hasExtra =
+ message.contains('INSTALL_FAILED_VERIFICATION_FAILURE') ||
+ message.contains('INSTALL_FAILED_VERSION_DOWNGRADE');
+ if (statusCode == 0 || (statusCode == 3 && !hasExtra)) {
+ return statusCode;
} else {
- final install = await InstallPlugin.installApk(outFile!.path);
- return install['isSuccess'];
+ _managerAPI.ctx = context;
+ return await installErrorDialog(
+ statusCode,
+ status,
+ hasExtra,
+ );
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
- return false;
+ return 3;
}
}
- return false;
-}
-void exportPatchedFile(String appName, String version) {
- try {
- if (outFile != null) {
- final String newName = _getFileName(appName, version);
- FlutterFileDialog.saveFile(
- params: SaveFileDialogParams(
- sourceFilePath: outFile!.path,
- fileName: newName,
- mimeTypesFilter: ['application/vnd.android.package-archive'],
- ),
- );
- }
- } on Exception catch (e) {
- if (kDebugMode) {
- print(e);
- }
- }
-}
-
-void sharePatchedFile(String appName, String version) {
- try {
- if (outFile != null) {
- final String newName = _getFileName(appName, version);
- final int lastSeparator = outFile!.path.lastIndexOf('/');
- final String newPath =
- outFile!.path.substring(0, lastSeparator + 1) + newName;
- final File shareFile = outFile!.copySync(newPath);
- Share.shareXFiles([XFile(shareFile.path)]);
- }
- } on Exception catch (e) {
- if (kDebugMode) {
- print(e);
- }
- }
-}
-
-String _getFileName(String appName, String version) {
- final String patchVersion = _managerAPI.patchesVersion!;
- final String prefix = appName.toLowerCase().replaceAll(' ', '-');
- final String newName = '$prefix-revanced_v$version-patches_$patchVersion.apk';
- return newName;
-}
-
-Future exportPatcherLog(String logs) async {
- final Directory appCache = await getTemporaryDirectory();
- final Directory logDir = Directory('${appCache.path}/logs');
- logDir.createSync();
- final String dateTime = DateTime.now()
- .toIso8601String()
- .replaceAll('-', '')
- .replaceAll(':', '')
- .replaceAll('T', '')
- .replaceAll('.', '');
- final String fileName = 'revanced-manager_patcher_$dateTime.txt';
- final File log = File('${logDir.path}/$fileName');
- log.writeAsStringSync(logs);
- FlutterFileDialog.saveFile(
- params:SaveFileDialogParams(
- sourceFilePath: log.path,
- fileName: fileName,
- ),
- );
-}
-
-String getSuggestedVersion(String packageName) {
- final Map versions = {};
- for (final Patch patch in _patches) {
- final Package? package = patch.compatiblePackages.firstWhereOrNull(
- (pack) => pack.name == packageName,
+ Future installErrorDialog(
+ num statusCode, [
+ status,
+ bool hasExtra = false,
+ ]) async {
+ final String statusValue = InstallStatus.byCode(
+ hasExtra ? double.parse('$statusCode.1') : statusCode,
);
- if (package != null) {
- for (final String version in package.versions) {
- versions.update(
- version,
- (value) => versions[version]! + 1,
- ifAbsent: () => 1,
+ bool cleanInstall = false;
+ final bool isFixable = statusCode == 4 || statusCode == 5;
+ await showDialog(
+ context: _managerAPI.ctx!,
+ builder: (context) => AlertDialog(
+ backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
+ title: I18nText('installErrorDialog.$statusValue'),
+ content: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ I18nText(
+ 'installErrorDialog.${statusValue}_description',
+ translationParams: statusCode == 2
+ ? {
+ 'packageName': status['otherPackageName'],
+ }
+ : null,
+ ),
+ ],
+ ),
+ actions: (status == null)
+ ? [
+ CustomMaterialButton(
+ label: I18nText('okButton'),
+ onPressed: () async {
+ Navigator.pop(context);
+ },
+ ),
+ ]
+ : [
+ CustomMaterialButton(
+ isFilled: !isFixable,
+ label: I18nText('cancelButton'),
+ onPressed: () {
+ Navigator.pop(context);
+ },
+ ),
+ if (isFixable)
+ CustomMaterialButton(
+ label: I18nText('okButton'),
+ onPressed: () async {
+ final int response = await patcherChannel.invokeMethod(
+ 'uninstallApp',
+ {'packageName': status['packageName']},
+ );
+ if (response == 0 && context.mounted) {
+ cleanInstall = true;
+ Navigator.pop(context);
+ }
+ },
+ ),
+ ],
+ ),
+ );
+ return cleanInstall ? 10 : 1;
+ }
+
+ void exportPatchedFile(String appName, String version) {
+ try {
+ if (outFile != null) {
+ final String newName = _getFileName(appName, version);
+ FlutterFileDialog.saveFile(
+ params: SaveFileDialogParams(
+ sourceFilePath: outFile!.path,
+ fileName: newName,
+ mimeTypesFilter: ['application/vnd.android.package-archive'],
+ ),
);
}
+ } on Exception catch (e) {
+ if (kDebugMode) {
+ print(e);
+ }
}
}
- if (versions.isNotEmpty) {
- final entries = versions.entries.toList()
- ..sort((a, b) => a.value.compareTo(b.value));
- versions
- ..clear()
- ..addEntries(entries);
- versions.removeWhere((key, value) => value != versions.values.last);
- return (versions.keys.toList()
- ..sort()).last;
+
+ void sharePatchedFile(String appName, String version) {
+ try {
+ if (outFile != null) {
+ final String newName = _getFileName(appName, version);
+ final int lastSeparator = outFile!.path.lastIndexOf('/');
+ final String newPath =
+ outFile!.path.substring(0, lastSeparator + 1) + newName;
+ final File shareFile = outFile!.copySync(newPath);
+ Share.shareXFiles([XFile(shareFile.path)]);
+ }
+ } on Exception catch (e) {
+ if (kDebugMode) {
+ print(e);
+ }
+ }
}
- return '';
-}}
+
+ String _getFileName(String appName, String version) {
+ final String patchVersion = _managerAPI.patchesVersion!;
+ final String prefix = appName.toLowerCase().replaceAll(' ', '-');
+ final String newName =
+ '$prefix-revanced_v$version-patches_$patchVersion.apk';
+ return newName;
+ }
+
+ Future exportPatcherLog(String logs) async {
+ final Directory appCache = await getTemporaryDirectory();
+ final Directory logDir = Directory('${appCache.path}/logs');
+ logDir.createSync();
+ final String dateTime = DateTime.now()
+ .toIso8601String()
+ .replaceAll('-', '')
+ .replaceAll(':', '')
+ .replaceAll('T', '')
+ .replaceAll('.', '');
+ final String fileName = 'revanced-manager_patcher_$dateTime.txt';
+ final File log = File('${logDir.path}/$fileName');
+ log.writeAsStringSync(logs);
+ FlutterFileDialog.saveFile(
+ params: SaveFileDialogParams(
+ sourceFilePath: log.path,
+ fileName: fileName,
+ ),
+ );
+ }
+
+ String getSuggestedVersion(String packageName) {
+ final Map versions = {};
+ for (final Patch patch in _patches) {
+ final Package? package = patch.compatiblePackages.firstWhereOrNull(
+ (pack) => pack.name == packageName,
+ );
+ if (package != null) {
+ for (final String version in package.versions) {
+ versions.update(
+ version,
+ (value) => versions[version]! + 1,
+ ifAbsent: () => 1,
+ );
+ }
+ }
+ }
+ if (versions.isNotEmpty) {
+ final entries = versions.entries.toList()
+ ..sort((a, b) => a.value.compareTo(b.value));
+ versions
+ ..clear()
+ ..addEntries(entries);
+ versions.removeWhere((key, value) => value != versions.values.last);
+ return (versions.keys.toList()..sort()).last;
+ }
+ return '';
+ }
+}
+
+enum InstallStatus {
+ mountNoRoot(1),
+ mountVersionMismatch(1.1),
+ mountMissingInstallation(1.2),
+
+ statusFailureBlocked(2),
+ installFailedVerificationFailure(3.1),
+ statusFailureInvalid(4),
+ installFailedVersionDowngrade(4.1),
+ statusFailureConflict(5),
+ statusFailureStorage(6),
+ statusFailureIncompatible(7),
+ statusFailureTimeout(8);
+
+ const InstallStatus(this.statusCode);
+ final double statusCode;
+
+ static String byCode(num code) {
+ try {
+ return InstallStatus.values
+ .firstWhere((flag) => flag.statusCode == code)
+ .status;
+ } catch (e) {
+ return 'status_unknown';
+ }
+ }
+}
+
+extension InstallStatusExtension on InstallStatus {
+ String get status {
+ switch (this) {
+ case InstallStatus.mountNoRoot:
+ return 'mount_no_root';
+ case InstallStatus.mountVersionMismatch:
+ return 'mount_version_mismatch';
+ case InstallStatus.mountMissingInstallation:
+ return 'mount_missing_installation';
+ case InstallStatus.statusFailureBlocked:
+ return 'status_failure_blocked';
+ case InstallStatus.installFailedVerificationFailure:
+ return 'install_failed_verification_failure';
+ case InstallStatus.statusFailureInvalid:
+ return 'status_failure_invalid';
+ case InstallStatus.installFailedVersionDowngrade:
+ return 'install_failed_version_downgrade';
+ case InstallStatus.statusFailureConflict:
+ return 'status_failure_conflict';
+ case InstallStatus.statusFailureStorage:
+ return 'status_failure_storage';
+ case InstallStatus.statusFailureIncompatible:
+ return 'status_failure_incompatible';
+ case InstallStatus.statusFailureTimeout:
+ return 'status_failure_timeout';
+ }
+ }
+}
diff --git a/lib/services/root_api.dart b/lib/services/root_api.dart
index f0c7d917..2b3f4cf2 100644
--- a/lib/services/root_api.dart
+++ b/lib/services/root_api.dart
@@ -2,10 +2,10 @@ import 'package:flutter/foundation.dart';
import 'package:root/root.dart';
class RootAPI {
- // TODO(ponces): remove in the future, keep it for now during migration.
- final String _revancedOldDirPath = '/data/local/tmp/revanced-manager';
- final String _revancedDirPath = '/data/adb/revanced';
+ // TODO(aAbed): remove in the future, keep it for now during migration.
final String _postFsDataDirPath = '/data/adb/post-fs-data.d';
+
+ final String _revancedDirPath = '/data/adb/revanced';
final String _serviceDDirPath = '/data/adb/service.d';
Future isRooted() async {
@@ -75,7 +75,7 @@ class RootAPI {
Future> getInstalledApps() async {
final List apps = List.empty(growable: true);
try {
- String? res = await Root.exec(
+ final String? res = await Root.exec(
cmd: 'ls "$_revancedDirPath"',
);
if (res != null) {
@@ -83,15 +83,6 @@ class RootAPI {
list.removeWhere((pack) => pack.isEmpty);
apps.addAll(list.map((pack) => pack.trim()).toList());
}
- // TODO(ponces): remove in the future, keep it for now during migration.
- res = await Root.exec(
- cmd: 'ls "$_revancedOldDirPath"',
- );
- if (res != null) {
- final List list = res.split('\n');
- list.removeWhere((pack) => pack.isEmpty);
- apps.addAll(list.map((pack) => pack.trim()).toList());
- }
} on Exception catch (e) {
if (kDebugMode) {
print(e);
@@ -100,16 +91,9 @@ class RootAPI {
return apps;
}
- Future deleteApp(String packageName, String originalFilePath) async {
+ Future unmount(String packageName) async {
await Root.exec(
- cmd: 'am force-stop "$packageName"',
- );
- await Root.exec(
- cmd: 'su -mm -c "umount -l $originalFilePath"',
- );
- // TODO(ponces): remove in the future, keep it for now during migration.
- await Root.exec(
- cmd: 'rm -rf "$_revancedOldDirPath/$packageName"',
+ cmd: 'grep $packageName /proc/mounts | while read -r line; do echo \$line | cut -d " " -f 2 | sed "s/apk.*/apk/" | xargs -r umount -l; done',
);
await Root.exec(
cmd: 'rm -rf "$_revancedDirPath/$packageName"',
@@ -117,8 +101,21 @@ class RootAPI {
await Root.exec(
cmd: 'rm -rf "$_serviceDDirPath/$packageName.sh"',
);
+ }
+
+ // TODO(aAbed): remove in the future, keep it for now during migration.
+ Future removeOrphanedFiles() async {
await Root.exec(
- cmd: 'rm -rf "$_postFsDataDirPath/$packageName.sh"',
+ cmd: '''
+ find "$_revancedDirPath" -type f -name original.apk -delete
+ for file in "$_serviceDDirPath"/*; do
+ filename=\$(basename "\$file")
+ if [ -f "$_postFsDataDirPath/\$filename" ]; then
+ rm "$_postFsDataDirPath/\$filename"
+ fi
+ done
+ '''
+ .trim(),
);
}
@@ -128,7 +125,6 @@ class RootAPI {
String patchedFilePath,
) async {
try {
- await deleteApp(packageName, originalFilePath);
await Root.exec(
cmd: 'mkdir -p "$_revancedDirPath/$packageName"',
);
@@ -138,11 +134,9 @@ class RootAPI {
'',
'$_revancedDirPath/$packageName',
);
- await saveOriginalFilePath(packageName, originalFilePath);
await installServiceDScript(packageName);
- await installPostFsDataScript(packageName);
await installApk(packageName, patchedFilePath);
- await mountApk(packageName, originalFilePath);
+ await mountApk(packageName);
return true;
} on Exception catch (e) {
if (kDebugMode) {
@@ -156,26 +150,25 @@ class RootAPI {
await Root.exec(
cmd: 'mkdir -p "$_serviceDDirPath"',
);
- final String content = '#!/system/bin/sh\n'
- 'while [ "\$(getprop sys.boot_completed | tr -d \'"\'"\'\\\\r\'"\'"\')" != "1" ]; do sleep 3; done\n'
- 'base_path=$_revancedDirPath/$packageName/base.apk\n'
- 'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
- r'[ ! -z $stock_path ] && mount -o bind $base_path $stock_path';
- final String scriptFilePath = '$_serviceDDirPath/$packageName.sh';
- await Root.exec(
- cmd: 'echo \'$content\' > "$scriptFilePath"',
- );
- await setPermissions('0744', '', '', scriptFilePath);
- }
+ final String content = '''
+ #!/system/bin/sh
+ MAGISKTMP="\$(magisk --path)" || MAGISKTMP=/sbin
+ MIRROR="\$MAGISKTMP/.magisk/mirror"
- Future installPostFsDataScript(String packageName) async {
- await Root.exec(
- cmd: 'mkdir -p "$_postFsDataDirPath"',
- );
- final String content = '#!/system/bin/sh\n'
- 'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
- r'[ ! -z $stock_path ] && umount -l $stock_path';
- final String scriptFilePath = '$_postFsDataDirPath/$packageName.sh';
+ until [ "\$(getprop sys.boot_completed)" = 1 ]; do sleep 3; done
+ until [ -d "/sdcard/Android" ]; do sleep 1; done
+
+ base_path=$_revancedDirPath/$packageName/base.apk
+ stock_path=\$(pm path $packageName | grep base | sed 's/package://g' )
+
+ chcon u:object_r:apk_data_file:s0 \$base_path
+ mount -o bind \$MIRROR\$base_path \$stock_path
+
+ # Kill the app to force it to restart the mounted APK in case it's already running
+ am force-stop $packageName
+ '''
+ .trim();
+ final String scriptFilePath = '$_serviceDDirPath/$packageName.sh';
await Root.exec(
cmd: 'echo \'$content\' > "$scriptFilePath"',
);
@@ -195,49 +188,12 @@ class RootAPI {
);
}
- Future mountApk(String packageName, String originalFilePath) async {
- final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk';
+ Future mountApk(String packageName,) async {
await Root.exec(
- cmd: 'am force-stop "$packageName"',
- );
- await Root.exec(
- cmd: 'su -mm -c "umount -l $originalFilePath"',
- );
- await Root.exec(
- cmd: 'su -mm -c "mount -o bind $newPatchedFilePath $originalFilePath"',
- );
- }
-
- Future isMounted(String packageName) async {
- final String? res = await Root.exec(
- cmd: 'cat /proc/mounts | grep $packageName',
- );
- return res != null && res.isNotEmpty;
- }
-
- Future saveOriginalFilePath(
- String packageName,
- String originalFilePath,
- ) async {
- final String originalRootPath =
- '$_revancedDirPath/$packageName/original.apk';
- await Root.exec(
- cmd: 'mkdir -p "$_revancedDirPath/$packageName"',
- );
- await setPermissions(
- '0755',
- 'shell:shell',
- '',
- '$_revancedDirPath/$packageName',
- );
- await Root.exec(
- cmd: 'cp "$originalFilePath" "$originalRootPath"',
- );
- await setPermissions(
- '0644',
- 'shell:shell',
- 'u:object_r:apk_data_file:s0',
- originalFilePath,
+ cmd: '''
+ grep $packageName /proc/mounts | while read -r line; do echo \$line | cut -d " " -f 2 | sed "s/apk.*/apk/" | xargs -r umount -l; done
+ .$_serviceDDirPath/$packageName.sh
+ '''.trim(),
);
}
diff --git a/lib/ui/views/home/home_viewmodel.dart b/lib/ui/views/home/home_viewmodel.dart
index 8df23910..a93fb16f 100644
--- a/lib/ui/views/home/home_viewmodel.dart
+++ b/lib/ui/views/home/home_viewmodel.dart
@@ -8,7 +8,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:injectable/injectable.dart';
-import 'package:install_plugin/install_plugin.dart';
import 'package:path_provider/path_provider.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/app/app.router.dart';
@@ -53,7 +52,7 @@ class HomeViewModel extends BaseViewModel {
_toast.showBottom('homeView.installingMessage');
final File? managerApk = await _managerAPI.downloadManager();
if (managerApk != null) {
- await InstallPlugin.installApk(managerApk.path);
+ await _patcherAPI.installApk(context, managerApk.path);
} else {
_toast.showBottom('homeView.errorDownloadMessage');
}
@@ -75,7 +74,7 @@ class HomeViewModel extends BaseViewModel {
_toast.showBottom('homeView.installingMessage');
final File? managerApk = await _managerAPI.downloadManager();
if (managerApk != null) {
- await InstallPlugin.installApk(managerApk.path);
+ await _patcherAPI.installApk(context, managerApk.path);
} else {
_toast.showBottom('homeView.errorDownloadMessage');
}
@@ -84,6 +83,7 @@ class HomeViewModel extends BaseViewModel {
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
}
+
void navigateToAppInfo(PatchedApplication app) {
_navigationService.navigateTo(
Routes.appInfoView,
@@ -268,6 +268,7 @@ class HomeViewModel extends BaseViewModel {
valueListenable: downloaded,
builder: (context, value, child) {
return SimpleDialog(
+ backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
contentPadding: const EdgeInsets.all(16.0),
title: I18nText(
!value
@@ -365,9 +366,7 @@ class HomeViewModel extends BaseViewModel {
alignment: Alignment.centerRight,
child: FilledButton(
onPressed: () async {
- await InstallPlugin.installApk(
- downloadedApk!.path,
- );
+ await _patcherAPI.installApk(context, downloadedApk!.path);
},
child: I18nText('updateButton'),
),
@@ -412,7 +411,7 @@ class HomeViewModel extends BaseViewModel {
// UILocalNotificationDateInterpretation.absoluteTime,
// );
_toast.showBottom('homeView.installingMessage');
- await InstallPlugin.installApk(managerApk.path);
+ await _patcherAPI.installApk(context, managerApk.path);
} else {
_toast.showBottom('homeView.errorDownloadMessage');
}
diff --git a/lib/ui/views/installer/installer_viewmodel.dart b/lib/ui/views/installer/installer_viewmodel.dart
index 32a30197..039af2b4 100644
--- a/lib/ui/views/installer/installer_viewmodel.dart
+++ b/lib/ui/views/installer/installer_viewmodel.dart
@@ -316,7 +316,7 @@ class InstallerViewModel extends BaseViewModel {
await showDialog(
context: context,
barrierDismissible: false,
- builder: (context) => AlertDialog(
+ builder: (innerContext) => AlertDialog(
title: I18nText(
'installerView.installType',
),
@@ -367,6 +367,19 @@ class InstallerViewModel extends BaseViewModel {
installType.value = selected!;
},
),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: I18nText(
+ 'installerView.warning',
+ child: Text(
+ '',
+ style: TextStyle(
+ fontWeight: FontWeight.w500,
+ color: Theme.of(context).colorScheme.error,
+ ),
+ ),
+ ),
+ ),
],
);
},
@@ -375,13 +388,13 @@ class InstallerViewModel extends BaseViewModel {
actions: [
TextButton(
onPressed: () {
- Navigator.of(context).pop();
+ Navigator.of(innerContext).pop();
},
child: I18nText('cancelButton'),
),
FilledButton(
onPressed: () {
- Navigator.of(context).pop();
+ Navigator.of(innerContext).pop();
installResult(context, installType.value == 1);
},
child: I18nText('installerView.installButton'),
@@ -390,7 +403,32 @@ class InstallerViewModel extends BaseViewModel {
),
);
} else {
- installResult(context, false);
+ await showDialog(
+ context: context,
+ barrierDismissible: false,
+ builder: (innerContext) => AlertDialog(
+ title: I18nText(
+ 'warning',
+ ),
+ contentPadding: const EdgeInsets.all(16),
+ content: I18nText('installerView.warning'),
+ actions: [
+ TextButton(
+ onPressed: () {
+ Navigator.of(innerContext).pop();
+ },
+ child: I18nText('cancelButton'),
+ ),
+ FilledButton(
+ onPressed: () {
+ Navigator.of(innerContext).pop();
+ installResult(context, false);
+ },
+ child: I18nText('installerView.installButton'),
+ ),
+ ],
+ ),
+ );
}
}
@@ -411,15 +449,18 @@ class InstallerViewModel extends BaseViewModel {
Future installResult(BuildContext context, bool installAsRoot) async {
try {
_app.isRooted = installAsRoot;
- update(
- 1.0,
- 'Installing...',
- _app.isRooted
- ? 'Installing patched file using root method'
- : 'Installing patched file using nonroot method',
- );
- isInstalled = await _patcherAPI.installPatchedFile(_app);
- if (isInstalled) {
+ if (headerLogs != 'Installing...') {
+ update(
+ 1.0,
+ 'Installing...',
+ _app.isRooted
+ ? 'Mounting patched app'
+ : 'Installing patched app',
+ );
+ }
+ final int response = await _patcherAPI.installPatchedFile(context, _app);
+ if (response == 0) {
+ isInstalled = true;
_app.isFromStorage = false;
_app.patchDate = DateTime.now();
_app.appliedPatches = _patches.map((p) => p.name).toList();
@@ -435,9 +476,26 @@ class InstallerViewModel extends BaseViewModel {
await _managerAPI.savePatchedApp(_app);
- update(1.0, 'Installed!', 'Installed!');
+ update(1.0, 'Installed', 'Installed');
+ } else if (response == 3) {
+ update(
+ 1.0,
+ 'Installation canceled',
+ 'Installation canceled',
+ );
+ } else if (response == 10) {
+ installResult(context, installAsRoot);
+ update(
+ 1.0,
+ '',
+ 'Starting installer',
+ );
} else {
- // TODO(aabed): Show error message.
+ update(
+ 1.0,
+ 'Installation failed',
+ 'Installation failed',
+ );
}
} on Exception catch (e) {
if (kDebugMode) {
diff --git a/lib/ui/views/patches_selector/patches_selector_viewmodel.dart b/lib/ui/views/patches_selector/patches_selector_viewmodel.dart
index 6886e0a0..82f330b5 100644
--- a/lib/ui/views/patches_selector/patches_selector_viewmodel.dart
+++ b/lib/ui/views/patches_selector/patches_selector_viewmodel.dart
@@ -184,10 +184,6 @@ class PatchesSelectorViewModel extends BaseViewModel {
void selectPatches() {
locator().selectedPatches = selectedPatches;
saveSelectedPatches();
- if (_managerAPI.ctx != null) {
- Navigator.pop(_managerAPI.ctx!);
- _managerAPI.ctx = null;
- }
locator().notifyListeners();
}
diff --git a/lib/ui/widgets/appInfoView/app_info_viewmodel.dart b/lib/ui/widgets/appInfoView/app_info_viewmodel.dart
index 60b9681a..d4652f44 100644
--- a/lib/ui/widgets/appInfoView/app_info_viewmodel.dart
+++ b/lib/ui/widgets/appInfoView/app_info_viewmodel.dart
@@ -29,7 +29,9 @@ class AppInfoViewModel extends BaseViewModel {
if (app.isRooted) {
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) {
- await _rootAPI.deleteApp(app.packageName, app.apkFilePath);
+ await _rootAPI.unmount(
+ app.packageName,
+ );
if (!onlyUnpatch) {
await DeviceApps.uninstallApp(app.packageName);
}
diff --git a/pubspec.yaml b/pubspec.yaml
index 9f4c120e..9b1af650 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -65,10 +65,6 @@ dependencies:
flutter_dotenv: ^5.0.2
flutter_markdown: ^0.6.14
dio_cache_interceptor: ^3.4.0
- install_plugin:
- git: # remove once https://github.com/hui-z/flutter_install_plugin/pull/67 is merged
- url: https://github.com/BenjaminHalko/flutter_install_plugin
- ref: 5f9b1a8c956fc3355ae655eefcbcadb457bd10f7 # Branch: master
screenshot_callback:
git: # remove once https://github.com/flutter-moum/flutter_screenshot_callback/pull/81 is merged
url: https://github.com/BenjaminHalko/flutter_screenshot_callback