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