From fa0237b027eb60a08402935afca77f20b05cc97d Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 06:51:11 +0100 Subject: [PATCH 01/19] feat: Implemented `Magisk module` installation --- .../data/room/apps/installed/InstalledApp.kt | 3 +- .../manager/domain/installer/RootInstaller.kt | 61 +++++++++++++++++++ .../ui/screen/InstalledAppInfoScreen.kt | 18 +++++- .../manager/ui/viewmodel/AppsViewModel.kt | 11 +++- .../ui/viewmodel/InstalledAppInfoViewModel.kt | 2 + .../manager/ui/viewmodel/PatcherViewModel.kt | 29 +++++++++ .../ui/viewmodel/SelectedAppInfoViewModel.kt | 1 + app/src/main/res/values/strings.xml | 1 + 8 files changed, 123 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt index c0986dfd10..e42773afc0 100644 --- a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt +++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt @@ -7,7 +7,8 @@ import app.revanced.manager.R enum class InstallType(val stringResource: Int) { DEFAULT(R.string.default_install), - MOUNT(R.string.mount_install) + MOUNT(R.string.mount_install), + MAGISK(R.string.magisk_install) } @Entity(tableName = "installed_app") diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index b7fbf19c7a..43c65805dd 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -69,6 +69,9 @@ class RootInstaller( suspend fun isAppInstalled(packageName: String) = awaitRemoteFS().getFile("$modulesPath/$packageName-revanced").exists() + suspend fun isAppInstalledAsMagiskModule(packageName: String) = + awaitRemoteFS().getFile("$modulesPath/revanced_${packageName.replace('.', '_')}").exists() + suspend fun isAppMounted(packageName: String) = withContext(Dispatchers.IO) { pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let { execute("mount | grep \"$it\"").isSuccess @@ -163,6 +166,64 @@ class RootInstaller( } } + suspend fun installAsMagiskModule( + patchedAPK: File, + packageName: String, + version: String, + label: String + ) = withContext(Dispatchers.IO) { + val remoteFS = awaitRemoteFS() + val sanitizedPackageName = packageName.replace('.', '_') + val modulePath = "$modulesPath/revanced_$sanitizedPackageName" + + val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir + ?: throw Exception("Failed to load application info") + + // Derive the parent directory relative to root (e.g. "system/app/Example"). + val stockApkParent = stockAPK.removePrefix("/").substringBeforeLast("/") + val moduleApkDir = "$modulePath/$stockApkParent" + + remoteFS.getFile(moduleApkDir).apply { + if (!mkdirs() && !exists()) { + throw Exception("Failed to create Magisk module directory") + } + } + + val moduleProp = buildString { + appendLine("id=revanced_$sanitizedPackageName") + appendLine("name=$label ReVanced") + appendLine("version=$version") + appendLine("versionCode=1") + appendLine("author=ReVanced") + append("description=Patched by ReVanced") + } + remoteFS.getFile("$modulePath/module.prop").newOutputStream() + .use { it.write(moduleProp.toByteArray()) } + + val targetApkPath = "$moduleApkDir/base.apk" + remoteFS.getFile(patchedAPK.absolutePath) + .also { if (!it.exists()) throw Exception("File doesn't exist") } + .newInputStream().use { inputStream -> + remoteFS.getFile(targetApkPath).newOutputStream().use { outputStream -> + inputStream.copyTo(outputStream) + } + } + + execute( + "chmod 644 $targetApkPath", + "chown system:system $targetApkPath", + "chcon u:object_r:apk_data_file:s0 $targetApkPath" + ).assertSuccess("Failed to set file permissions") + } + + suspend fun uninstallMagiskModule(packageName: String) { + val remoteFS = awaitRemoteFS() + val sanitizedPackageName = packageName.replace('.', '_') + + remoteFS.getFile("$modulesPath/revanced_$sanitizedPackageName").deleteRecursively() + .also { if (!it) throw Exception("Failed to delete Magisk module files") } + } + suspend fun uninstall(packageName: String) { val remoteFS = awaitRemoteFS() if (isAppMounted(packageName)) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt index 49257b5c47..339a9622d6 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt @@ -105,6 +105,13 @@ fun InstalledAppInfoScreen( style = MaterialTheme.typography.bodySmall ) } + + if (installedApp.installType == InstallType.MAGISK) { + Text( + text = stringResource(R.string.magisk_install), + style = MaterialTheme.typography.bodySmall + ) + } } Row( @@ -142,6 +149,15 @@ fun InstalledAppInfoScreen( ) } + InstallType.MAGISK -> { + SegmentedButton( + icon = Icons.Outlined.SettingsBackupRestore, + text = stringResource(R.string.unpatch), + onClick = { showUninstallDialog = true }, + enabled = viewModel.rootInstaller.hasRootAccess() + ) + } + } SegmentedButton( @@ -150,7 +166,7 @@ fun InstalledAppInfoScreen( onClick = { onPatchClick(installedApp.originalPackageName) }, - enabled = installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess() + enabled = installedApp.installType == InstallType.DEFAULT || viewModel.rootInstaller.hasRootAccess() ) } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppsViewModel.kt index 09ac84f171..9968d85977 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppsViewModel.kt @@ -130,9 +130,18 @@ class AppsViewModel( } catch (_: RootServiceException) { } } + if (app.installType == InstallType.MAGISK) { + try { + if (!rootInstaller.isAppInstalledAsMagiskModule(app.currentPackageName)) { + installedAppsRepository.delete(app) + return@withContext null + } + } catch (_: RootServiceException) { } + } + val packageInfo = pm.getPackageInfo(app.currentPackageName) - if (packageInfo == null && app.installType != InstallType.MOUNT) { + if (packageInfo == null && app.installType != InstallType.MOUNT && app.installType != InstallType.MAGISK) { installedAppsRepository.delete(app) return@withContext null } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt index 70914d5122..074c4e6d7f 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt @@ -109,6 +109,8 @@ class InstalledAppInfoViewModel( } InstallType.MOUNT -> rootInstaller.uninstall(app.currentPackageName) + + InstallType.MAGISK -> rootInstaller.uninstallMagiskModule(app.currentPackageName) } installedAppRepository.delete(app) onBackClick() diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index 32d2098270..cafc5b8ae3 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -636,6 +636,35 @@ class PatcherViewModel( needsRootUninstall = false downloadedAppRepository.deleteFor(packageName) } + + InstallType.MAGISK -> { + val label = with(pm) { + currentPackageInfo.label() + } + + val inputVersion = input.selectedApp.version + ?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName } + ?: throw Exception("Failed to determine input APK version") + + rootInstaller.installAsMagiskModule( + outputFile, packageName, inputVersion, label + ) + + val bundleInfo = patchBundleRepository.bundleInfoFlow.first() + installedAppRepository.addOrUpdate( + currentPackageInfo.packageName, + packageName, + inputVersion, + InstallType.MAGISK, + input.selectedPatches, + bundleInfo + ) + + installedPackageName = packageName + + app.toast(app.getString(R.string.install_app_success)) + downloadedAppRepository.deleteFor(packageName) + } } } } finally { diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt index 981143c301..76153af67c 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -243,6 +243,7 @@ class SelectedAppInfoViewModel( val matchesVersion = requiredVersion == null || installed.version == requiredVersion val usable = when { meta?.installType == InstallType.MOUNT && !hasRoot -> false + meta?.installType == InstallType.MAGISK && !hasRoot -> false meta?.installType == InstallType.DEFAULT -> false else -> true } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c7ae10283e..97d6cec1b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -377,6 +377,7 @@ It’s only compatible with these versions: %2$s Default Mount + Magisk module Mounted Not mounted Mount From 71f75f6e1cb70b30af49c1f610a7ddef7c7e614b Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 14:10:07 +0100 Subject: [PATCH 02/19] fix(RootInstaller): Use `service.sh` for reliable magisk data mounts --- .../manager/domain/installer/RootInstaller.kt | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index 43c65805dd..6f19babe4e 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -175,15 +175,9 @@ class RootInstaller( val remoteFS = awaitRemoteFS() val sanitizedPackageName = packageName.replace('.', '_') val modulePath = "$modulesPath/revanced_$sanitizedPackageName" + val assets = app.assets - val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir - ?: throw Exception("Failed to load application info") - - // Derive the parent directory relative to root (e.g. "system/app/Example"). - val stockApkParent = stockAPK.removePrefix("/").substringBeforeLast("/") - val moduleApkDir = "$modulePath/$stockApkParent" - - remoteFS.getFile(moduleApkDir).apply { + remoteFS.getFile(modulePath).apply { if (!mkdirs() && !exists()) { throw Exception("Failed to create Magisk module directory") } @@ -200,7 +194,20 @@ class RootInstaller( remoteFS.getFile("$modulePath/module.prop").newOutputStream() .use { it.write(moduleProp.toByteArray()) } - val targetApkPath = "$moduleApkDir/base.apk" + assets.open("root/service.sh").use { inputStream -> + remoteFS.getFile("$modulePath/service.sh").newOutputStream() + .use { outputStream -> + val content = String(inputStream.readBytes()) + .replace("__PKG_NAME__", packageName) + .replace("__VERSION__", version) + .replace("__LABEL__", label) + .toByteArray() + + outputStream.write(content) + } + } + + val targetApkPath = "$modulePath/$packageName.apk" remoteFS.getFile(patchedAPK.absolutePath) .also { if (!it.exists()) throw Exception("File doesn't exist") } .newInputStream().use { inputStream -> @@ -212,7 +219,8 @@ class RootInstaller( execute( "chmod 644 $targetApkPath", "chown system:system $targetApkPath", - "chcon u:object_r:apk_data_file:s0 $targetApkPath" + "chcon u:object_r:apk_data_file:s0 $targetApkPath", + "chmod +x $modulePath/service.sh" ).assertSuccess("Failed to set file permissions") } From 7c69819b8644993f714c73c209714c80cc224329 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 15:06:06 +0100 Subject: [PATCH 03/19] feat(ui): Add restart recommendation after Magisk installation --- .../ui/component/InstallerStatusDialog.kt | 7 ++++++ .../manager/ui/model/InstallerModel.kt | 1 + .../manager/ui/viewmodel/PatcherViewModel.kt | 22 ++++++++++++++----- app/src/main/res/values/strings.xml | 3 +++ 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt index 82fddad0a6..d85716f721 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt @@ -116,6 +116,13 @@ enum class DialogKind( contentStringResId = R.string.installation_timeout_description, confirmButton = installerStatusDialogButton(R.string.try_again) { it.install() }, dismissButton = installerStatusDialogButton(R.string.cancel), + ), + SUCCESS_MAGISK( + flag = 1000, + title = R.string.magisk_install_success_title, + contentStringResId = R.string.magisk_install_success_description, + confirmButton = installerStatusDialogButton(R.string.restart_now) { it.reboot() }, + dismissButton = installerStatusDialogButton(R.string.close), ); // Needed due to the @FromValue annotation. diff --git a/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt b/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt index 410b64c1bd..1c080c53c0 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt @@ -3,4 +3,5 @@ package app.revanced.manager.ui.model interface InstallerModel { fun reinstall() fun install() + fun reboot() } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index cafc5b8ae3..fc4e25a595 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -646,9 +646,14 @@ class PatcherViewModel( ?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName } ?: throw Exception("Failed to determine input APK version") - rootInstaller.installAsMagiskModule( - outputFile, packageName, inputVersion, label - ) + try { + rootInstaller.installAsMagiskModule( + outputFile, packageName, inputVersion, label + ) + } catch (e: Exception) { + packageInstallerStatus = AndroidPackageInstaller.STATUS_FAILURE + throw e + } val bundleInfo = patchBundleRepository.bundleInfoFlow.first() installedAppRepository.addOrUpdate( @@ -661,8 +666,7 @@ class PatcherViewModel( ) installedPackageName = packageName - - app.toast(app.getString(R.string.install_app_success)) + packageInstallerStatus = 1000 // SUCCESS_MAGISK downloadedAppRepository.deleteFor(packageName) } } @@ -682,7 +686,13 @@ class PatcherViewModel( override fun install() { // InstallType.MOUNT is never used here since this overload is for the package installer status dialog. - install(InstallType.DEFAULT) + install(if (packageInstallerStatus == AndroidPackageInstaller.STATUS_FAILURE) InstallType.MAGISK else InstallType.DEFAULT) + } + + override fun reboot() { + viewModelScope.launch { + rootInstaller.execute("reboot") + } } override fun reinstall() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 97d6cec1b7..75beb9055d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -611,4 +611,7 @@ Tap them for more details. %d selected + Magisk module installed + A system restart is recommended to properly apply the changes. + Restart now From 3933e93acaa52a97c914fb2a24c3be7c43cece79 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 15:07:52 +0100 Subject: [PATCH 04/19] fix: Support non-installed/uninstalled apps --- app/src/main/assets/root/service.sh | 38 +++++++++++------ .../manager/domain/installer/RootInstaller.kt | 42 +++++++++---------- 2 files changed, 46 insertions(+), 34 deletions(-) diff --git a/app/src/main/assets/root/service.sh b/app/src/main/assets/root/service.sh index dc3bcb5f45..f9cf15648f 100644 --- a/app/src/main/assets/root/service.sh +++ b/app/src/main/assets/root/service.sh @@ -3,38 +3,52 @@ DIR=${0%/*} package_name="__PKG_NAME__" version="__VERSION__" +sanitized_package_name=$(echo "$package_name" | sed 's/\./_/g') -rm "$DIR/log" +rm -f "$DIR/log" { +echo "Induction check for $package_name" until [ "$(getprop sys.boot_completed)" = 1 ]; do sleep 5; done -sleep 5 +# Wait a bit more for package manager to settle +sleep 10 -base_path="$DIR/$package_name.apk" -stock_path="$(pm path "$package_name" | grep base | sed 's/package://g')" -stock_version="$(dumpsys package "$package_name" | grep versionName | cut -d "=" -f2)" +base_path="$DIR/system/app/$sanitized_package_name/base.apk" +if [ ! -f "$base_path" ]; then + # Fallback to old path for compatibility during transition + base_path="$DIR/$package_name.apk" +fi + +stock_path="$(pm path "$package_name" | grep base | sed 's/package://g' | head -n 1)" +stock_version="$(dumpsys package "$package_name" | grep versionName | cut -d "=" -f2 | head -n 1 | sed 's/ //g')" echo "base_path: $base_path" echo "stock_path: $stock_path" echo "base_version: $version" echo "stock_version: $stock_version" -if mount | grep -q "$stock_path" ; then - echo "Not mounting as stock path is already mounted" +if [ -z "$stock_path" ]; then + echo "App $package_name is not installed. System app induction might have failed or still being processed." exit 1 fi -if [ "$version" != "$stock_version" ]; then - echo "Not mounting as versions don't match" - exit 1 +if echo "$stock_path" | grep -q "^/system/"; then + echo "App is already running from system partition (likely our Magisk overlay). Skipping bind mount." + exit 0 fi -if [ -z "$stock_path" ]; then - echo "Not mounting as app info could not be loaded" +if mount | grep -q "$stock_path" ; then + echo "Not mounting as stock path is already mounted" exit 1 fi +if [ "$version" != "$stock_version" ]; then + echo "Version mismatch: base=$version, stock=$stock_version. Attempting to mount anyway as it might be a minor diff." + # Optional: exit 1 if you want to be strict +fi + +echo "Mounting $base_path over $stock_path" mount -o bind "$base_path" "$stock_path" } >> "$DIR/log" diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index 6f19babe4e..5965eb4f80 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -135,16 +135,15 @@ class RootInstaller( "module.prop", ).forEach { file -> assets.open("root/$file").use { inputStream -> - remoteFS.getFile("$modulePath/$file").newOutputStream() - .use { outputStream -> - val content = String(inputStream.readBytes()) - .replace("__PKG_NAME__", packageName) - .replace("__VERSION__", version) - .replace("__LABEL__", label) - .toByteArray() - - outputStream.write(content) - } + remoteFS.getFile("$modulePath/$file").newOutputStream().use { outputStream -> + val content = String(inputStream.readBytes()) + .replace("__PKG_NAME__", packageName) + .replace("__VERSION__", version) + .replace("__LABEL__", label) + .toByteArray() + + outputStream.write(content) + } } } @@ -175,13 +174,10 @@ class RootInstaller( val remoteFS = awaitRemoteFS() val sanitizedPackageName = packageName.replace('.', '_') val modulePath = "$modulesPath/revanced_$sanitizedPackageName" + val systemAppPath = "$modulePath/system/app/$sanitizedPackageName" val assets = app.assets - remoteFS.getFile(modulePath).apply { - if (!mkdirs() && !exists()) { - throw Exception("Failed to create Magisk module directory") - } - } + execute("mkdir -p \"$systemAppPath\"").assertSuccess("Failed to create system app directory") val moduleProp = buildString { appendLine("id=revanced_$sanitizedPackageName") @@ -191,8 +187,9 @@ class RootInstaller( appendLine("author=ReVanced") append("description=Patched by ReVanced") } - remoteFS.getFile("$modulePath/module.prop").newOutputStream() - .use { it.write(moduleProp.toByteArray()) } + remoteFS.getFile("$modulePath/module.prop").newOutputStream().use { outputStream -> + outputStream.write(moduleProp.toByteArray()) + } assets.open("root/service.sh").use { inputStream -> remoteFS.getFile("$modulePath/service.sh").newOutputStream() @@ -207,7 +204,7 @@ class RootInstaller( } } - val targetApkPath = "$modulePath/$packageName.apk" + val targetApkPath = "$systemAppPath/base.apk" remoteFS.getFile(patchedAPK.absolutePath) .also { if (!it.exists()) throw Exception("File doesn't exist") } .newInputStream().use { inputStream -> @@ -217,10 +214,11 @@ class RootInstaller( } execute( - "chmod 644 $targetApkPath", - "chown system:system $targetApkPath", - "chcon u:object_r:apk_data_file:s0 $targetApkPath", - "chmod +x $modulePath/service.sh" + "chmod 644 \"$targetApkPath\"", + "chmod 755 \"$systemAppPath\"", + "chown -R system:system \"$modulePath/system\"", + "chcon -R u:object_r:system_file:s0 \"$modulePath/system\"", + "chmod +x \"$modulePath/service.sh\"" ).assertSuccess("Failed to set file permissions") } From f5280ab378edf9d43d1ba994135205f03ea9e3b0 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 16:09:09 +0100 Subject: [PATCH 05/19] fix: Support native library extraction for modules --- .../manager/domain/installer/RootInstaller.kt | 54 +++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index 5965eb4f80..b7c82647c6 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.time.withTimeoutOrNull import kotlinx.coroutines.withContext import java.io.File import java.time.Duration +import java.util.zip.ZipFile class RootInstaller( private val app: Application, @@ -157,14 +158,54 @@ class RootInstaller( } execute( - "chmod 644 $apkPath", - "chown system:system $apkPath", - "chcon u:object_r:apk_data_file:s0 $apkPath", - "chmod +x $modulePath/service.sh" + "chmod 644 \"$apkPath\"", + "chown system:system \"$apkPath\"", + "chcon u:object_r:apk_data_file:s0 \"$apkPath\"", + "chmod +x \"$modulePath/service.sh\"" ).assertSuccess("Failed to set file permissions") } } + private fun extractNativeLibraries(apkFile: File, systemAppPath: String, remoteFS: FileSystemManager) { + val libPath = "$systemAppPath/lib" + + // Delete existing library directory to avoid remnants from different architectures + remoteFS.getFile(libPath).apply { + if (exists()) deleteRecursively() + mkdirs() + } + + ZipFile(apkFile).use { zip -> + zip.entries().asSequence() + .filter { it.name.startsWith("lib/") && it.name.endsWith(".so") } + .forEach { entry -> + val parts = entry.name.split("/") + if (parts.size < 3) return@forEach + + val apkAbi = parts[1] + val libName = parts.last() + + val systemAbi = when (apkAbi) { + "arm64-v8a" -> "arm64" + "armeabi-v7a" -> "arm" + "x86_64" -> "x86_64" + "x86" -> "x86" + else -> apkAbi + } + + val targetDir = "$libPath/$systemAbi" + remoteFS.getFile(targetDir).apply { if (!exists()) mkdirs() } + + val targetFile = "$targetDir/$libName" + zip.getInputStream(entry).use { inputStream -> + remoteFS.getFile(targetFile).newOutputStream().use { outputStream -> + inputStream.copyTo(outputStream) + } + } + } + } + } + suspend fun installAsMagiskModule( patchedAPK: File, packageName: String, @@ -213,9 +254,14 @@ class RootInstaller( } } + // Extract native libraries if any exist in the APK + extractNativeLibraries(patchedAPK, systemAppPath, remoteFS) + execute( "chmod 644 \"$targetApkPath\"", "chmod 755 \"$systemAppPath\"", + "chmod -R 755 \"$systemAppPath/lib\"", + "find \"$systemAppPath/lib\" -type f -name \"*.so\" -exec chmod 644 {} +", "chown -R system:system \"$modulePath/system\"", "chcon -R u:object_r:system_file:s0 \"$modulePath/system\"", "chmod +x \"$modulePath/service.sh\"" From 977668a85c1e2d12e9ae5a4dd1961808725ff1ce Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 17:49:24 +0100 Subject: [PATCH 06/19] feat: Migration of magisk logic from `manager` to `library` --- .../manager/domain/installer/RootInstaller.kt | 243 ++---------------- 1 file changed, 26 insertions(+), 217 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index b7c82647c6..a0ed6bb51c 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -5,19 +5,20 @@ import android.content.ComponentName import android.content.Intent import android.content.ServiceConnection import android.os.IBinder +import app.revanced.library.MagiskUtils +import app.revanced.library.ShellCommandException import app.revanced.manager.IRootSystemService import app.revanced.manager.service.ManagerRootService import app.revanced.manager.util.PM import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.ipc.RootService import com.topjohnwu.superuser.nio.FileSystemManager +import java.io.File +import java.time.Duration import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.time.withTimeoutOrNull import kotlinx.coroutines.withContext -import java.io.File -import java.time.Duration -import java.util.zip.ZipFile class RootInstaller( private val app: Application, @@ -44,9 +45,8 @@ class RootInstaller( } } - return withTimeoutOrNull(Duration.ofSeconds(20L)) { - remoteFS.await() - } ?: throw RootServiceException() + return withTimeoutOrNull(Duration.ofSeconds(20L)) { remoteFS.await() } + ?: throw RootServiceException() } private suspend fun getShell() = with(CompletableDeferred()) { @@ -61,17 +61,15 @@ class RootInstaller( return getShell().newJob().add(*commands).to(stdout, stderr).exec() } - fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false + fun hasRootAccess() = MagiskUtils.hasRootAccess() - fun isDeviceRooted() = System.getenv("PATH")?.split(":")?.any { path -> - File(path, "su").canExecute() - } ?: false + fun isDeviceRooted() = MagiskUtils.isDeviceRooted() suspend fun isAppInstalled(packageName: String) = - awaitRemoteFS().getFile("$modulesPath/$packageName-revanced").exists() + MagiskUtils.isInstalled(packageName, awaitRemoteFS()) suspend fun isAppInstalledAsMagiskModule(packageName: String) = - awaitRemoteFS().getFile("$modulesPath/revanced_${packageName.replace('.', '_')}").exists() + MagiskUtils.isInstalledAsMagiskModule(packageName, awaitRemoteFS()) suspend fun isAppMounted(packageName: String) = withContext(Dispatchers.IO) { pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let { @@ -83,11 +81,11 @@ class RootInstaller( if (isAppMounted(packageName)) return withContext(Dispatchers.IO) { - val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir - ?: throw Exception("Failed to load application info") - val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk" - - execute("mount -o bind \"$patchedAPK\" \"$stockAPK\"").assertSuccess("Failed to mount APK") + val sourceDir = + pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir + ?: throw Exception("Failed to load application info") + + MagiskUtils.mount(packageName, sourceDir) } } @@ -95,10 +93,10 @@ class RootInstaller( if (!isAppMounted(packageName)) return withContext(Dispatchers.IO) { - val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir + val sourceDir = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir ?: throw Exception("Failed to load application info") - execute("umount -l \"$stockAPK\"").assertSuccess("Failed to unmount APK") + MagiskUtils.unmount(sourceDir) } } @@ -110,100 +108,19 @@ class RootInstaller( label: String ) = withContext(Dispatchers.IO) { val remoteFS = awaitRemoteFS() - val assets = app.assets - val modulePath = "$modulesPath/$packageName-revanced" unmount(packageName) stockAPK?.let { stockApp -> - // TODO: get user id programmatically execute("pm uninstall -k --user 0 $packageName") - - execute("pm install -r -d --user 0 \"${stockApp.absolutePath}\"") - .assertSuccess("Failed to install stock app") - - stockApp.delete() - } - - remoteFS.getFile(modulePath).apply { - if (!mkdirs() && !exists()) { - throw Exception("Failed to create module directory") - } - } - - listOf( - "service.sh", - "module.prop", - ).forEach { file -> - assets.open("root/$file").use { inputStream -> - remoteFS.getFile("$modulePath/$file").newOutputStream().use { outputStream -> - val content = String(inputStream.readBytes()) - .replace("__PKG_NAME__", packageName) - .replace("__VERSION__", version) - .replace("__LABEL__", label) - .toByteArray() - - outputStream.write(content) - } + val result = execute("pm install -r -d --user 0 \"${stockApp.absolutePath}\"") + if (!result.isSuccess) { + throw ShellCommandException("Failed to install stock app", result.code, result.out, result.err) } + stockApp.delete() } - "$modulePath/$packageName.apk".let { apkPath -> - remoteFS.getFile(patchedAPK.absolutePath) - .also { if (!it.exists()) throw Exception("File doesn't exist") } - .newInputStream().use { inputStream -> - remoteFS.getFile(apkPath).newOutputStream().use { outputStream -> - inputStream.copyTo(outputStream) - } - } - - execute( - "chmod 644 \"$apkPath\"", - "chown system:system \"$apkPath\"", - "chcon u:object_r:apk_data_file:s0 \"$apkPath\"", - "chmod +x \"$modulePath/service.sh\"" - ).assertSuccess("Failed to set file permissions") - } - } - - private fun extractNativeLibraries(apkFile: File, systemAppPath: String, remoteFS: FileSystemManager) { - val libPath = "$systemAppPath/lib" - - // Delete existing library directory to avoid remnants from different architectures - remoteFS.getFile(libPath).apply { - if (exists()) deleteRecursively() - mkdirs() - } - - ZipFile(apkFile).use { zip -> - zip.entries().asSequence() - .filter { it.name.startsWith("lib/") && it.name.endsWith(".so") } - .forEach { entry -> - val parts = entry.name.split("/") - if (parts.size < 3) return@forEach - - val apkAbi = parts[1] - val libName = parts.last() - - val systemAbi = when (apkAbi) { - "arm64-v8a" -> "arm64" - "armeabi-v7a" -> "arm" - "x86_64" -> "x86_64" - "x86" -> "x86" - else -> apkAbi - } - - val targetDir = "$libPath/$systemAbi" - remoteFS.getFile(targetDir).apply { if (!exists()) mkdirs() } - - val targetFile = "$targetDir/$libName" - zip.getInputStream(entry).use { inputStream -> - remoteFS.getFile(targetFile).newOutputStream().use { outputStream -> - inputStream.copyTo(outputStream) - } - } - } - } + MagiskUtils.provisionRootFolder(remoteFS, app.assets, packageName, version, label, patchedAPK) } suspend fun installAsMagiskModule( @@ -212,124 +129,16 @@ class RootInstaller( version: String, label: String ) = withContext(Dispatchers.IO) { - val remoteFS = awaitRemoteFS() - val sanitizedPackageName = packageName.replace('.', '_') - val modulePath = "$modulesPath/revanced_$sanitizedPackageName" - val systemAppPath = "$modulePath/system/app/$sanitizedPackageName" - val assets = app.assets - - execute("mkdir -p \"$systemAppPath\"").assertSuccess("Failed to create system app directory") - - val moduleProp = buildString { - appendLine("id=revanced_$sanitizedPackageName") - appendLine("name=$label ReVanced") - appendLine("version=$version") - appendLine("versionCode=1") - appendLine("author=ReVanced") - append("description=Patched by ReVanced") - } - remoteFS.getFile("$modulePath/module.prop").newOutputStream().use { outputStream -> - outputStream.write(moduleProp.toByteArray()) - } - - assets.open("root/service.sh").use { inputStream -> - remoteFS.getFile("$modulePath/service.sh").newOutputStream() - .use { outputStream -> - val content = String(inputStream.readBytes()) - .replace("__PKG_NAME__", packageName) - .replace("__VERSION__", version) - .replace("__LABEL__", label) - .toByteArray() - - outputStream.write(content) - } - } - - val targetApkPath = "$systemAppPath/base.apk" - remoteFS.getFile(patchedAPK.absolutePath) - .also { if (!it.exists()) throw Exception("File doesn't exist") } - .newInputStream().use { inputStream -> - remoteFS.getFile(targetApkPath).newOutputStream().use { outputStream -> - inputStream.copyTo(outputStream) - } - } - - // Extract native libraries if any exist in the APK - extractNativeLibraries(patchedAPK, systemAppPath, remoteFS) - - execute( - "chmod 644 \"$targetApkPath\"", - "chmod 755 \"$systemAppPath\"", - "chmod -R 755 \"$systemAppPath/lib\"", - "find \"$systemAppPath/lib\" -type f -name \"*.so\" -exec chmod 644 {} +", - "chown -R system:system \"$modulePath/system\"", - "chcon -R u:object_r:system_file:s0 \"$modulePath/system\"", - "chmod +x \"$modulePath/service.sh\"" - ).assertSuccess("Failed to set file permissions") + MagiskUtils.provisionMagiskModule(awaitRemoteFS(), app.assets, packageName, version, label, patchedAPK) } suspend fun uninstallMagiskModule(packageName: String) { - val remoteFS = awaitRemoteFS() - val sanitizedPackageName = packageName.replace('.', '_') - - remoteFS.getFile("$modulesPath/revanced_$sanitizedPackageName").deleteRecursively() - .also { if (!it) throw Exception("Failed to delete Magisk module files") } + MagiskUtils.uninstallMagiskModule(packageName, awaitRemoteFS()) } suspend fun uninstall(packageName: String) { - val remoteFS = awaitRemoteFS() - if (isAppMounted(packageName)) - unmount(packageName) - - remoteFS.getFile("$modulesPath/$packageName-revanced").deleteRecursively() - .also { if (!it) throw Exception("Failed to delete files") } - } - - companion object { - const val modulesPath = "/data/adb/modules" - - private fun Shell.Result.assertSuccess(errorMessage: String) { - if (!isSuccess) { - throw ShellCommandException( - errorMessage, - code, - out, - err - ) - } - } - } -} - -class ShellCommandException( - val userMessage: String, - val exitCode: Int, - val stdout: List, - val stderr: List -) : Exception(format(userMessage, exitCode, stdout, stderr)) { - companion object { - private fun format( - message: String, - exitCode: Int, - stdout: List, - stderr: List - ): String = - buildString { - appendLine(message) - appendLine("Exit code: $exitCode") - - val output = stdout.filter { it.isNotBlank() } - val errors = stderr.filter { it.isNotBlank() } - - if (output.isNotEmpty()) { - appendLine("stdout:") - output.forEach(::appendLine) - } - if (errors.isNotEmpty()) { - appendLine("stderr:") - errors.forEach(::appendLine) - } - } + if (isAppMounted(packageName)) unmount(packageName) + MagiskUtils.uninstall(packageName, awaitRemoteFS()) } } From 81dc1469016a4279d630a18758a9d41b7a6ec832 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 23:44:29 +0100 Subject: [PATCH 07/19] feat: Activate and roll back Magisk modules without a reboot --- .../manager/domain/installer/RootInstaller.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index a0ed6bb51c..57ed213c09 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -110,6 +110,9 @@ class RootInstaller( val remoteFS = awaitRemoteFS() unmount(packageName) + if (isAppInstalledAsMagiskModule(packageName)) { + uninstallMagiskModule(packageName) + } stockAPK?.let { stockApp -> execute("pm uninstall -k --user 0 $packageName") @@ -120,7 +123,7 @@ class RootInstaller( stockApp.delete() } - MagiskUtils.provisionRootFolder(remoteFS, app.assets, packageName, version, label, patchedAPK) + MagiskUtils.provisionRootFolder(remoteFS, packageName, version, label, patchedAPK) } suspend fun installAsMagiskModule( @@ -129,10 +132,15 @@ class RootInstaller( version: String, label: String ) = withContext(Dispatchers.IO) { - MagiskUtils.provisionMagiskModule(awaitRemoteFS(), app.assets, packageName, version, label, patchedAPK) + if (isAppInstalled(packageName)) { + uninstall(packageName) + } + MagiskUtils.provisionMagiskModule(awaitRemoteFS(), packageName, version, label, patchedAPK) + runCatching { mount(packageName) } } suspend fun uninstallMagiskModule(packageName: String) { + if (isAppMounted(packageName)) unmount(packageName) MagiskUtils.uninstallMagiskModule(packageName, awaitRemoteFS()) } From 668a94f7f011a8c46497e863caf82dab069312ed Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 23:44:45 +0100 Subject: [PATCH 08/19] feat: Update Magisk install success message for live activation --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 75beb9055d..a3fc808b29 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -612,6 +612,6 @@ Tap them for more details. %d selected Magisk module installed - A system restart is recommended to properly apply the changes. + The patched app is now active. You can restart the device if you want to fully apply the changes. Restart now From 5a90c43d58d590e5f83cf5c27f37bbb3bb27fe0b Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Thu, 9 Apr 2026 16:45:41 +0100 Subject: [PATCH 09/19] refactor: Removed deprecated files Moved to `revanced-library` --- app/src/main/assets/root/module.prop | 6 ---- app/src/main/assets/root/service.sh | 54 ---------------------------- 2 files changed, 60 deletions(-) delete mode 100644 app/src/main/assets/root/module.prop delete mode 100644 app/src/main/assets/root/service.sh diff --git a/app/src/main/assets/root/module.prop b/app/src/main/assets/root/module.prop deleted file mode 100644 index 05a5a159dd..0000000000 --- a/app/src/main/assets/root/module.prop +++ /dev/null @@ -1,6 +0,0 @@ -id=__PKG_NAME__-ReVanced -name=__LABEL__ ReVanced -version=__VERSION__ -versionCode=0 -author=ReVanced -description=Mounts the patched APK on top of the original one \ No newline at end of file diff --git a/app/src/main/assets/root/service.sh b/app/src/main/assets/root/service.sh deleted file mode 100644 index f9cf15648f..0000000000 --- a/app/src/main/assets/root/service.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/system/bin/sh -DIR=${0%/*} - -package_name="__PKG_NAME__" -version="__VERSION__" -sanitized_package_name=$(echo "$package_name" | sed 's/\./_/g') - -rm -f "$DIR/log" - -{ -echo "Induction check for $package_name" - -until [ "$(getprop sys.boot_completed)" = 1 ]; do sleep 5; done -# Wait a bit more for package manager to settle -sleep 10 - -base_path="$DIR/system/app/$sanitized_package_name/base.apk" -if [ ! -f "$base_path" ]; then - # Fallback to old path for compatibility during transition - base_path="$DIR/$package_name.apk" -fi - -stock_path="$(pm path "$package_name" | grep base | sed 's/package://g' | head -n 1)" -stock_version="$(dumpsys package "$package_name" | grep versionName | cut -d "=" -f2 | head -n 1 | sed 's/ //g')" - -echo "base_path: $base_path" -echo "stock_path: $stock_path" -echo "base_version: $version" -echo "stock_version: $stock_version" - -if [ -z "$stock_path" ]; then - echo "App $package_name is not installed. System app induction might have failed or still being processed." - exit 1 -fi - -if echo "$stock_path" | grep -q "^/system/"; then - echo "App is already running from system partition (likely our Magisk overlay). Skipping bind mount." - exit 0 -fi - -if mount | grep -q "$stock_path" ; then - echo "Not mounting as stock path is already mounted" - exit 1 -fi - -if [ "$version" != "$stock_version" ]; then - echo "Version mismatch: base=$version, stock=$stock_version. Attempting to mount anyway as it might be a minor diff." - # Optional: exit 1 if you want to be strict -fi - -echo "Mounting $base_path over $stock_path" -mount -o bind "$base_path" "$stock_path" - -} >> "$DIR/log" From 2f9271af1f151cc24940704980923b73b087d5be Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Thu, 9 Apr 2026 17:02:57 +0100 Subject: [PATCH 10/19] fix: `Magisk module` logic, root checks and migration --- .../manager/domain/installer/RootInstaller.kt | 22 ++++++++---- .../component/patcher/InstallPickerDialog.kt | 3 ++ .../revanced/manager/ui/model/RootStatus.kt | 7 ++++ .../manager/ui/screen/PatcherScreen.kt | 26 +++++++++++--- .../ui/viewmodel/InstalledAppInfoViewModel.kt | 2 +- .../manager/ui/viewmodel/PatcherViewModel.kt | 34 ++++++++++++++++--- 6 files changed, 77 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/app/revanced/manager/ui/model/RootStatus.kt diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index 57ed213c09..ba1d4058f5 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -50,6 +50,7 @@ class RootInstaller( } private suspend fun getShell() = with(CompletableDeferred()) { + Shell.getCachedShell()?.takeIf { !it.isRoot }?.close() Shell.getShell(::complete) await() @@ -65,6 +66,10 @@ class RootInstaller( fun isDeviceRooted() = MagiskUtils.isDeviceRooted() + fun isMagiskInstalled() = MagiskUtils.isMagiskInstalled() + + fun requestRoot() = MagiskUtils.requestRoot() + suspend fun isAppInstalled(packageName: String) = MagiskUtils.isInstalled(packageName, awaitRemoteFS()) @@ -108,10 +113,11 @@ class RootInstaller( label: String ) = withContext(Dispatchers.IO) { val remoteFS = awaitRemoteFS() + val patchedPackageName = withContext(Dispatchers.IO) { pm.getPackageInfo(patchedAPK)?.packageName } ?: packageName unmount(packageName) if (isAppInstalledAsMagiskModule(packageName)) { - uninstallMagiskModule(packageName) + uninstallMagiskModule(packageName, patchedPackageName) } stockAPK?.let { stockApp -> @@ -129,19 +135,21 @@ class RootInstaller( suspend fun installAsMagiskModule( patchedAPK: File, packageName: String, + patchedPackageName: String, version: String, label: String ) = withContext(Dispatchers.IO) { - if (isAppInstalled(packageName)) { + if (isAppInstalledAsMagiskModule(packageName)) { + uninstallMagiskModule(packageName, patchedPackageName) + } else if (isAppInstalled(packageName)) { uninstall(packageName) } - MagiskUtils.provisionMagiskModule(awaitRemoteFS(), packageName, version, label, patchedAPK) - runCatching { mount(packageName) } + MagiskUtils.provisionMagiskModule(awaitRemoteFS(), packageName, patchedPackageName, version, label, patchedAPK) + runCatching { execute("pm install -r -d --user 0 \"/data/adb/revanced/$packageName/base.apk\"") } } - suspend fun uninstallMagiskModule(packageName: String) { - if (isAppMounted(packageName)) unmount(packageName) - MagiskUtils.uninstallMagiskModule(packageName, awaitRemoteFS()) + suspend fun uninstallMagiskModule(packageName: String, patchedPackageName: String) { + MagiskUtils.uninstallMagiskModule(packageName, patchedPackageName, awaitRemoteFS()) } suspend fun uninstall(packageName: String) { diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt index aa5c1b748c..c1033b032a 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt @@ -20,6 +20,7 @@ import app.revanced.manager.util.transparentListItemColors @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun InstallPickerDialog( + isMagiskInstalled: Boolean, onDismiss: () -> Unit, onConfirm: (InstallType) -> Unit ) { @@ -47,6 +48,8 @@ fun InstallPickerDialog( text = { Column(modifier = Modifier.verticalScroll(rememberScrollState())) { InstallType.entries.forEach { + // Dont show magisk if its not installed + if (it == InstallType.MAGISK && !isMagiskInstalled) return@forEach ListItem( modifier = Modifier.clickable { selectedInstallType = it }, leadingContent = { diff --git a/app/src/main/java/app/revanced/manager/ui/model/RootStatus.kt b/app/src/main/java/app/revanced/manager/ui/model/RootStatus.kt new file mode 100644 index 0000000000..58c7c667a7 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/model/RootStatus.kt @@ -0,0 +1,7 @@ +package app.revanced.manager.ui.model + +enum class RootCheckResult { + GRANTED, + DENIED, + UNAVAILABLE +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index 0d13a647b8..437b88187c 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -41,6 +41,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -61,6 +62,8 @@ import app.revanced.manager.ui.component.patcher.InstallPickerDialog import app.revanced.manager.ui.component.patcher.Steps import app.revanced.manager.ui.model.StepCategory import app.revanced.manager.ui.viewmodel.PatcherViewModel +import app.revanced.manager.ui.viewmodel.RootCheckResult +import kotlinx.coroutines.launch import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.EventEffect import app.revanced.manager.util.toast @@ -87,6 +90,8 @@ fun PatcherScreen( } val patcherSucceeded by viewModel.patcherSucceeded.observeAsState(null) + val coroutineScope = rememberCoroutineScope() + var isCheckingRoot by remember { mutableStateOf(false) } val canInstall by remember { derivedStateOf { patcherSucceeded == true && (viewModel.installedPackageName != null || !viewModel.isInstalling) } } var showInstallPicker by rememberSaveable { mutableStateOf(false) } var showDismissConfirmationDialog by rememberSaveable { mutableStateOf(false) } @@ -117,6 +122,7 @@ fun PatcherScreen( if (showInstallPicker) InstallPickerDialog( + isMagiskInstalled = viewModel.isMagiskInstalled, onDismiss = { showInstallPicker = false }, onConfirm = viewModel::install ) @@ -243,10 +249,22 @@ fun PatcherScreen( ) }, onClick = { - if (viewModel.installedPackageName == null) - if (viewModel.isDeviceRooted()) showInstallPicker = true - else viewModel.install(InstallType.DEFAULT) - else viewModel.open() + if (viewModel.installedPackageName == null) { + isCheckingRoot = true + coroutineScope.launch { + try { + when (viewModel.requestRootForInstall()) { + RootCheckResult.GRANTED -> showInstallPicker = true + RootCheckResult.DENIED, + RootCheckResult.UNAVAILABLE -> viewModel.install(InstallType.DEFAULT) + } + } finally { + isCheckingRoot = false + } + } + } else { + viewModel.open() + } }, elevation = FloatingActionButtonDefaults.elevation( defaultElevation = 0.dp, diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt index 074c4e6d7f..43fe646a6f 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt @@ -110,7 +110,7 @@ class InstalledAppInfoViewModel( InstallType.MOUNT -> rootInstaller.uninstall(app.currentPackageName) - InstallType.MAGISK -> rootInstaller.uninstallMagiskModule(app.currentPackageName) + InstallType.MAGISK -> rootInstaller.uninstallMagiskModule(app.originalPackageName, app.currentPackageName) } installedAppRepository.delete(app) onBackClick() diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index fc4e25a595..820b43dbd5 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -50,6 +50,7 @@ import app.revanced.manager.patcher.patch.PatchBundleInfo import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.ui.model.InstallerModel +import app.revanced.manager.ui.model.RootCheckResult import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.Step @@ -133,6 +134,11 @@ class PatcherViewModel( var isInstalling by mutableStateOf(false) private set + var isMagiskInstalled by mutableStateOf(false) + private set + + private var lastInstallType = InstallType.DEFAULT + private var currentActivityRequest: Pair, String>? by mutableStateOf( null ) @@ -271,6 +277,12 @@ class PatcherViewModel( viewModelScope.launch { installedApp = installedAppRepository.get(packageName) } + + viewModelScope.launch(Dispatchers.IO) { + if (rootInstaller.isDeviceRooted()) { + isMagiskInstalled = rootInstaller.isMagiskInstalled() + } + } } @OptIn(DelicateCoroutinesApi::class) @@ -339,6 +351,20 @@ class PatcherViewModel( fun isDeviceRooted() = rootInstaller.isDeviceRooted() + suspend fun requestRootForInstall(): RootCheckResult { + if (!isDeviceRooted()) return RootCheckResult.UNAVAILABLE + + val hasRoot = withContext(Dispatchers.IO) { rootInstaller.requestRoot() } + return if (hasRoot) { + isMagiskInstalled = true + RootCheckResult.GRANTED + } else { + logger.warn(app.getString(R.string.root_access_denied_warning)) + withContext(Dispatchers.Main) { app.toast(R.string.root_access_denied_warning) } + RootCheckResult.DENIED + } + } + fun rejectInteraction() { currentActivityRequest?.first?.complete(false) } @@ -567,6 +593,7 @@ class PatcherViewModel( fun install(installType: InstallType) = viewModelScope.launch { isInstalling = true + lastInstallType = installType var needsRootUninstall = false try { uiSafe(app, R.string.install_app_fail, "Failed to install") { @@ -648,7 +675,7 @@ class PatcherViewModel( try { rootInstaller.installAsMagiskModule( - outputFile, packageName, inputVersion, label + outputFile, packageName, currentPackageInfo.packageName, inputVersion, label ) } catch (e: Exception) { packageInstallerStatus = AndroidPackageInstaller.STATUS_FAILURE @@ -684,10 +711,7 @@ class PatcherViewModel( } } - override fun install() { - // InstallType.MOUNT is never used here since this overload is for the package installer status dialog. - install(if (packageInstallerStatus == AndroidPackageInstaller.STATUS_FAILURE) InstallType.MAGISK else InstallType.DEFAULT) - } + override fun install() { install(lastInstallType) } override fun reboot() { viewModelScope.launch { From 0a825e7189ef93dd7b8a3d4a91218072585777aa Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Thu, 9 Apr 2026 17:03:42 +0100 Subject: [PATCH 11/19] enhance: Display module status --- .../app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt index 339a9622d6..0abdf1e90f 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt @@ -108,7 +108,7 @@ fun InstalledAppInfoScreen( if (installedApp.installType == InstallType.MAGISK) { Text( - text = stringResource(R.string.magisk_install), + text = stringResource(if (viewModel.appInfo != null) R.string.generic_active else R.string.generic_inactive), style = MaterialTheme.typography.bodySmall ) } From 1a35ce93c12e5acb6a30789dd434ecc35bef8037 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Thu, 9 Apr 2026 17:03:59 +0100 Subject: [PATCH 12/19] refactor: Updated strings --- app/src/main/res/values/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a3fc808b29..4373ef99e7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -612,6 +612,9 @@ Tap them for more details. %d selected Magisk module installed - The patched app is now active. You can restart the device if you want to fully apply the changes. + The patched app has been installed and is immediately active. We recommended restarting your device to fully apply the changes. Restart now + Active + Inactive + Root access denied. Installing without root. From 51d57b46dcea7d0d9f51068c13b2d4c35ac8d698 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Thu, 9 Apr 2026 17:15:02 +0100 Subject: [PATCH 13/19] enhance: Finished library migration --- .../revanced/manager/domain/installer/RootInstaller.kt | 10 +++------- .../app/revanced/manager/ui/screen/PatcherScreen.kt | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index ba1d4058f5..7fe2411821 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -6,7 +6,6 @@ import android.content.Intent import android.content.ServiceConnection import android.os.IBinder import app.revanced.library.MagiskUtils -import app.revanced.library.ShellCommandException import app.revanced.manager.IRootSystemService import app.revanced.manager.service.ManagerRootService import app.revanced.manager.util.PM @@ -121,11 +120,8 @@ class RootInstaller( } stockAPK?.let { stockApp -> - execute("pm uninstall -k --user 0 $packageName") - val result = execute("pm install -r -d --user 0 \"${stockApp.absolutePath}\"") - if (!result.isSuccess) { - throw ShellCommandException("Failed to install stock app", result.code, result.out, result.err) - } + MagiskUtils.uninstallKeepData(packageName) + MagiskUtils.installApk(stockApp.absolutePath) stockApp.delete() } @@ -145,7 +141,7 @@ class RootInstaller( uninstall(packageName) } MagiskUtils.provisionMagiskModule(awaitRemoteFS(), packageName, patchedPackageName, version, label, patchedAPK) - runCatching { execute("pm install -r -d --user 0 \"/data/adb/revanced/$packageName/base.apk\"") } + runCatching { MagiskUtils.installApk("/data/adb/revanced/$packageName/base.apk") } } suspend fun uninstallMagiskModule(packageName: String, patchedPackageName: String) { diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index 437b88187c..252eff5f0c 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -62,7 +62,7 @@ import app.revanced.manager.ui.component.patcher.InstallPickerDialog import app.revanced.manager.ui.component.patcher.Steps import app.revanced.manager.ui.model.StepCategory import app.revanced.manager.ui.viewmodel.PatcherViewModel -import app.revanced.manager.ui.viewmodel.RootCheckResult +import app.revanced.manager.ui.model.RootCheckResult import kotlinx.coroutines.launch import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.EventEffect From 1d49ffec90d0169f846bf39062158711b4c18df5 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Thu, 9 Apr 2026 19:53:58 +0100 Subject: [PATCH 14/19] refactor: Use `Constants` value instead of hardcode --- .../app/revanced/manager/domain/installer/RootInstaller.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index 7fe2411821..b6da21c9ae 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -6,6 +6,8 @@ import android.content.Intent import android.content.ServiceConnection import android.os.IBinder import app.revanced.library.MagiskUtils +import app.revanced.library.installation.installer.Constants +import app.revanced.library.installation.installer.Constants.invoke import app.revanced.manager.IRootSystemService import app.revanced.manager.service.ManagerRootService import app.revanced.manager.util.PM @@ -141,7 +143,7 @@ class RootInstaller( uninstall(packageName) } MagiskUtils.provisionMagiskModule(awaitRemoteFS(), packageName, patchedPackageName, version, label, patchedAPK) - runCatching { MagiskUtils.installApk("/data/adb/revanced/$packageName/base.apk") } + runCatching { MagiskUtils.installApk(Constants.MOUNTED_APK_PATH(packageName)) } } suspend fun uninstallMagiskModule(packageName: String, patchedPackageName: String) { From 626feef85e19be756f6d1ad95b107999f7707453 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Thu, 9 Apr 2026 19:54:53 +0100 Subject: [PATCH 15/19] fix: Surface root fail-safe to default install --- .../main/java/app/revanced/manager/ui/screen/PatcherScreen.kt | 3 +++ app/src/main/res/values/strings.xml | 1 + 2 files changed, 4 insertions(+) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index 252eff5f0c..1c402edd07 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -258,6 +258,9 @@ fun PatcherScreen( RootCheckResult.DENIED, RootCheckResult.UNAVAILABLE -> viewModel.install(InstallType.DEFAULT) } + } catch (_: Exception) { + context.toast(R.string.root_check_failed) + viewModel.install(InstallType.DEFAULT) } finally { isCheckingRoot = false } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4373ef99e7..10d0d368ef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -617,4 +617,5 @@ Tap them for more details. Active Inactive Root access denied. Installing without root. + Failed to check root access. Installing without root. From 337469314597bb3567aa30ceb3e662117738036b Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Thu, 9 Apr 2026 23:09:21 +0100 Subject: [PATCH 16/19] feat: Root status in `About device` and patch logs --- .../manager/domain/installer/RootInstaller.kt | 6 ++++++ .../revanced/manager/ui/model/RootStatus.kt | 10 ++++++---- .../screen/settings/AdvancedSettingsScreen.kt | 4 ++++ .../ui/viewmodel/AdvancedSettingsViewModel.kt | 19 ++++++++++++++++++- .../manager/ui/viewmodel/PatcherViewModel.kt | 4 ++-- app/src/main/res/values/strings.xml | 2 ++ 6 files changed, 38 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index b6da21c9ae..4db6b31aa3 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -9,6 +9,7 @@ import app.revanced.library.MagiskUtils import app.revanced.library.installation.installer.Constants import app.revanced.library.installation.installer.Constants.invoke import app.revanced.manager.IRootSystemService +import app.revanced.manager.ui.model.RootCheckResult import app.revanced.manager.service.ManagerRootService import app.revanced.manager.util.PM import com.topjohnwu.superuser.Shell @@ -71,6 +72,11 @@ class RootInstaller( fun requestRoot() = MagiskUtils.requestRoot() + fun checkRootStatus(): RootCheckResult = when { + !isDeviceRooted() -> RootCheckResult.UNAVAILABLE + isMagiskInstalled() -> RootCheckResult.GRANTED + else -> RootCheckResult.DENIED + } suspend fun isAppInstalled(packageName: String) = MagiskUtils.isInstalled(packageName, awaitRemoteFS()) diff --git a/app/src/main/java/app/revanced/manager/ui/model/RootStatus.kt b/app/src/main/java/app/revanced/manager/ui/model/RootStatus.kt index 58c7c667a7..59bbda6ebb 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/RootStatus.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/RootStatus.kt @@ -1,7 +1,9 @@ package app.revanced.manager.ui.model -enum class RootCheckResult { - GRANTED, - DENIED, - UNAVAILABLE +import app.revanced.manager.R + +enum class RootCheckResult(val displayName: Int) { + GRANTED(R.string.generic_active), + DENIED(R.string.generic_inactive), + UNAVAILABLE(R.string.generic_not_available) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt index 771aebab41..50ec82fda2 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt @@ -48,6 +48,7 @@ import app.revanced.manager.ui.component.settings.BooleanItem import app.revanced.manager.ui.component.settings.IntegerItem import app.revanced.manager.ui.component.settings.SafeguardBooleanItem import app.revanced.manager.ui.component.settings.SettingsListItem +import app.revanced.manager.ui.model.RootCheckResult import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel import app.revanced.manager.util.toast import app.revanced.manager.util.withHapticFeedback @@ -61,6 +62,7 @@ fun AdvancedSettingsScreen( ) { val context = LocalContext.current val resources = LocalResources.current + LaunchedEffect(Unit) { viewModel.refreshRootStatus() } val memoryLimit = remember(resources) { val activityManager = context.getSystemService()!! resources.getString( @@ -209,6 +211,7 @@ fun AdvancedSettingsScreen( it?.let(viewModel::exportDebugLogs) } val clipboard = remember { context.getSystemService()!! } + val rootStatusText = viewModel.rootStatus?.let { stringResource(it.displayName) } ?: stringResource(R.string.generic_checking) val deviceContent = """ Version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) Build type: ${BuildConfig.BUILD_TYPE} @@ -216,6 +219,7 @@ fun AdvancedSettingsScreen( Android version: ${Build.VERSION.RELEASE} (${Build.VERSION.SDK_INT}) Supported Archs: ${Build.SUPPORTED_ABIS.joinToString(", ")} Memory limit: $memoryLimit + Root: $rootStatusText """.trimIndent() SettingsListItem( headlineContent = stringResource(R.string.debug_logs_export), diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt index 420bc4e39b..ec971bf79f 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt @@ -3,12 +3,17 @@ package app.revanced.manager.ui.viewmodel import android.app.Application import android.net.Uri import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.revanced.manager.R +import app.revanced.manager.domain.installer.RootInstaller import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.DownloaderRepository import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.ui.model.RootCheckResult import app.revanced.manager.util.tag import app.revanced.manager.util.toast import com.github.pgreze.process.Redirect @@ -27,8 +32,20 @@ class AdvancedSettingsViewModel( val prefs: PreferencesManager, private val app: Application, private val patchBundleRepository: PatchBundleRepository, - private val downloaderRepository: DownloaderRepository + private val downloaderRepository: DownloaderRepository, + private val rootInstaller: RootInstaller ) : ViewModel() { + var rootStatus by mutableStateOf(null) + private set + + init { refreshRootStatus() } + + fun refreshRootStatus() { + viewModelScope.launch(Dispatchers.IO) { + rootStatus = rootInstaller.checkRootStatus() + } + } + val debugLogFileName: String get() { val time = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()) diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index 820b43dbd5..9608ef11c7 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -445,7 +445,7 @@ class PatcherViewModel( val statFs = StatFs(Environment.getDataDirectory().path) - val hasRoot = rootInstaller.hasRootAccess() + val rootStatus = context.getString(rootInstaller.checkRootStatus().displayName) val suggestedVersion = patchBundleRepository.suggestedVersions.first()[packageName] val allowIncompatiblePatches = prefs.disablePatchVersionCompatCheck.get() val disableSelectionWarning = prefs.disableSelectionWarning.get() @@ -520,7 +520,7 @@ class PatcherViewModel( addAll(managerConfiguration) addAll(patchingConfiguration) addAll(runtimeConfiguration) - add("Root permissions: ${if (hasRoot) "Yes" else "No"}") + add("Root permissions: $rootStatus") add("RAM: ${Formatter.formatFileSize(context, memInfo.availMem)} / ${Formatter.formatFileSize(context, memInfo.totalMem)} available") add("Storage: ${Formatter.formatFileSize(context, statFs.availableBytes)} / ${Formatter.formatFileSize(context, statFs.totalBytes)} available") add("Android version: ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})") diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 10d0d368ef..50353acb6e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -616,6 +616,8 @@ Tap them for more details. Restart now Active Inactive + Not available + Checking… Root access denied. Installing without root. Failed to check root access. Installing without root. From 1b014b5aa5fe0e306fd4efcba3c0b4937f1e25e6 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sun, 12 Apr 2026 16:41:14 +0100 Subject: [PATCH 17/19] enhance: Remove pollution to avoid confusion --- .../manager/domain/installer/RootInstaller.kt | 8 ++------ .../manager/ui/viewmodel/PatcherViewModel.kt | 12 ++---------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index 4db6b31aa3..d5d41b6c38 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -116,8 +116,6 @@ class RootInstaller( patchedAPK: File, stockAPK: File?, packageName: String, - version: String, - label: String ) = withContext(Dispatchers.IO) { val remoteFS = awaitRemoteFS() val patchedPackageName = withContext(Dispatchers.IO) { pm.getPackageInfo(patchedAPK)?.packageName } ?: packageName @@ -133,22 +131,20 @@ class RootInstaller( stockApp.delete() } - MagiskUtils.provisionRootFolder(remoteFS, packageName, version, label, patchedAPK) + MagiskUtils.provisionRootFolder(remoteFS, packageName, patchedAPK) } suspend fun installAsMagiskModule( patchedAPK: File, packageName: String, patchedPackageName: String, - version: String, - label: String ) = withContext(Dispatchers.IO) { if (isAppInstalledAsMagiskModule(packageName)) { uninstallMagiskModule(packageName, patchedPackageName) } else if (isAppInstalled(packageName)) { uninstall(packageName) } - MagiskUtils.provisionMagiskModule(awaitRemoteFS(), packageName, patchedPackageName, version, label, patchedAPK) + MagiskUtils.provisionMagiskModule(awaitRemoteFS(), packageName, patchedPackageName, patchedAPK) runCatching { MagiskUtils.installApk(Constants.MOUNTED_APK_PATH(packageName)) } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index 9608ef11c7..1cdf3be2d1 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -632,10 +632,6 @@ class PatcherViewModel( } InstallType.MOUNT -> { - val label = with(pm) { - currentPackageInfo.label() - } - val inputVersion = input.selectedApp.version ?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName } ?: throw Exception("Failed to determine input APK version") @@ -643,7 +639,7 @@ class PatcherViewModel( needsRootUninstall = true // Install as root rootInstaller.install( - outputFile, inputFile, packageName, inputVersion, label + outputFile, inputFile, packageName, ) val bundleInfo = patchBundleRepository.bundleInfoFlow.first() @@ -665,17 +661,13 @@ class PatcherViewModel( } InstallType.MAGISK -> { - val label = with(pm) { - currentPackageInfo.label() - } - val inputVersion = input.selectedApp.version ?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName } ?: throw Exception("Failed to determine input APK version") try { rootInstaller.installAsMagiskModule( - outputFile, packageName, currentPackageInfo.packageName, inputVersion, label + outputFile, packageName, currentPackageInfo.packageName, ) } catch (e: Exception) { packageInstallerStatus = AndroidPackageInstaller.STATUS_FAILURE From 8f8efa58bbefc528085f920e8a634d8ed6ec439c Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 14 Apr 2026 20:02:01 +0100 Subject: [PATCH 18/19] refactor: Standardize naming to "prepare" --- .../app/revanced/manager/domain/installer/RootInstaller.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index d5d41b6c38..97bc20ab62 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -131,7 +131,7 @@ class RootInstaller( stockApp.delete() } - MagiskUtils.provisionRootFolder(remoteFS, packageName, patchedAPK) + MagiskUtils.prepareRootFolder(remoteFS, packageName, patchedAPK) } suspend fun installAsMagiskModule( @@ -144,7 +144,7 @@ class RootInstaller( } else if (isAppInstalled(packageName)) { uninstall(packageName) } - MagiskUtils.provisionMagiskModule(awaitRemoteFS(), packageName, patchedPackageName, patchedAPK) + MagiskUtils.prepareMagiskModule(awaitRemoteFS(), packageName, patchedPackageName, patchedAPK) runCatching { MagiskUtils.installApk(Constants.MOUNTED_APK_PATH(packageName)) } } From 14f8a09e40eceffd82e9f74f7f65cd4d7d406aba Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sat, 18 Apr 2026 15:39:38 +0100 Subject: [PATCH 19/19] refactor: Use current user instead of static value Related to 995d1552f2244cd51d2842ebcf08b8e77db06e47 --- .../app/revanced/manager/domain/installer/RootInstaller.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index 97bc20ab62..ac852c3af2 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -28,6 +28,9 @@ class RootInstaller( ) : ServiceConnection { private var remoteFS = CompletableDeferred() + // Android user (0 for primary, 10+ for secondary/work profiles) via pure public API. + private val userId = android.os.Process.myUid() / 100000 + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { val ipc = IRootSystemService.Stub.asInterface(service) val binder = ipc.fileSystemService @@ -127,7 +130,7 @@ class RootInstaller( stockAPK?.let { stockApp -> MagiskUtils.uninstallKeepData(packageName) - MagiskUtils.installApk(stockApp.absolutePath) + execute("pm install -r -d --user $userId \"${stockApp.absolutePath}\"") stockApp.delete() } @@ -145,7 +148,7 @@ class RootInstaller( uninstall(packageName) } MagiskUtils.prepareMagiskModule(awaitRemoteFS(), packageName, patchedPackageName, patchedAPK) - runCatching { MagiskUtils.installApk(Constants.MOUNTED_APK_PATH(packageName)) } + runCatching { execute("pm install -r -d --user $userId \"${Constants.MOUNTED_APK_PATH(packageName)}\"") } } suspend fun uninstallMagiskModule(packageName: String, patchedPackageName: String) {