From 85dd550188345882ce1693b61ef9ceed29f852b6 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 14:37:57 +0100 Subject: [PATCH 01/47] feat: Magisk module installer Introduce Magisk-based installation support: add an abstract MagiskInstaller implementing install/uninstall/getInstallation logic for deploying patched APKs as Magisk modules (writes module.prop, copies patched APK to module, restarts/kills apps). Add platform-specific installers: AdbMagiskInstaller (ADB root) and LocalMagiskInstaller (local root via LocalShellCommandRunner, Closeable). Add Magisk-related constants (module paths, COPY_APK_TO_MODULE command, MAGISK_MODULE_PROP template) used by the installer. --- .../installer/LocalMagiskInstaller.kt | 32 +++++ .../installer/AdbMagiskInstaller.kt | 21 ++++ .../installation/installer/Constants.kt | 20 ++++ .../installation/installer/MagiskInstaller.kt | 109 ++++++++++++++++++ 4 files changed, 182 insertions(+) create mode 100644 library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt create mode 100644 library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbMagiskInstaller.kt create mode 100644 library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt diff --git a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt new file mode 100644 index 0000000..a6da6a3 --- /dev/null +++ b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt @@ -0,0 +1,32 @@ +package app.revanced.library.installation.installer + +import android.content.Context +import app.revanced.library.installation.command.LocalShellCommandRunner +import com.topjohnwu.superuser.ipc.RootService +import java.io.Closeable + +/** + * [LocalMagiskInstaller] for installing and uninstalling [Apk] files locally with root permissions via Magisk modules. + * + * @param context The [Context] to use for binding to the [RootService]. + * @param onReady A callback to be invoked when [LocalMagiskInstaller] is ready to be used. + * + * @throws NoRootPermissionException If the device does not have root permission. + * + * @see Installer + * @see LocalShellCommandRunner + */ +@Suppress("unused") +class LocalMagiskInstaller( + context: Context, + onReady: LocalMagiskInstaller.() -> Unit = {}, +) : MagiskInstaller( + { installer -> + LocalShellCommandRunner(context) { + (installer as LocalMagiskInstaller).onReady() + } + }, +), + Closeable { + override fun close() = (shellCommandRunner as LocalShellCommandRunner).close() +} diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbMagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbMagiskInstaller.kt new file mode 100644 index 0000000..62261f3 --- /dev/null +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbMagiskInstaller.kt @@ -0,0 +1,21 @@ +package app.revanced.library.installation.installer + +import app.revanced.library.installation.command.AdbShellCommandRunner + +/** + * [AdbMagiskInstaller] for installing and uninstalling [Apk] files using ADB root permissions via Magisk modules. + * + * @param deviceSerial The device serial. If null, the first connected device will be used. + * + * @throws NoRootPermissionException If the device does not have root permission. + * + * @see MagiskInstaller + * @see AdbShellCommandRunner + */ +class AdbMagiskInstaller( + deviceSerial: String? = null, +) : MagiskInstaller({ AdbShellCommandRunner(deviceSerial) }) { + init { + logger.fine("Connected to $deviceSerial") + } +} diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index dc2d020..dcff813 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -20,6 +20,26 @@ internal object Constants { const val CREATE_INSTALLATION_PATH = "$CREATE_DIR $MOUNT_PATH" const val GET_SDK_VERSION = "getprop ro.build.version.sdk" + const val MAGISK_MODULES_PATH = "/data/adb/modules/" + const val MAGISK_MODULE_ID = "revanced_$PLACEHOLDER" + const val MAGISK_MODULE_PATH = "$MAGISK_MODULES_PATH$MAGISK_MODULE_ID" + + const val COPY_APK_TO_MODULE = + "cp $TMP_FILE_PATH $PLACEHOLDER && " + + "chmod 644 $PLACEHOLDER && " + + "chown system:system $PLACEHOLDER && " + + "chcon $SELINUX_CONTEXT $PLACEHOLDER" + + val MAGISK_MODULE_PROP = + $$""" + id=$$MAGISK_MODULE_ID + name=ReVanced $$PLACEHOLDER + version=1.0 + versionCode=1 + author=ReVanced + description=Patched by ReVanced + """.trimIndent() + const val MOUNT_APK = "base_path=\"$MOUNTED_APK_PATH\" && " + $$"mv $$TMP_FILE_PATH $base_path && " + diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt new file mode 100644 index 0000000..a9374fb --- /dev/null +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt @@ -0,0 +1,109 @@ +package app.revanced.library.installation.installer + +import app.revanced.library.installation.command.ShellCommandRunner +import app.revanced.library.installation.installer.Constants.COPY_APK_TO_MODULE +import app.revanced.library.installation.installer.Constants.DELETE +import app.revanced.library.installation.installer.Constants.EXISTS +import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH +import app.revanced.library.installation.installer.Constants.KILL +import app.revanced.library.installation.installer.Constants.MAGISK_MODULE_PATH +import app.revanced.library.installation.installer.Constants.MAGISK_MODULE_PROP +import app.revanced.library.installation.installer.Constants.RESTART +import app.revanced.library.installation.installer.Constants.TMP_FILE_PATH +import app.revanced.library.installation.installer.Constants.invoke + +/** + * [MagiskInstaller] for installing and uninstalling [Apk] files using root permissions via Magisk modules. + * + * @param shellCommandRunnerSupplier A supplier for the [ShellCommandRunner] to use. + * + * @throws NoRootPermissionException If the device does not have root permission. + */ +@Suppress("MemberVisibilityCanBePrivate") +abstract class MagiskInstaller internal constructor( + shellCommandRunnerSupplier: (RootInstaller) -> ShellCommandRunner, +) : RootInstaller(shellCommandRunnerSupplier) { + + /** + * Installs the given [apk] as a Magisk module. + * + * @param apk The [Apk] to install. + * + * @throws PackageNameRequiredException If the [Apk] does not have a package name. + */ + override suspend fun install(apk: Apk): RootInstallerResult { + logger.info("Installing ${apk.packageName} as a Magisk module") + + val packageName = apk.packageName?.also { it.assertInstalled() } ?: throw PackageNameRequiredException() + + val sanitizedPackageName = packageName.replace('.', '_') + + // Resolve the stock APK path. + val stockApkPath = INSTALLED_APK_PATH(packageName)().output + .lineSequence() + .firstOrNull { it.startsWith("package:") } + ?.removePrefix("package:") + ?: throw FailedToFindInstalledPackageException(packageName) + + // Derive the parent directory relative to /system. + val stockApkParent = stockApkPath.substringAfter("/") + .substringBeforeLast("/") + + // Create the Magisk module directory structure. + val modulePath = MAGISK_MODULE_PATH(sanitizedPackageName) + val moduleApkDir = "$modulePath/$stockApkParent" + "mkdir -p $moduleApkDir"().waitFor() + + // Write module.prop. + apk.file.move(TMP_FILE_PATH) + "$modulePath/module.prop".write(MAGISK_MODULE_PROP(sanitizedPackageName)(packageName)) + + // Copy the patched APK into the module. + val targetApkPath = "$moduleApkDir/base.apk" + COPY_APK_TO_MODULE(targetApkPath)().waitFor() + + DELETE(TMP_FILE_PATH)() + + RESTART(packageName)() + + return RootInstallerResult.SUCCESS + } + + override suspend fun uninstall(packageName: String): RootInstallerResult { + logger.info("Uninstalling $packageName Magisk module") + + val sanitizedPackageName = packageName.replace('.', '_') + + DELETE(MAGISK_MODULE_PATH(sanitizedPackageName))() + DELETE(TMP_FILE_PATH)() + + KILL(packageName)() + + return RootInstallerResult.SUCCESS + } + + override suspend fun getInstallation(packageName: String): RootInstallation? { + val sanitizedPackageName = packageName.replace('.', '_') + val modulePath = MAGISK_MODULE_PATH(sanitizedPackageName) + + val moduleExists = EXISTS("$modulePath/module.prop")().exitCode == 0 + if (!moduleExists) return null + + return RootInstallation( + INSTALLED_APK_PATH(packageName)().output.ifEmpty { null }, + modulePath, + moduleExists, + ) + } + + /** + * Asserts that the package is installed. + * + * @throws FailedToFindInstalledPackageException If the package is not installed. + */ + private fun String.assertInstalled() { + if (INSTALLED_APK_PATH(this)().output.isEmpty()) { + throw FailedToFindInstalledPackageException(this) + } + } +} From 9ba4e3ecda31e9e63973947496e8005b4508db1b Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 15:10:20 +0100 Subject: [PATCH 02/47] fix: Ensure dex streams are closed to prevent resource leaks --- .../src/commonMain/kotlin/app/revanced/library/ApkUtils.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/ApkUtils.kt b/library/src/commonMain/kotlin/app/revanced/library/ApkUtils.kt index bea9f0f..53b0390 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/ApkUtils.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/ApkUtils.kt @@ -53,8 +53,9 @@ object ApkUtils { fun PatchesResult.applyTo(apkFile: File) { ZFile.openReadWrite(apkFile, zFileOptions).use { targetApkZFile -> dexFiles.forEach { dexFile -> - targetApkZFile.add(dexFile.name, dexFile.stream) - dexFile.stream.close() + dexFile.stream.use { stream -> + targetApkZFile.add(dexFile.name, stream) + } } resources?.let { resources -> From a283be195b02c2fcf3ae51bebc678bf8dab20769 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 17:45:01 +0100 Subject: [PATCH 03/47] feat: Migration of magisk logic from `manager` to `library` --- .../src/androidMain/assets/root/module.prop | 6 + .../src/androidMain/assets/root/service.sh | 54 ++++ .../app/revanced/library/MagiskUtils.kt | 233 ++++++++++++++++++ 3 files changed, 293 insertions(+) create mode 100644 library/src/androidMain/assets/root/module.prop create mode 100644 library/src/androidMain/assets/root/service.sh create mode 100644 library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt diff --git a/library/src/androidMain/assets/root/module.prop b/library/src/androidMain/assets/root/module.prop new file mode 100644 index 0000000..05a5a15 --- /dev/null +++ b/library/src/androidMain/assets/root/module.prop @@ -0,0 +1,6 @@ +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/library/src/androidMain/assets/root/service.sh b/library/src/androidMain/assets/root/service.sh new file mode 100644 index 0000000..f9cf156 --- /dev/null +++ b/library/src/androidMain/assets/root/service.sh @@ -0,0 +1,54 @@ +#!/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" diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt new file mode 100644 index 0000000..eb377a2 --- /dev/null +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -0,0 +1,233 @@ +package app.revanced.library + +import android.content.res.AssetManager +import com.topjohnwu.superuser.Shell +import com.topjohnwu.superuser.nio.FileSystemManager +import java.io.File +import java.util.zip.ZipFile + +object MagiskUtils { + const val MODULES_PATH = "/data/adb/modules" + + fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false + + fun isDeviceRooted() = + System.getenv("PATH")?.split(":")?.any { path -> File(path, "su").canExecute() } ?: false + + fun isInstalled(packageName: String, remoteFS: FileSystemManager) = + remoteFS.getFile("$MODULES_PATH/$packageName-revanced").exists() + + fun isInstalledAsMagiskModule(packageName: String, remoteFS: FileSystemManager) = + remoteFS.getFile("$MODULES_PATH/revanced_${packageName.replace('.', '_')}").exists() + + fun mount(packageName: String, sourceDir: String) { + val modulePath = "$MODULES_PATH/$packageName-revanced" + val patchedApk = "$modulePath/$packageName.apk" + Shell.getShell().newJob() + .add("mount -o bind \"$patchedApk\" \"$sourceDir\"") + .exec() + .assertSuccess("Failed to mount APK") + } + + fun unmount(sourceDir: String) { + Shell.getShell().newJob() + .add("umount -l \"$sourceDir\"") + .exec() + .assertSuccess("Failed to unmount APK") + } + + fun uninstall(packageName: String, remoteFS: FileSystemManager) { + remoteFS.getFile("$MODULES_PATH/$packageName-revanced").deleteRecursively() + .also { if (!it) throw Exception("Failed to delete files") } + } + + fun uninstallMagiskModule(packageName: String, remoteFS: FileSystemManager) { + val sanitizedPackageName = packageName.replace('.', '_') + remoteFS.getFile("$MODULES_PATH/revanced_$sanitizedPackageName").deleteRecursively() + .also { if (!it) throw Exception("Failed to delete Magisk module files") } + } + + fun extractNativeLibraries(apkFile: File, systemAppPath: String, remoteFS: FileSystemManager) { + val libPath = "$systemAppPath/lib" + 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) + } + } + } + } + } + + fun provisionMagiskModule( + remoteFS: FileSystemManager, + assets: AssetManager, + packageName: String, + version: String, + label: String, + patchedApk: File + ) { + val sanitizedPackageName = packageName.replace('.', '_') + val modulePath = "$MODULES_PATH/revanced_$sanitizedPackageName" + val systemAppPath = "$modulePath/system/app/$sanitizedPackageName" + + Shell.getShell().newJob() + .add("mkdir -p \"$systemAppPath\"") + .exec() + .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 { it.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) + } + } + + extractNativeLibraries(patchedApk, systemAppPath, remoteFS) + + Shell.getShell().newJob() + .add("chmod 644 \"$targetApkPath\"") + .add("chmod 755 \"$systemAppPath\"") + .add("chmod -R 755 \"$systemAppPath/lib\"") + .add("find \"$systemAppPath/lib\" -type f -name \"*.so\" -exec chmod 644 {} +") + .add("chown -R system:system \"$modulePath/system\"") + .add("chcon -R u:object_r:system_file:s0 \"$modulePath/system\"") + .add("chmod +x \"$modulePath/service.sh\"") + .exec() + .assertSuccess("Failed to set file permissions") + } + + fun provisionRootFolder( + remoteFS: FileSystemManager, + assets: AssetManager, + packageName: String, + version: String, + label: String, + patchedApk: File + ) { + val modulePath = "$MODULES_PATH/$packageName-revanced" + 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 apkPath = "$modulePath/$packageName.apk" + 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) + } + } + + Shell.getShell().newJob() + .add("chmod 644 \"$apkPath\"") + .add("chown system:system \"$apkPath\"") + .add("chcon u:object_r:apk_data_file:s0 \"$apkPath\"") + .add("chmod +x \"$modulePath/service.sh\"") + .exec() + .assertSuccess("Failed to set file permissions") + } + + 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) + } + } + } +} From b180aea68973389260d42665ce22bee89c118451 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 21:13:33 +0100 Subject: [PATCH 04/47] docs: Explain boot wait loop and sync induction behavior --- library/src/androidMain/assets/root/service.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/library/src/androidMain/assets/root/service.sh b/library/src/androidMain/assets/root/service.sh index f9cf156..18fd601 100644 --- a/library/src/androidMain/assets/root/service.sh +++ b/library/src/androidMain/assets/root/service.sh @@ -8,8 +8,11 @@ sanitized_package_name=$(echo "$package_name" | sed 's/\./_/g') rm -f "$DIR/log" { -echo "Induction check for $package_name" +# Induction check for $package_name +# This loop waits for the system to finish booting before attempting the bind-mount. +# This is required for boot-time execution (service.sh) but is not needed for +# manual/direct mounts performed while the system is already running. until [ "$(getprop sys.boot_completed)" = 1 ]; do sleep 5; done # Wait a bit more for package manager to settle sleep 10 From 62997374153e1752df0239137cb4742e0fbee163 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 21:14:38 +0100 Subject: [PATCH 05/47] refactor: Improve mount safety and path detection --- .../app/revanced/library/MagiskUtils.kt | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index eb377a2..7a3fc01 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -20,9 +20,29 @@ object MagiskUtils { fun isInstalledAsMagiskModule(packageName: String, remoteFS: FileSystemManager) = remoteFS.getFile("$MODULES_PATH/revanced_${packageName.replace('.', '_')}").exists() + /** + * Bind-mounts the patched APK over the stock APK path. + * Matches the logic in [SERVICE_SH_TEMPLATE]. + */ fun mount(packageName: String, sourceDir: String) { - val modulePath = "$MODULES_PATH/$packageName-revanced" - val patchedApk = "$modulePath/$packageName.apk" + // Induction check: verify if already mounted + val checkMount = Shell.getShell().newJob().add("mount | grep -q \"$sourceDir\"").exec() + if (checkMount.isSuccess) return + + val sanitizedPackageName = packageName.replace('.', '_') + val modulePath = "$MODULES_PATH/revanced_$sanitizedPackageName" + val fallbackModulePath = "$MODULES_PATH/$packageName-revanced" + + // Automatic detection of APK path (Magisk Induction vs Standard Root) + val patchedApkCandidates = listOf( + "$modulePath/system/app/$sanitizedPackageName/base.apk", + "$fallbackModulePath/$packageName.apk" + ) + + val patchedApk = patchedApkCandidates.firstOrNull { path -> + Shell.getShell().newJob().add("[ -f \"$path\" ]").exec().isSuccess + } ?: throw Exception("Patch APK not found for $packageName") + Shell.getShell().newJob() .add("mount -o bind \"$patchedApk\" \"$sourceDir\"") .exec() @@ -30,6 +50,10 @@ object MagiskUtils { } fun unmount(sourceDir: String) { + // Skip if not mounted + val checkMount = Shell.getShell().newJob().add("mount | grep -q \"$sourceDir\"").exec() + if (!checkMount.isSuccess) return + Shell.getShell().newJob() .add("umount -l \"$sourceDir\"") .exec() From 647e7d6803ef47e930ae4c878dc615f0192bd449 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 21:17:15 +0100 Subject: [PATCH 06/47] refactor: Better `service.sh` logging --- library/src/androidMain/assets/root/service.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/library/src/androidMain/assets/root/service.sh b/library/src/androidMain/assets/root/service.sh index 18fd601..72569ba 100644 --- a/library/src/androidMain/assets/root/service.sh +++ b/library/src/androidMain/assets/root/service.sh @@ -26,10 +26,10 @@ 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" +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." @@ -51,7 +51,7 @@ if [ "$version" != "$stock_version" ]; then # Optional: exit 1 if you want to be strict fi -echo "Mounting $base_path over $stock_path" +echo "Mounting patched APK over stock path ($base_path => $stock_path)" mount -o bind "$base_path" "$stock_path" } >> "$DIR/log" From bba8eb34ca3e4913376a17cf624144655baadcfc Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 21:20:48 +0100 Subject: [PATCH 07/47] fix: Add missing check for already overlayed app --- .../androidMain/kotlin/app/revanced/library/MagiskUtils.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index 7a3fc01..4e05ace 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -29,6 +29,10 @@ object MagiskUtils { val checkMount = Shell.getShell().newJob().add("mount | grep -q \"$sourceDir\"").exec() if (checkMount.isSuccess) return + // Induction check: verify if app is already running from system (e.g. Magisk overlay active) + val checkSystem = Shell.getShell().newJob().add("pm path \"$packageName\" | grep -q \"^package:/system/\"").exec() + if (checkSystem.isSuccess) return + val sanitizedPackageName = packageName.replace('.', '_') val modulePath = "$MODULES_PATH/revanced_$sanitizedPackageName" val fallbackModulePath = "$MODULES_PATH/$packageName-revanced" From 3e9dbc9bcd840d2feb372943e3f096f0fdd0d26d Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 21:28:33 +0100 Subject: [PATCH 08/47] refactor: Proceed with remount if already mounted/overlayed --- library/src/androidMain/assets/root/service.sh | 7 +++---- .../androidMain/kotlin/app/revanced/library/MagiskUtils.kt | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/library/src/androidMain/assets/root/service.sh b/library/src/androidMain/assets/root/service.sh index 72569ba..4a7080e 100644 --- a/library/src/androidMain/assets/root/service.sh +++ b/library/src/androidMain/assets/root/service.sh @@ -37,13 +37,12 @@ if [ -z "$stock_path" ]; then 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 + echo "App is already running from system partition (likely our Magisk overlay). Proceeding with mount." fi if mount | grep -q "$stock_path" ; then - echo "Not mounting as stock path is already mounted" - exit 1 + echo "Stock path is already mounted. Performing remount." + umount -l "$stock_path" fi if [ "$version" != "$stock_version" ]; then diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index 4e05ace..45b8245 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -25,13 +25,13 @@ object MagiskUtils { * Matches the logic in [SERVICE_SH_TEMPLATE]. */ fun mount(packageName: String, sourceDir: String) { - // Induction check: verify if already mounted + // Induction check: verify if already mounted, if so unmount to ensure clean remount val checkMount = Shell.getShell().newJob().add("mount | grep -q \"$sourceDir\"").exec() - if (checkMount.isSuccess) return + if (checkMount.isSuccess) unmount(sourceDir) // Induction check: verify if app is already running from system (e.g. Magisk overlay active) val checkSystem = Shell.getShell().newJob().add("pm path \"$packageName\" | grep -q \"^package:/system/\"").exec() - if (checkSystem.isSuccess) return + // Proceed with mount even if already in system partition val sanitizedPackageName = packageName.replace('.', '_') val modulePath = "$MODULES_PATH/revanced_$sanitizedPackageName" From 1d972f180df6ab6a344e89437e4bd5667a202ae6 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 21:32:00 +0100 Subject: [PATCH 09/47] refactor: Human readable logging --- library/src/androidMain/assets/root/service.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/androidMain/assets/root/service.sh b/library/src/androidMain/assets/root/service.sh index 4a7080e..a752d45 100644 --- a/library/src/androidMain/assets/root/service.sh +++ b/library/src/androidMain/assets/root/service.sh @@ -46,7 +46,7 @@ if mount | grep -q "$stock_path" ; then 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." + echo "The version of the installed app ($stock_version) does not match the patched app ($version). Mounting anyways, as it might still work." # Optional: exit 1 if you want to be strict fi From f6e078328132d5b062c0878e2fa6257d6f4eaebd Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 21:51:47 +0100 Subject: [PATCH 10/47] feat: Add interactive volume key support and SELinux permission enforcement --- .../src/androidMain/assets/root/service.sh | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/library/src/androidMain/assets/root/service.sh b/library/src/androidMain/assets/root/service.sh index a752d45..a288f45 100644 --- a/library/src/androidMain/assets/root/service.sh +++ b/library/src/androidMain/assets/root/service.sh @@ -3,8 +3,27 @@ DIR=${0%/*} package_name="__PKG_NAME__" version="__VERSION__" +label="__LABEL__" sanitized_package_name=$(echo "$package_name" | sed 's/\./_/g') +ReadVolumeKeys() { + local result=$(getevent -ql | while read dev type code value; do + case "$code" in + KEY_VOLUMEUP) [ "$value" = "DOWN" ] && echo 1 && break ;; + KEY_VOLUMEDOWN) [ "$value" = "DOWN" ] && echo 2 && break ;; + esac + done) + return "${result:-0}" +} + +vibrate() { + su -lp 2000 -c "cmd vibrator vibrate ${1:-500}" > /dev/null 2>&1 +} + +notify() { + su -lp 2000 -c "cmd notification post -S bigtext -t '$1' 'ReVancedInduction' '$2'" > /dev/null 2>&1 +} + rm -f "$DIR/log" { @@ -46,8 +65,30 @@ if mount | grep -q "$stock_path" ; then fi if [ "$version" != "$stock_version" ]; then - echo "The version of the installed app ($stock_version) does not match the patched app ($version). Mounting anyways, as it might still work." - # Optional: exit 1 if you want to be strict + echo "The version of the installed app ($stock_version) does not match the patched app ($version)." + + vibrate 300 + notify "Version Mismatch for $label" "Press Volume Up to mount anyway, or Volume Down to skip." + + ReadVolumeKeys + case $? in + 2) + echo "User pressed Volume Down. Skipping bind mount." + exit 0 + ;; + *) + echo "User pressed Volume Up. Proceeding with mount." + ;; + esac +fi + +echo "Setting permissions for $base_path" +chmod 644 "$base_path" +chown system:system "$base_path" +if echo "$base_path" | grep -q "/system/"; then + chcon u:object_r:system_file:s0 "$base_path" +else + chcon u:object_r:apk_data_file:s0 "$base_path" fi echo "Mounting patched APK over stock path ($base_path => $stock_path)" From fa4e8838984a5f745e8c55a9214096e39edf3d90 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 22:05:23 +0100 Subject: [PATCH 11/47] refactor: Moved service to `Constants.kt` --- .../installation/installer/Constants.kt | 175 +++++++++++++++--- 1 file changed, 148 insertions(+), 27 deletions(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index dcff813..65851bd 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -1,7 +1,7 @@ package app.revanced.library.installation.installer @Suppress("MemberVisibilityCanBePrivate") -internal object Constants { +object Constants { const val PLACEHOLDER = "PLACEHOLDER" const val SELINUX_CONTEXT = "u:object_r:apk_data_file:s0" @@ -31,9 +31,9 @@ internal object Constants { "chcon $SELINUX_CONTEXT $PLACEHOLDER" val MAGISK_MODULE_PROP = - $$""" - id=$$MAGISK_MODULE_ID - name=ReVanced $$PLACEHOLDER + """ + id=revanced_$PLACEHOLDER + name=ReVanced $PLACEHOLDER version=1.0 versionCode=1 author=ReVanced @@ -42,54 +42,175 @@ internal object Constants { const val MOUNT_APK = "base_path=\"$MOUNTED_APK_PATH\" && " + - $$"mv $$TMP_FILE_PATH $base_path && " + - $$"chmod 644 $base_path && " + - $$"chown system:system $base_path && " + - $$"chcon $$SELINUX_CONTEXT $base_path" - - const val UMOUNT = - "grep $PLACEHOLDER /proc/mounts | " + - $$"while read -r line; do echo $line | " + - "cut -d ' ' -f 2 | " + - "sed 's/apk.*/apk/' | " + - "xargs -r umount -l; done" + "mv $TMP_FILE_PATH ${"$"}{base_path} && " + + "chmod 644 ${"$"}{base_path} && " + + "chown system:system ${"$"}{base_path} && " + + "chcon $SELINUX_CONTEXT ${"$"}{base_path}" + + val UMOUNT = + """ + grep $PLACEHOLDER /proc/mounts | + while read -r line; do echo ${"$"}{line} | + cut -d ' ' -f 2 | + sed 's/apk.*/apk/' | + xargs -r umount -l; done + """.trimIndent() const val INSTALL_MOUNT_SCRIPT = "mv $TMP_FILE_PATH $MOUNT_SCRIPT_PATH && chmod +x $MOUNT_SCRIPT_PATH" val MOUNT_SCRIPT = - $$""" + """ #!/system/bin/sh - until [ "$( getprop sys.boot_completed )" = 1 ]; do sleep 3; done + until [ "${"$"}( getprop sys.boot_completed )" = 1 ]; do sleep 3; done until [ -d "/sdcard/Android" ]; do sleep 1; done - stock_path=$( pm path $$PLACEHOLDER | grep base | sed 's/package://g' ) + stock_path=${"$"}( pm path $PLACEHOLDER | grep base | sed 's/package://g' ) # Make sure the app is installed. - if [ -z "$stock_path" ]; then + if [ -z "${"$"}{stock_path}" ]; then exit 1 fi # Unmount any existing installations to prevent multiple unnecessary mounts. - $$UMOUNT + $UMOUNT - base_path="$$MOUNTED_APK_PATH" + base_path="${"$"}{MOUNTED_APK_PATH}" - chcon $$SELINUX_CONTEXT $base_path + chcon $SELINUX_CONTEXT ${"$"}{base_path} # Mount using Magisk mirror, if available. if command -v magisk >/dev/null 2>&1; then - if ! MAGISKTMP="$(magisk --path 2>/dev/null)"; then + if ! MAGISKTMP="${"$"}(magisk --path 2>/dev/null)"; then MAGISKTMP=/sbin fi - MIRROR="$MAGISKTMP/.magisk/mirror" - [ -d "$MIRROR" ] || MIRROR="" + MIRROR="${"$"}{MAGISKTMP}/.magisk/mirror" + [ -d "${"$"}{MIRROR}" ] || MIRROR="" fi - mount -o bind $MIRROR$base_path $stock_path + mount -o bind ${"$"}{MIRROR}${"$"}{base_path} ${"$"}{stock_path} # Kill the app to force it to restart the mounted APK in case it's currently running. - $$KILL + $KILL + """.trimIndent() + + /** + * Induction module property template. + * Placeholders: __PKG_NAME__, __VERSION__, __LABEL__ + */ + val INDUCTION_MODULE_PROP = + """ + id=__PKG_NAME__-ReVanced + name=__LABEL__ ReVanced + version=__VERSION__ + versionCode=0 + author=ReVanced + description=Mounts the patched APK on top of the original one + """.trimIndent() + + /** + * Induction service script template. + * Placeholders: __PKG_NAME__, __VERSION__, __LABEL__ + */ + val INDUCTION_SERVICE_SCRIPT = + """ + #!/system/bin/sh + DIR=${"$"}{0%/*} + + package_name="__PKG_NAME__" + version="__VERSION__" + label="__LABEL__" + sanitized_package_name=${"$"}(echo "${"$"}{package_name}" | sed 's/\./_/g') + + ReadVolumeKeys() { + local result=${"$"}(getevent -ql | while read dev type code value; do + case "${"$"}{code}" in + KEY_VOLUMEUP) [ "${"$"}{value}" = "DOWN" ] && echo 1 && break ;; + KEY_VOLUMEDOWN) [ "${"$"}{value}" = "DOWN" ] && echo 2 && break ;; + esac + done) + return "${"$"}{result:-0}" + } + + vibrate() { + su -lp 2000 -c "cmd vibrator vibrate ${"$"}{1:-500}" > /dev/null 2>&1 + } + + notify() { + su -lp 2000 -c "cmd notification post -S bigtext -t '${"$"}{1}' 'ReVancedInduction' '${"$"}{2}'" > /dev/null 2>&1 + } + + rm -f "${"$"}{DIR}/log" + + { + # Induction check for ${"$"}{package_name} + + # This loop waits for the system to finish booting before attempting the bind-mount. + # This is required for boot-time execution (service.sh) but is not needed for + # manual/direct mounts performed while the system is already running. + 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). Proceeding with mount." + fi + + if mount | grep -q "${"$"}{stock_path}" ; then + echo "Stock path is already mounted. Performing remount." + umount -l "${"$"}{stock_path}" + fi + + if [ "${"$"}{version}" != "${"$"}{stock_version}" ]; then + echo "The version of the installed app (${"$"}{stock_version}) does not match the patched app (${"$"}{version})." + + vibrate 300 + notify "Version Mismatch for ${"$"}{label}" "Press Volume Up to mount anyway, or Volume Down to skip." + + ReadVolumeKeys + case ${"$"}{?} in + 2) + echo "User pressed Volume Down. Skipping bind mount." + exit 0 + ;; + *) + echo "User pressed Volume Up. Proceeding with mount." + ;; + esac + fi + + echo "Setting permissions for ${"$"}{base_path}" + chmod 644 "${"$"}{base_path}" + chown system:system "${"$"}{base_path}" + if echo "${"$"}{base_path}" | grep -q "/system/"; then + chcon u:object_r:system_file:s0 "${"$"}{base_path}" + else + chcon u:object_r:apk_data_file:s0 "${"$"}{base_path}" + fi + + echo "Mounting patched APK over stock path (${"$"}{base_path} => ${"$"}{stock_path})" + mount -o bind "${"$"}{base_path}" "${"$"}{stock_path}" + + } >> "${"$"}{DIR}/log" """.trimIndent() /** From b026668b0b8f5a9f7de09e35355beb028ef4d193 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 22:05:54 +0100 Subject: [PATCH 12/47] refactor: Optimization and reuse of operations --- .../app/revanced/library/MagiskUtils.kt | 91 ++++++++----------- 1 file changed, 37 insertions(+), 54 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index 45b8245..d0bba85 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -1,6 +1,6 @@ package app.revanced.library -import android.content.res.AssetManager +import app.revanced.library.installation.installer.Constants import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.nio.FileSystemManager import java.io.File @@ -22,7 +22,7 @@ object MagiskUtils { /** * Bind-mounts the patched APK over the stock APK path. - * Matches the logic in [SERVICE_SH_TEMPLATE]. + * Matches the logic in the induction service script. */ fun mount(packageName: String, sourceDir: String) { // Induction check: verify if already mounted, if so unmount to ensure clean remount @@ -30,7 +30,7 @@ object MagiskUtils { if (checkMount.isSuccess) unmount(sourceDir) // Induction check: verify if app is already running from system (e.g. Magisk overlay active) - val checkSystem = Shell.getShell().newJob().add("pm path \"$packageName\" | grep -q \"^package:/system/\"").exec() + Shell.getShell().newJob().add("pm path \"$packageName\" | grep -q \"^package:/system/\"").exec() // Proceed with mount even if already in system partition val sanitizedPackageName = packageName.replace('.', '_') @@ -114,7 +114,6 @@ object MagiskUtils { fun provisionMagiskModule( remoteFS: FileSystemManager, - assets: AssetManager, packageName: String, version: String, label: String, @@ -129,35 +128,10 @@ object MagiskUtils { .exec() .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 { it.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) - } - } + writeInductionFiles(remoteFS, modulePath, packageName, version, label) 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) - } - } + copyApk(remoteFS, patchedApk, targetApkPath) extractNativeLibraries(patchedApk, systemAppPath, remoteFS) @@ -175,7 +149,6 @@ object MagiskUtils { fun provisionRootFolder( remoteFS: FileSystemManager, - assets: AssetManager, packageName: String, version: String, label: String, @@ -188,30 +161,10 @@ object MagiskUtils { } } - 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) - } - } - } + writeInductionFiles(remoteFS, modulePath, packageName, version, label) val apkPath = "$modulePath/$packageName.apk" - 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) - } - } + copyApk(remoteFS, patchedApk, apkPath) Shell.getShell().newJob() .add("chmod 644 \"$apkPath\"") @@ -222,6 +175,36 @@ object MagiskUtils { .assertSuccess("Failed to set file permissions") } + private fun writeInductionFiles( + remoteFS: FileSystemManager, + modulePath: String, + packageName: String, + version: String, + label: String + ) { + val moduleProp = Constants.INDUCTION_MODULE_PROP + .replace("__PKG_NAME__", packageName) + .replace("__VERSION__", version) + .replace("__LABEL__", label) + remoteFS.getFile("$modulePath/module.prop").newOutputStream().use { it.write(moduleProp.toByteArray()) } + + val serviceSh = Constants.INDUCTION_SERVICE_SCRIPT + .replace("__PKG_NAME__", packageName) + .replace("__VERSION__", version) + .replace("__LABEL__", label) + remoteFS.getFile("$modulePath/service.sh").newOutputStream().use { it.write(serviceSh.toByteArray()) } + } + + private fun copyApk(remoteFS: FileSystemManager, source: File, destination: String) { + remoteFS.getFile(source.absolutePath) + .also { if (!it.exists()) throw Exception("Source APK file doesn't exist: ${source.absolutePath}") } + .newInputStream().use { inputStream -> + remoteFS.getFile(destination).newOutputStream().use { outputStream -> + inputStream.copyTo(outputStream) + } + } + } + private fun Shell.Result.assertSuccess(errorMessage: String) { if (!isSuccess) { throw ShellCommandException(errorMessage, code, out, err) From c2d57b71f1d0700e46b024d388a407abb31c36aa Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 22:05:58 +0100 Subject: [PATCH 13/47] refactor: Removed redundant code --- .../src/androidMain/assets/root/module.prop | 6 -- .../src/androidMain/assets/root/service.sh | 97 ------------------- 2 files changed, 103 deletions(-) delete mode 100644 library/src/androidMain/assets/root/module.prop delete mode 100644 library/src/androidMain/assets/root/service.sh diff --git a/library/src/androidMain/assets/root/module.prop b/library/src/androidMain/assets/root/module.prop deleted file mode 100644 index 05a5a15..0000000 --- a/library/src/androidMain/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/library/src/androidMain/assets/root/service.sh b/library/src/androidMain/assets/root/service.sh deleted file mode 100644 index a288f45..0000000 --- a/library/src/androidMain/assets/root/service.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/system/bin/sh -DIR=${0%/*} - -package_name="__PKG_NAME__" -version="__VERSION__" -label="__LABEL__" -sanitized_package_name=$(echo "$package_name" | sed 's/\./_/g') - -ReadVolumeKeys() { - local result=$(getevent -ql | while read dev type code value; do - case "$code" in - KEY_VOLUMEUP) [ "$value" = "DOWN" ] && echo 1 && break ;; - KEY_VOLUMEDOWN) [ "$value" = "DOWN" ] && echo 2 && break ;; - esac - done) - return "${result:-0}" -} - -vibrate() { - su -lp 2000 -c "cmd vibrator vibrate ${1:-500}" > /dev/null 2>&1 -} - -notify() { - su -lp 2000 -c "cmd notification post -S bigtext -t '$1' 'ReVancedInduction' '$2'" > /dev/null 2>&1 -} - -rm -f "$DIR/log" - -{ -# Induction check for $package_name - -# This loop waits for the system to finish booting before attempting the bind-mount. -# This is required for boot-time execution (service.sh) but is not needed for -# manual/direct mounts performed while the system is already running. -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). Proceeding with mount." -fi - -if mount | grep -q "$stock_path" ; then - echo "Stock path is already mounted. Performing remount." - umount -l "$stock_path" -fi - -if [ "$version" != "$stock_version" ]; then - echo "The version of the installed app ($stock_version) does not match the patched app ($version)." - - vibrate 300 - notify "Version Mismatch for $label" "Press Volume Up to mount anyway, or Volume Down to skip." - - ReadVolumeKeys - case $? in - 2) - echo "User pressed Volume Down. Skipping bind mount." - exit 0 - ;; - *) - echo "User pressed Volume Up. Proceeding with mount." - ;; - esac -fi - -echo "Setting permissions for $base_path" -chmod 644 "$base_path" -chown system:system "$base_path" -if echo "$base_path" | grep -q "/system/"; then - chcon u:object_r:system_file:s0 "$base_path" -else - chcon u:object_r:apk_data_file:s0 "$base_path" -fi - -echo "Mounting patched APK over stock path ($base_path => $stock_path)" -mount -o bind "$base_path" "$stock_path" - -} >> "$DIR/log" From 35ab7dc3efbbbdce36e4c88495f92d575c932e28 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 22:16:47 +0100 Subject: [PATCH 14/47] refactor: Generalize induction constants and optimize installer to use `mv` --- .../library/installation/installer/Constants.kt | 7 ++----- .../library/installation/installer/MagiskInstaller.kt | 10 +++++----- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index 65851bd..819c5dd 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -24,11 +24,8 @@ object Constants { const val MAGISK_MODULE_ID = "revanced_$PLACEHOLDER" const val MAGISK_MODULE_PATH = "$MAGISK_MODULES_PATH$MAGISK_MODULE_ID" - const val COPY_APK_TO_MODULE = - "cp $TMP_FILE_PATH $PLACEHOLDER && " + - "chmod 644 $PLACEHOLDER && " + - "chown system:system $PLACEHOLDER && " + - "chcon $SELINUX_CONTEXT $PLACEHOLDER" + const val MOVE = "mv $TMP_FILE_PATH $PLACEHOLDER" + const val SET_FILE_PERMISSIONS = "chmod 644 $PLACEHOLDER && chown system:system $PLACEHOLDER && chcon $SELINUX_CONTEXT $PLACEHOLDER" val MAGISK_MODULE_PROP = """ diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt index a9374fb..eb34502 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt @@ -1,14 +1,15 @@ package app.revanced.library.installation.installer import app.revanced.library.installation.command.ShellCommandRunner -import app.revanced.library.installation.installer.Constants.COPY_APK_TO_MODULE import app.revanced.library.installation.installer.Constants.DELETE import app.revanced.library.installation.installer.Constants.EXISTS import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH import app.revanced.library.installation.installer.Constants.KILL import app.revanced.library.installation.installer.Constants.MAGISK_MODULE_PATH import app.revanced.library.installation.installer.Constants.MAGISK_MODULE_PROP +import app.revanced.library.installation.installer.Constants.MOVE import app.revanced.library.installation.installer.Constants.RESTART +import app.revanced.library.installation.installer.Constants.SET_FILE_PERMISSIONS import app.revanced.library.installation.installer.Constants.TMP_FILE_PATH import app.revanced.library.installation.installer.Constants.invoke @@ -58,11 +59,10 @@ abstract class MagiskInstaller internal constructor( apk.file.move(TMP_FILE_PATH) "$modulePath/module.prop".write(MAGISK_MODULE_PROP(sanitizedPackageName)(packageName)) - // Copy the patched APK into the module. + // Move the patched APK into the module and set permissions. val targetApkPath = "$moduleApkDir/base.apk" - COPY_APK_TO_MODULE(targetApkPath)().waitFor() - - DELETE(TMP_FILE_PATH)() + MOVE(targetApkPath)().waitFor() + SET_FILE_PERMISSIONS(targetApkPath)().waitFor() RESTART(packageName)() From 2c4a8d6824bde72afa63b5da37423e249a67dd92 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 22:25:01 +0100 Subject: [PATCH 15/47] refactor: Implement unified source-and-trigger architecture for root installations --- .../app/revanced/library/MagiskUtils.kt | 59 +++++++++++-------- .../installation/installer/Constants.kt | 22 ++++--- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index d0bba85..03085df 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -1,6 +1,7 @@ package app.revanced.library import app.revanced.library.installation.installer.Constants +import app.revanced.library.installation.installer.Constants.invoke import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.nio.FileSystemManager import java.io.File @@ -37,8 +38,9 @@ object MagiskUtils { val modulePath = "$MODULES_PATH/revanced_$sanitizedPackageName" val fallbackModulePath = "$MODULES_PATH/$packageName-revanced" - // Automatic detection of APK path (Magisk Induction vs Standard Root) + // Automatic detection of APK path (Unified Path vs Magisk Induction vs Legacy Root) val patchedApkCandidates = listOf( + Constants.MOUNTED_APK_PATH(packageName), "$modulePath/system/app/$sanitizedPackageName/base.apk", "$fallbackModulePath/$packageName.apk" ) @@ -65,11 +67,17 @@ object MagiskUtils { } fun uninstall(packageName: String, remoteFS: FileSystemManager) { + val unifiedPath = Constants.MOUNTED_APK_PATH(packageName).substringBeforeLast("/") + remoteFS.getFile(unifiedPath).deleteRecursively() + remoteFS.getFile("$MODULES_PATH/$packageName-revanced").deleteRecursively() .also { if (!it) throw Exception("Failed to delete files") } } fun uninstallMagiskModule(packageName: String, remoteFS: FileSystemManager) { + val unifiedPath = Constants.MOUNTED_APK_PATH(packageName).substringBeforeLast("/") + remoteFS.getFile(unifiedPath).deleteRecursively() + val sanitizedPackageName = packageName.replace('.', '_') remoteFS.getFile("$MODULES_PATH/revanced_$sanitizedPackageName").deleteRecursively() .also { if (!it) throw Exception("Failed to delete Magisk module files") } @@ -121,27 +129,26 @@ object MagiskUtils { ) { val sanitizedPackageName = packageName.replace('.', '_') val modulePath = "$MODULES_PATH/revanced_$sanitizedPackageName" - val systemAppPath = "$modulePath/system/app/$sanitizedPackageName" + val unifiedApkPath = Constants.MOUNTED_APK_PATH(packageName) + // Ensure directories exist + val unifiedDir = unifiedApkPath.substringBeforeLast("/") Shell.getShell().newJob() - .add("mkdir -p \"$systemAppPath\"") + .add("mkdir -p \"$modulePath\"") + .add("mkdir -p \"$unifiedDir\"") .exec() - .assertSuccess("Failed to create system app directory") + .assertSuccess("Failed to create induction directories") writeInductionFiles(remoteFS, modulePath, packageName, version, label) - val targetApkPath = "$systemAppPath/base.apk" - copyApk(remoteFS, patchedApk, targetApkPath) - - extractNativeLibraries(patchedApk, systemAppPath, remoteFS) + // Source of truth APK + copyApk(remoteFS, patchedApk, unifiedApkPath) + // Set permissions for unified path Shell.getShell().newJob() - .add("chmod 644 \"$targetApkPath\"") - .add("chmod 755 \"$systemAppPath\"") - .add("chmod -R 755 \"$systemAppPath/lib\"") - .add("find \"$systemAppPath/lib\" -type f -name \"*.so\" -exec chmod 644 {} +") - .add("chown -R system:system \"$modulePath/system\"") - .add("chcon -R u:object_r:system_file:s0 \"$modulePath/system\"") + .add("chmod 644 \"$unifiedApkPath\"") + .add("chown system:system \"$unifiedApkPath\"") + .add("chcon u:object_r:apk_data_file:s0 \"$unifiedApkPath\"") .add("chmod +x \"$modulePath/service.sh\"") .exec() .assertSuccess("Failed to set file permissions") @@ -155,21 +162,25 @@ object MagiskUtils { patchedApk: File ) { val modulePath = "$MODULES_PATH/$packageName-revanced" - remoteFS.getFile(modulePath).apply { - if (!mkdirs() && !exists()) { - throw Exception("Failed to create module directory") - } - } + val unifiedApkPath = Constants.MOUNTED_APK_PATH(packageName) + + // Ensure directories exist + val unifiedDir = unifiedApkPath.substringBeforeLast("/") + Shell.getShell().newJob() + .add("mkdir -p \"$modulePath\"") + .add("mkdir -p \"$unifiedDir\"") + .exec() + .assertSuccess("Failed to create induction directories") writeInductionFiles(remoteFS, modulePath, packageName, version, label) - val apkPath = "$modulePath/$packageName.apk" - copyApk(remoteFS, patchedApk, apkPath) + // Source of truth APK + copyApk(remoteFS, patchedApk, unifiedApkPath) Shell.getShell().newJob() - .add("chmod 644 \"$apkPath\"") - .add("chown system:system \"$apkPath\"") - .add("chcon u:object_r:apk_data_file:s0 \"$apkPath\"") + .add("chmod 644 \"$unifiedApkPath\"") + .add("chown system:system \"$unifiedApkPath\"") + .add("chcon u:object_r:apk_data_file:s0 \"$unifiedApkPath\"") .add("chmod +x \"$modulePath/service.sh\"") .exec() .assertSuccess("Failed to set file permissions") diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index 819c5dd..a421020 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -7,7 +7,7 @@ object Constants { const val SELINUX_CONTEXT = "u:object_r:apk_data_file:s0" const val TMP_FILE_PATH = "/data/local/tmp/revanced.tmp" const val MOUNT_PATH = "/data/adb/revanced/" - const val MOUNTED_APK_PATH = "$MOUNT_PATH$PLACEHOLDER.apk" + const val MOUNTED_APK_PATH = "$MOUNT_PATH$PLACEHOLDER/base.apk" const val MOUNT_SCRIPT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh" const val EXISTS = "[[ -f $PLACEHOLDER ]] || exit 1" @@ -17,7 +17,7 @@ object Constants { const val RESTART = "am start -S $PLACEHOLDER" const val KILL = "am force-stop $PLACEHOLDER" const val INSTALLED_APK_PATH = "pm path $PLACEHOLDER" - const val CREATE_INSTALLATION_PATH = "$CREATE_DIR $MOUNT_PATH" + const val CREATE_INSTALLATION_PATH = "$CREATE_DIR $MOUNT_PATH$PLACEHOLDER" const val GET_SDK_VERSION = "getprop ro.build.version.sdk" const val MAGISK_MODULES_PATH = "/data/adb/modules/" @@ -39,10 +39,11 @@ object Constants { const val MOUNT_APK = "base_path=\"$MOUNTED_APK_PATH\" && " + - "mv $TMP_FILE_PATH ${"$"}{base_path} && " + - "chmod 644 ${"$"}{base_path} && " + - "chown system:system ${"$"}{base_path} && " + - "chcon $SELINUX_CONTEXT ${"$"}{base_path}" + "mkdir -p \"${"$"}(dirname \"${"$"}{base_path}\")\" && " + + "mv $TMP_FILE_PATH \"${"$"}{base_path}\" && " + + "chmod 644 \"${"$"}{base_path}\" && " + + "chown system:system \"${"$"}{base_path}\" && " + + "chcon $SELINUX_CONTEXT \"${"$"}{base_path}\"" val UMOUNT = """ @@ -149,9 +150,14 @@ object Constants { # Wait a bit more for package manager to settle sleep 10 - base_path="${"$"}{DIR}/system/app/${"$"}{sanitized_package_name}/base.apk" + # Unified path for the patched APK (Source of truth) + base_path="/data/adb/revanced/${"$"}{package_name}/base.apk" + + # Fallback to local path if unified path doesn't exist (Legacy compatibility) + if [ ! -f "${"$"}{base_path}" ]; then + base_path="${"$"}{DIR}/system/app/${"$"}{sanitized_package_name}/base.apk" + fi if [ ! -f "${"$"}{base_path}" ]; then - # Fallback to old path for compatibility during transition base_path="${"$"}{DIR}/${"$"}{package_name}.apk" fi From 7b86534bebfaddfd0ba724d9a3185feb3949278c Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 22:47:57 +0100 Subject: [PATCH 16/47] refactor: Consolidate induction templates and synchronize Magisk installers --- .../app/revanced/library/MagiskUtils.kt | 2 +- .../installation/installer/Constants.kt | 27 +++++++------------ .../installation/installer/Installer.kt | 7 ++++- .../installation/installer/MagiskInstaller.kt | 8 +++++- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index 03085df..455cd97 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -193,7 +193,7 @@ object MagiskUtils { version: String, label: String ) { - val moduleProp = Constants.INDUCTION_MODULE_PROP + val moduleProp = Constants.MAGISK_MODULE_PROP .replace("__PKG_NAME__", packageName) .replace("__VERSION__", version) .replace("__LABEL__", label) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index a421020..10971b4 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -27,14 +27,18 @@ object Constants { const val MOVE = "mv $TMP_FILE_PATH $PLACEHOLDER" const val SET_FILE_PERMISSIONS = "chmod 644 $PLACEHOLDER && chown system:system $PLACEHOLDER && chcon $SELINUX_CONTEXT $PLACEHOLDER" + /** + * Magisk module property template. + * Placeholders: __PKG_NAME__, __VERSION__, __LABEL__ + */ val MAGISK_MODULE_PROP = """ - id=revanced_$PLACEHOLDER - name=ReVanced $PLACEHOLDER - version=1.0 - versionCode=1 + id=__PKG_NAME__-ReVanced + name=__LABEL__ ReVanced + version=__VERSION__ + versionCode=0 author=ReVanced - description=Patched by ReVanced + description=Mounts the patched APK on top of the original one """.trimIndent() const val MOUNT_APK = @@ -92,19 +96,6 @@ object Constants { $KILL """.trimIndent() - /** - * Induction module property template. - * Placeholders: __PKG_NAME__, __VERSION__, __LABEL__ - */ - val INDUCTION_MODULE_PROP = - """ - id=__PKG_NAME__-ReVanced - name=__LABEL__ ReVanced - version=__VERSION__ - versionCode=0 - author=ReVanced - description=Mounts the patched APK on top of the original one - """.trimIndent() /** * Induction service script template. diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt index da709ea..e962cbd 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt @@ -49,5 +49,10 @@ abstract class Installer interna * @param file The [Apk] file. * @param packageName The package name of the [Apk] file. */ - class Apk(val file: File, val packageName: String? = null) + class Apk( + val file: File, + val packageName: String? = null, + val version: String? = null, + val label: String? = null + ) } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt index eb34502..f05cfaf 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt @@ -57,7 +57,13 @@ abstract class MagiskInstaller internal constructor( // Write module.prop. apk.file.move(TMP_FILE_PATH) - "$modulePath/module.prop".write(MAGISK_MODULE_PROP(sanitizedPackageName)(packageName)) + + val moduleProp = MAGISK_MODULE_PROP + .replace("__PKG_NAME__", packageName) + .replace("__VERSION__", apk.version ?: "1.0") + .replace("__LABEL__", apk.label ?: packageName) + + "$modulePath/module.prop".write(moduleProp) // Move the patched APK into the module and set permissions. val targetApkPath = "$moduleApkDir/base.apk" From 08ac5dd3096f94323cc9d3e5222e4037287e18d1 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 22:55:27 +0100 Subject: [PATCH 17/47] refactor: Simplify `ShellCommandRunner` supplier and improve naming conventions --- .../kotlin/app/revanced/library/MagiskUtils.kt | 14 +++++++------- .../installation/installer/MagiskInstaller.kt | 14 +++++++------- .../installation/installer/RootInstaller.kt | 5 ++--- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index 455cd97..647a71a 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -34,14 +34,14 @@ object MagiskUtils { Shell.getShell().newJob().add("pm path \"$packageName\" | grep -q \"^package:/system/\"").exec() // Proceed with mount even if already in system partition - val sanitizedPackageName = packageName.replace('.', '_') - val modulePath = "$MODULES_PATH/revanced_$sanitizedPackageName" + val formattedPackageName = packageName.replace('.', '_') + val modulePath = "$MODULES_PATH/revanced_$formattedPackageName" val fallbackModulePath = "$MODULES_PATH/$packageName-revanced" // Automatic detection of APK path (Unified Path vs Magisk Induction vs Legacy Root) val patchedApkCandidates = listOf( Constants.MOUNTED_APK_PATH(packageName), - "$modulePath/system/app/$sanitizedPackageName/base.apk", + "$modulePath/system/app/$formattedPackageName/base.apk", "$fallbackModulePath/$packageName.apk" ) @@ -78,8 +78,8 @@ object MagiskUtils { val unifiedPath = Constants.MOUNTED_APK_PATH(packageName).substringBeforeLast("/") remoteFS.getFile(unifiedPath).deleteRecursively() - val sanitizedPackageName = packageName.replace('.', '_') - remoteFS.getFile("$MODULES_PATH/revanced_$sanitizedPackageName").deleteRecursively() + val formattedPackageName = packageName.replace('.', '_') + remoteFS.getFile("$MODULES_PATH/revanced_$formattedPackageName").deleteRecursively() .also { if (!it) throw Exception("Failed to delete Magisk module files") } } @@ -127,8 +127,8 @@ object MagiskUtils { label: String, patchedApk: File ) { - val sanitizedPackageName = packageName.replace('.', '_') - val modulePath = "$MODULES_PATH/revanced_$sanitizedPackageName" + val formattedPackageName = packageName.replace('.', '_') + val modulePath = "$MODULES_PATH/revanced_$formattedPackageName" val unifiedApkPath = Constants.MOUNTED_APK_PATH(packageName) // Ensure directories exist diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt index f05cfaf..fcc1b81 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt @@ -22,7 +22,7 @@ import app.revanced.library.installation.installer.Constants.invoke */ @Suppress("MemberVisibilityCanBePrivate") abstract class MagiskInstaller internal constructor( - shellCommandRunnerSupplier: (RootInstaller) -> ShellCommandRunner, + shellCommandRunnerSupplier: () -> ShellCommandRunner, ) : RootInstaller(shellCommandRunnerSupplier) { /** @@ -37,7 +37,7 @@ abstract class MagiskInstaller internal constructor( val packageName = apk.packageName?.also { it.assertInstalled() } ?: throw PackageNameRequiredException() - val sanitizedPackageName = packageName.replace('.', '_') + val formattedPackageName = packageName.replace('.', '_') // Resolve the stock APK path. val stockApkPath = INSTALLED_APK_PATH(packageName)().output @@ -51,7 +51,7 @@ abstract class MagiskInstaller internal constructor( .substringBeforeLast("/") // Create the Magisk module directory structure. - val modulePath = MAGISK_MODULE_PATH(sanitizedPackageName) + val modulePath = MAGISK_MODULE_PATH(formattedPackageName) val moduleApkDir = "$modulePath/$stockApkParent" "mkdir -p $moduleApkDir"().waitFor() @@ -78,9 +78,9 @@ abstract class MagiskInstaller internal constructor( override suspend fun uninstall(packageName: String): RootInstallerResult { logger.info("Uninstalling $packageName Magisk module") - val sanitizedPackageName = packageName.replace('.', '_') + val formattedPackageName = packageName.replace('.', '_') - DELETE(MAGISK_MODULE_PATH(sanitizedPackageName))() + DELETE(MAGISK_MODULE_PATH(formattedPackageName))() DELETE(TMP_FILE_PATH)() KILL(packageName)() @@ -89,8 +89,8 @@ abstract class MagiskInstaller internal constructor( } override suspend fun getInstallation(packageName: String): RootInstallation? { - val sanitizedPackageName = packageName.replace('.', '_') - val modulePath = MAGISK_MODULE_PATH(sanitizedPackageName) + val formattedPackageName = packageName.replace('.', '_') + val modulePath = MAGISK_MODULE_PATH(formattedPackageName) val moduleExists = EXISTS("$modulePath/module.prop")().exitCode == 0 if (!moduleExists) return null diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt index 27992f6..df7f0cb 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt @@ -29,14 +29,13 @@ import java.io.File */ @Suppress("MemberVisibilityCanBePrivate") abstract class RootInstaller internal constructor( - shellCommandRunnerSupplier: (RootInstaller) -> ShellCommandRunner, + shellCommandRunnerSupplier: () -> ShellCommandRunner, ) : Installer() { /** * The command runner used to run commands on the device. */ - @Suppress("LeakingThis") - protected val shellCommandRunner = shellCommandRunnerSupplier(this) + protected val shellCommandRunner = shellCommandRunnerSupplier() init { if (!shellCommandRunner.hasRootPermission()) throw NoRootPermissionException() From ed0c7a08770160bbaec62ba3846970a5140752c4 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 23:37:03 +0100 Subject: [PATCH 18/47] feat: Add Magisk module uninstall script template --- .../library/installation/installer/Constants.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index 10971b4..7e8d4f4 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -41,6 +41,20 @@ object Constants { description=Mounts the patched APK on top of the original one """.trimIndent() + /** + * Magisk module uninstall script template. Magisk runs this when the module is + * removed via the Magisk app. It cleans up the unified source-of-truth APK that + * lives outside the module directory (which Magisk itself does not know about). + * + * Placeholders: __PKG_NAME__ + */ + val MAGISK_UNINSTALL_SCRIPT = + """ + #!/system/bin/sh + package_name="__PKG_NAME__" + rm -rf "/data/adb/revanced/${"$"}{package_name}" + """.trimIndent() + const val MOUNT_APK = "base_path=\"$MOUNTED_APK_PATH\" && " + "mkdir -p \"${"$"}(dirname \"${"$"}{base_path}\")\" && " + From dda483b6e28ee150d591073c55787294e6317609 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 23:37:59 +0100 Subject: [PATCH 19/47] refactor: Align `MagiskInstaller` with unified source-and-trigger architecture --- .../installation/installer/MagiskInstaller.kt | 80 +++++++++++++------ 1 file changed, 55 insertions(+), 25 deletions(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt index fcc1b81..6cdb8e8 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt @@ -3,14 +3,17 @@ package app.revanced.library.installation.installer import app.revanced.library.installation.command.ShellCommandRunner import app.revanced.library.installation.installer.Constants.DELETE import app.revanced.library.installation.installer.Constants.EXISTS +import app.revanced.library.installation.installer.Constants.INDUCTION_SERVICE_SCRIPT import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH import app.revanced.library.installation.installer.Constants.KILL import app.revanced.library.installation.installer.Constants.MAGISK_MODULE_PATH import app.revanced.library.installation.installer.Constants.MAGISK_MODULE_PROP -import app.revanced.library.installation.installer.Constants.MOVE +import app.revanced.library.installation.installer.Constants.MAGISK_UNINSTALL_SCRIPT +import app.revanced.library.installation.installer.Constants.MOUNTED_APK_PATH +import app.revanced.library.installation.installer.Constants.MOUNT_APK import app.revanced.library.installation.installer.Constants.RESTART -import app.revanced.library.installation.installer.Constants.SET_FILE_PERMISSIONS import app.revanced.library.installation.installer.Constants.TMP_FILE_PATH +import app.revanced.library.installation.installer.Constants.UMOUNT import app.revanced.library.installation.installer.Constants.invoke /** @@ -28,6 +31,13 @@ abstract class MagiskInstaller internal constructor( /** * Installs the given [apk] as a Magisk module. * + * The patched APK is staged at the unified source-of-truth path + * `/data/adb/revanced//base.apk` (the same location used by the + * non-Magisk root installer), and the module ships a `service.sh` that + * bind-mounts that file over the stock APK on every boot. After provisioning, + * `service.sh` is executed inline so the install takes effect immediately, + * without requiring a reboot. + * * @param apk The [Apk] to install. * * @throws PackageNameRequiredException If the [Apk] does not have a package name. @@ -36,51 +46,71 @@ abstract class MagiskInstaller internal constructor( logger.info("Installing ${apk.packageName} as a Magisk module") val packageName = apk.packageName?.also { it.assertInstalled() } ?: throw PackageNameRequiredException() - val formattedPackageName = packageName.replace('.', '_') + val modulePath = MAGISK_MODULE_PATH(formattedPackageName) - // Resolve the stock APK path. - val stockApkPath = INSTALLED_APK_PATH(packageName)().output - .lineSequence() - .firstOrNull { it.startsWith("package:") } - ?.removePrefix("package:") - ?: throw FailedToFindInstalledPackageException(packageName) + // Stage the patched APK at the unified source-of-truth path. + // MOUNT_APK moves the file from TMP_FILE_PATH and applies permissions/SELinux context. + apk.file.move(TMP_FILE_PATH) + MOUNT_APK(packageName)().waitFor() - // Derive the parent directory relative to /system. - val stockApkParent = stockApkPath.substringAfter("/") - .substringBeforeLast("/") - - // Create the Magisk module directory structure. - val modulePath = MAGISK_MODULE_PATH(formattedPackageName) - val moduleApkDir = "$modulePath/$stockApkParent" - "mkdir -p $moduleApkDir"().waitFor() + // Create the Magisk module directory. + "mkdir -p $modulePath"().waitFor() // Write module.prop. - apk.file.move(TMP_FILE_PATH) - val moduleProp = MAGISK_MODULE_PROP .replace("__PKG_NAME__", packageName) .replace("__VERSION__", apk.version ?: "1.0") .replace("__LABEL__", apk.label ?: packageName) - "$modulePath/module.prop".write(moduleProp) - // Move the patched APK into the module and set permissions. - val targetApkPath = "$moduleApkDir/base.apk" - MOVE(targetApkPath)().waitFor() - SET_FILE_PERMISSIONS(targetApkPath)().waitFor() + // Write service.sh — Magisk runs this on every boot to bind-mount the patched APK. + val serviceScript = INDUCTION_SERVICE_SCRIPT + .replace("__PKG_NAME__", packageName) + .replace("__VERSION__", apk.version ?: "1.0") + .replace("__LABEL__", apk.label ?: packageName) + val serviceScriptPath = "$modulePath/service.sh" + serviceScriptPath.write(serviceScript) + "chmod +x $serviceScriptPath"().waitFor() + + // Write uninstall.sh — Magisk runs this when the module is removed via the Magisk app, + // cleaning up the unified source APK directory that lives outside the module. + val uninstallScript = MAGISK_UNINSTALL_SCRIPT.replace("__PKG_NAME__", packageName) + val uninstallScriptPath = "$modulePath/uninstall.sh" + uninstallScriptPath.write(uninstallScript) + "chmod +x $uninstallScriptPath"().waitFor() + + // Live trigger: execute service.sh now so the bind-mount is active without a reboot. + "sh $serviceScriptPath"().waitFor() RESTART(packageName)() return RootInstallerResult.SUCCESS } + /** + * Uninstalls the Magisk module for the given [packageName]. + * + * Performs an immediate live unmount and removes both the module directory and the + * unified source APK so the rollback is visible without a reboot. The module also + * ships an `uninstall.sh` for the case where the user removes it from the Magisk + * app instead of going through the installer. + */ override suspend fun uninstall(packageName: String): RootInstallerResult { logger.info("Uninstalling $packageName Magisk module") val formattedPackageName = packageName.replace('.', '_') - DELETE(MAGISK_MODULE_PATH(formattedPackageName))() + // Live unmount so the stock APK is restored immediately. + UMOUNT(packageName)() + + // Remove the Magisk module directory. + DELETE(MAGISK_MODULE_PATH(formattedPackageName))().waitFor() + + // Remove the unified source APK. + DELETE(MOUNTED_APK_PATH(packageName))().waitFor() + + // Clean up any residual tmp file. DELETE(TMP_FILE_PATH)() KILL(packageName)() From e022803d8f5a14749469ba501a5d023de3cac923 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 23:38:41 +0100 Subject: [PATCH 20/47] fix: Restore `LocalMagiskInstaller` build after supplier signature change --- .../installer/LocalMagiskInstaller.kt | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt index a6da6a3..a214bd1 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt @@ -17,16 +17,27 @@ import java.io.Closeable * @see LocalShellCommandRunner */ @Suppress("unused") -class LocalMagiskInstaller( +class LocalMagiskInstaller private constructor( context: Context, - onReady: LocalMagiskInstaller.() -> Unit = {}, + onReady: LocalMagiskInstaller.() -> Unit, + private val readyHook: Array<(() -> Unit)?>, ) : MagiskInstaller( - { installer -> - LocalShellCommandRunner(context) { - (installer as LocalMagiskInstaller).onReady() - } - }, + { LocalShellCommandRunner(context) { readyHook[0]?.invoke() } }, ), Closeable { + + constructor( + context: Context, + onReady: LocalMagiskInstaller.() -> Unit = {}, + ) : this(context, onReady, arrayOfNulls(1)) + + init { + // The supplier passed to [MagiskInstaller] runs during super-init, before `this` + // exists as a subclass reference, so the ready callback cannot capture it directly. + // Instead we route through [readyHook], which is populated here — safe because + // [LocalShellCommandRunner.onServiceConnected] fires asynchronously after IPC bind. + readyHook[0] = { onReady() } + } + override fun close() = (shellCommandRunner as LocalShellCommandRunner).close() } From 09b39f82629fe58bc621defbb207c36b8283791c Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 23:39:01 +0100 Subject: [PATCH 21/47] fix: Restore `LocalRootInstaller` build after supplier signature change --- .../installer/LocalRootInstaller.kt | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalRootInstaller.kt b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalRootInstaller.kt index 02db711..13e5710 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalRootInstaller.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalRootInstaller.kt @@ -17,16 +17,27 @@ import java.io.Closeable * @see LocalShellCommandRunner */ @Suppress("unused") -class LocalRootInstaller( +class LocalRootInstaller private constructor( context: Context, - onReady: LocalRootInstaller.() -> Unit = {}, + onReady: LocalRootInstaller.() -> Unit, + private val readyHook: Array<(() -> Unit)?>, ) : RootInstaller( - { installer -> - LocalShellCommandRunner(context) { - (installer as LocalRootInstaller).onReady() - } - }, + { LocalShellCommandRunner(context) { readyHook[0]?.invoke() } }, ), Closeable { + + constructor( + context: Context, + onReady: LocalRootInstaller.() -> Unit = {}, + ) : this(context, onReady, arrayOfNulls(1)) + + init { + // The supplier passed to [RootInstaller] runs during super-init, before `this` + // exists as a subclass reference, so the ready callback cannot capture it directly. + // Instead we route through [readyHook], which is populated here — safe because + // [LocalShellCommandRunner.onServiceConnected] fires asynchronously after IPC bind. + readyHook[0] = { onReady() } + } + override fun close() = (shellCommandRunner as LocalShellCommandRunner).close() } From 277e368cc80be95efe15eb5d4008473ba80c42d3 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 7 Apr 2026 23:41:57 +0100 Subject: [PATCH 22/47] feat: Write `uninstall.sh` when provisioning Magisk and root modules --- .../androidMain/kotlin/app/revanced/library/MagiskUtils.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index 647a71a..e43cf5f 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -150,6 +150,7 @@ object MagiskUtils { .add("chown system:system \"$unifiedApkPath\"") .add("chcon u:object_r:apk_data_file:s0 \"$unifiedApkPath\"") .add("chmod +x \"$modulePath/service.sh\"") + .add("chmod +x \"$modulePath/uninstall.sh\"") .exec() .assertSuccess("Failed to set file permissions") } @@ -182,6 +183,7 @@ object MagiskUtils { .add("chown system:system \"$unifiedApkPath\"") .add("chcon u:object_r:apk_data_file:s0 \"$unifiedApkPath\"") .add("chmod +x \"$modulePath/service.sh\"") + .add("chmod +x \"$modulePath/uninstall.sh\"") .exec() .assertSuccess("Failed to set file permissions") } @@ -204,6 +206,10 @@ object MagiskUtils { .replace("__VERSION__", version) .replace("__LABEL__", label) remoteFS.getFile("$modulePath/service.sh").newOutputStream().use { it.write(serviceSh.toByteArray()) } + + val uninstallSh = Constants.MAGISK_UNINSTALL_SCRIPT + .replace("__PKG_NAME__", packageName) + remoteFS.getFile("$modulePath/uninstall.sh").newOutputStream().use { it.write(uninstallSh.toByteArray()) } } private fun copyApk(remoteFS: FileSystemManager, source: File, destination: String) { From 754bddfa4b254867c13d87e25c03eb0bc935d006 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Thu, 9 Apr 2026 17:52:19 +0100 Subject: [PATCH 23/47] fix: Module integration and migration impl --- .../app/revanced/library/MagiskUtils.kt | 120 ++++++++++---- .../installation/installer/Constants.kt | 152 ++++++++---------- .../installation/installer/MagiskInstaller.kt | 7 +- .../installation/installer/RootInstaller.kt | 4 +- 4 files changed, 157 insertions(+), 126 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index e43cf5f..53857e3 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -10,49 +10,68 @@ import java.util.zip.ZipFile object MagiskUtils { const val MODULES_PATH = "/data/adb/modules" + /* + Shell.isAppGrantedRoot() queries libsu's internal state. + It returns false until a root shell has actually been built and verified by libsu. + */ fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false + /* + Checks whether su exists in PATH. Pure filesystem check, no shell needed. + Returns true as soon as APatch makes su available, regardless of whether your app was granted root. + */ fun isDeviceRooted() = System.getenv("PATH")?.split(":")?.any { path -> File(path, "su").canExecute() } ?: false + /* + Shell.getShell().isRoot forces a shell to be built (or returns the cached one) and checks if it came up as root. + This is the only reliable "root is usable right now" check. + */ + fun isMagiskInstalled() = Shell.getShell().isRoot + + /* + Returns true if root was granted, false if denied. + Must be called on a background thread. + */ + fun requestRoot(): Boolean { + Shell.getCachedShell()?.takeIf { !it.isRoot }?.close() + return Shell.getShell().isRoot + } + fun isInstalled(packageName: String, remoteFS: FileSystemManager) = remoteFS.getFile("$MODULES_PATH/$packageName-revanced").exists() fun isInstalledAsMagiskModule(packageName: String, remoteFS: FileSystemManager) = remoteFS.getFile("$MODULES_PATH/revanced_${packageName.replace('.', '_')}").exists() - /** - * Bind-mounts the patched APK over the stock APK path. - * Matches the logic in the induction service script. - */ + /* + Bind-mounts the patched APK over the stock APK path using the Magisk mirror when + available, ensuring the mount is visible across all process namespaces (required on + Zygisk/MIUI). + */ fun mount(packageName: String, sourceDir: String) { - // Induction check: verify if already mounted, if so unmount to ensure clean remount - val checkMount = Shell.getShell().newJob().add("mount | grep -q \"$sourceDir\"").exec() - if (checkMount.isSuccess) unmount(sourceDir) - - // Induction check: verify if app is already running from system (e.g. Magisk overlay active) - Shell.getShell().newJob().add("pm path \"$packageName\" | grep -q \"^package:/system/\"").exec() - // Proceed with mount even if already in system partition - - val formattedPackageName = packageName.replace('.', '_') - val modulePath = "$MODULES_PATH/revanced_$formattedPackageName" - val fallbackModulePath = "$MODULES_PATH/$packageName-revanced" + if (Shell.getShell().newJob().add("mount | grep -q \"$sourceDir\"").exec().isSuccess) { + unmount(sourceDir) + } - // Automatic detection of APK path (Unified Path vs Magisk Induction vs Legacy Root) - val patchedApkCandidates = listOf( - Constants.MOUNTED_APK_PATH(packageName), - "$modulePath/system/app/$formattedPackageName/base.apk", - "$fallbackModulePath/$packageName.apk" - ) + val patchedApkPath = Constants.MOUNTED_APK_PATH(packageName) + val fallbackPath = "$MODULES_PATH/$packageName-revanced/$packageName.apk" - val patchedApk = patchedApkCandidates.firstOrNull { path -> - Shell.getShell().newJob().add("[ -f \"$path\" ]").exec().isSuccess - } ?: throw Exception("Patch APK not found for $packageName") + val patchedApk = when { + Shell.getShell().newJob().add("[ -f \"$patchedApkPath\" ]").exec().isSuccess -> patchedApkPath + Shell.getShell().newJob().add("[ -f \"$fallbackPath\" ]").exec().isSuccess -> fallbackPath + else -> throw ShellCommandException("Patched APK not found for $packageName", -1, emptyList(), emptyList()) + } - Shell.getShell().newJob() - .add("mount -o bind \"$patchedApk\" \"$sourceDir\"") - .exec() - .assertSuccess("Failed to mount APK") + Shell.getShell().newJob().add(""" + MIRROR="" + if command -v magisk >/dev/null 2>&1; then + if ! MAGISKTMP=${"$"}(magisk --path 2>/dev/null); then MAGISKTMP=/sbin; fi + MIRROR=${"$"}{MAGISKTMP}/.magisk/mirror + [ -d "${"$"}{MIRROR}" ] || MIRROR="" + fi + mount -o bind "${"$"}{MIRROR}$patchedApk" "$sourceDir" + """.trimIndent()).exec().assertSuccess("Failed to mount APK") } fun unmount(sourceDir: String) { @@ -74,11 +93,18 @@ object MagiskUtils { .also { if (!it) throw Exception("Failed to delete files") } } - fun uninstallMagiskModule(packageName: String, remoteFS: FileSystemManager) { + fun uninstallMagiskModule(packageName: String, patchedPackageName: String, remoteFS: FileSystemManager) { val unifiedPath = Constants.MOUNTED_APK_PATH(packageName).substringBeforeLast("/") remoteFS.getFile(unifiedPath).deleteRecursively() val formattedPackageName = packageName.replace('.', '_') + val guardScriptPath = Constants.GUARD_SCRIPT_PATH(formattedPackageName) + + Shell.getShell().newJob() + .add("pm uninstall --user 0 \"$patchedPackageName\"") + .add("rm -f \"$guardScriptPath\"") + .exec() + remoteFS.getFile("$MODULES_PATH/revanced_$formattedPackageName").deleteRecursively() .also { if (!it) throw Exception("Failed to delete Magisk module files") } } @@ -120,9 +146,21 @@ object MagiskUtils { } } + fun installApk(apkPath: String) = + Shell.getShell().newJob() + .add("pm install -r -d --user 0 \"$apkPath\"") + .exec() + .assertSuccess("Failed to install APK: $apkPath") + + fun uninstallKeepData(packageName: String) = + Shell.getShell().newJob() + .add("pm uninstall -k --user 0 $packageName") + .exec() + fun provisionMagiskModule( remoteFS: FileSystemManager, packageName: String, + patchedPackageName: String, version: String, label: String, patchedApk: File @@ -130,6 +168,7 @@ object MagiskUtils { val formattedPackageName = packageName.replace('.', '_') val modulePath = "$MODULES_PATH/revanced_$formattedPackageName" val unifiedApkPath = Constants.MOUNTED_APK_PATH(packageName) + val guardScriptPath = Constants.GUARD_SCRIPT_PATH(formattedPackageName) // Ensure directories exist val unifiedDir = unifiedApkPath.substringBeforeLast("/") @@ -139,7 +178,13 @@ object MagiskUtils { .exec() .assertSuccess("Failed to create induction directories") - writeInductionFiles(remoteFS, modulePath, packageName, version, label) + writeInductionFiles(remoteFS, modulePath, packageName, patchedPackageName, version, label) + + // Guard script: uninstalls the patched app when the module is disabled or removed. + val guardSh = Constants.GUARD_SCRIPT + .replace("__PATCHED_PKG__", patchedPackageName) + .replace("__FORMATTED_PKG__", formattedPackageName) + remoteFS.getFile(guardScriptPath).newOutputStream().use { it.write(guardSh.toByteArray()) } // Source of truth APK copyApk(remoteFS, patchedApk, unifiedApkPath) @@ -151,6 +196,7 @@ object MagiskUtils { .add("chcon u:object_r:apk_data_file:s0 \"$unifiedApkPath\"") .add("chmod +x \"$modulePath/service.sh\"") .add("chmod +x \"$modulePath/uninstall.sh\"") + .add("chmod +x \"$guardScriptPath\"") .exec() .assertSuccess("Failed to set file permissions") } @@ -173,7 +219,8 @@ object MagiskUtils { .exec() .assertSuccess("Failed to create induction directories") - writeInductionFiles(remoteFS, modulePath, packageName, version, label) + // MOUNT type: patched package name == original package name (bind-mount, no rename) + writeInductionFiles(remoteFS, modulePath, packageName, packageName, version, label) // Source of truth APK copyApk(remoteFS, patchedApk, unifiedApkPath) @@ -192,23 +239,28 @@ object MagiskUtils { remoteFS: FileSystemManager, modulePath: String, packageName: String, + patchedPackageName: String, version: String, label: String ) { + val formattedPackageName = packageName.replace('.', '_') + val moduleProp = Constants.MAGISK_MODULE_PROP - .replace("__PKG_NAME__", packageName) + .replace("__FORMATTED_PKG__", formattedPackageName) .replace("__VERSION__", version) .replace("__LABEL__", label) remoteFS.getFile("$modulePath/module.prop").newOutputStream().use { it.write(moduleProp.toByteArray()) } val serviceSh = Constants.INDUCTION_SERVICE_SCRIPT .replace("__PKG_NAME__", packageName) + .replace("__PATCHED_PKG__", patchedPackageName) .replace("__VERSION__", version) - .replace("__LABEL__", label) remoteFS.getFile("$modulePath/service.sh").newOutputStream().use { it.write(serviceSh.toByteArray()) } - + val uninstallSh = Constants.MAGISK_UNINSTALL_SCRIPT .replace("__PKG_NAME__", packageName) + .replace("__PATCHED_PKG__", patchedPackageName) + .replace("__FORMATTED_PKG__", formattedPackageName) remoteFS.getFile("$modulePath/uninstall.sh").newOutputStream().use { it.write(uninstallSh.toByteArray()) } } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index 7e8d4f4..631ee00 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -29,11 +29,14 @@ object Constants { /** * Magisk module property template. - * Placeholders: __PKG_NAME__, __VERSION__, __LABEL__ + * The id MUST match the module directory name (revanced___FORMATTED_PKG__) so that + * Magisk/APatch/KernelSU can find the module by id for enable/disable operations. + * + * Placeholders: __FORMATTED_PKG__ (original with dots→underscores), __VERSION__, __LABEL__ */ val MAGISK_MODULE_PROP = """ - id=__PKG_NAME__-ReVanced + id=revanced___FORMATTED_PKG__ name=__LABEL__ ReVanced version=__VERSION__ versionCode=0 @@ -43,16 +46,50 @@ object Constants { /** * Magisk module uninstall script template. Magisk runs this when the module is - * removed via the Magisk app. It cleans up the unified source-of-truth APK that - * lives outside the module directory (which Magisk itself does not know about). + * removed via the Magisk app. It cleans up the unified source-of-truth APK and + * the boot-time guard script. * - * Placeholders: __PKG_NAME__ + * Placeholders: __PKG_NAME__ (original), __PATCHED_PKG__ (patched), __FORMATTED_PKG__ (original with dots→underscores) */ val MAGISK_UNINSTALL_SCRIPT = """ #!/system/bin/sh - package_name="__PKG_NAME__" - rm -rf "/data/adb/revanced/${"$"}{package_name}" + pm uninstall --user 0 "__PATCHED_PKG__" + rm -rf "/data/adb/revanced/__PKG_NAME__" + rm -f "/data/adb/service.d/revanced_guard___FORMATTED_PKG__.sh" + """.trimIndent() + + const val GUARD_SCRIPT_PATH = "/data/adb/service.d/revanced_guard_$PLACEHOLDER.sh" + + /** + * Boot-time guard script. Runs on every boot (via service.d, independent of module state). + * Uninstalls the patched app when the module is disabled or removed, so the app + * disappears when the module is toggled off. + * + * Placeholders: __PATCHED_PKG__ (patched), __FORMATTED_PKG__ (original with dots→underscores) + */ + val GUARD_SCRIPT = + """ + #!/system/bin/sh + patched_pkg="__PATCHED_PKG__" + module_path="/data/adb/modules/revanced___FORMATTED_PKG__" + + until [ "${"$"}(getprop sys.boot_completed)" = 1 ]; do sleep 5; done + sleep 11 + + # Module was fully removed — uninstall app and self-destruct this script. + if [ ! -d "${"$"}{module_path}" ]; then + pm uninstall --user 0 "${"$"}{patched_pkg}" 2>/dev/null + rm -f "$0" + exit 0 + fi + + # If service.sh did not run this boot, the module is disabled — uninstall the app. + current_boot_id=${"$"}(cat /proc/sys/kernel/random/boot_id 2>/dev/null) + stored_boot_id=${"$"}(cat "${"$"}{module_path}/.boot_token" 2>/dev/null) + if [ "${"$"}{stored_boot_id}" != "${"$"}{current_boot_id}" ]; then + pm uninstall --user 0 "${"$"}{patched_pkg}" 2>/dev/null + fi """.trimIndent() const val MOUNT_APK = @@ -112,112 +149,53 @@ object Constants { /** - * Induction service script template. - * Placeholders: __PKG_NAME__, __VERSION__, __LABEL__ + * Magisk module service script template. Runs on every boot when the module is enabled. + * Installs the patched APK if not already installed. + * + * Placeholders: __PKG_NAME__ (original, used for APK path), __PATCHED_PKG__ (patched, used for pm commands), __VERSION__ */ val INDUCTION_SERVICE_SCRIPT = """ #!/system/bin/sh DIR=${"$"}{0%/*} - package_name="__PKG_NAME__" + package_name="__PATCHED_PKG__" version="__VERSION__" - label="__LABEL__" - sanitized_package_name=${"$"}(echo "${"$"}{package_name}" | sed 's/\./_/g') - - ReadVolumeKeys() { - local result=${"$"}(getevent -ql | while read dev type code value; do - case "${"$"}{code}" in - KEY_VOLUMEUP) [ "${"$"}{value}" = "DOWN" ] && echo 1 && break ;; - KEY_VOLUMEDOWN) [ "${"$"}{value}" = "DOWN" ] && echo 2 && break ;; - esac - done) - return "${"$"}{result:-0}" - } - - vibrate() { - su -lp 2000 -c "cmd vibrator vibrate ${"$"}{1:-500}" > /dev/null 2>&1 - } - - notify() { - su -lp 2000 -c "cmd notification post -S bigtext -t '${"$"}{1}' 'ReVancedInduction' '${"$"}{2}'" > /dev/null 2>&1 - } rm -f "${"$"}{DIR}/log" + # Write a boot token so the guard script can detect whether service.sh ran this boot. + cp /proc/sys/kernel/random/boot_id "${"$"}{DIR}/.boot_token" + { - # Induction check for ${"$"}{package_name} - # This loop waits for the system to finish booting before attempting the bind-mount. - # This is required for boot-time execution (service.sh) but is not needed for - # manual/direct mounts performed while the system is already running. until [ "${"$"}(getprop sys.boot_completed)" = 1 ]; do sleep 5; done - # Wait a bit more for package manager to settle sleep 10 - # Unified path for the patched APK (Source of truth) - base_path="/data/adb/revanced/${"$"}{package_name}/base.apk" - - # Fallback to local path if unified path doesn't exist (Legacy compatibility) - if [ ! -f "${"$"}{base_path}" ]; then - base_path="${"$"}{DIR}/system/app/${"$"}{sanitized_package_name}/base.apk" - fi + base_path="/data/adb/revanced/__PKG_NAME__/base.apk" + + # Fallback for legacy compatibility. if [ ! -f "${"$"}{base_path}" ]; then - base_path="${"$"}{DIR}/${"$"}{package_name}.apk" + base_path="${"$"}{DIR}/__PKG_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). Proceeding with mount." - fi - - if mount | grep -q "${"$"}{stock_path}" ; then - echo "Stock path is already mounted. Performing remount." - umount -l "${"$"}{stock_path}" - fi - - if [ "${"$"}{version}" != "${"$"}{stock_version}" ]; then - echo "The version of the installed app (${"$"}{stock_version}) does not match the patched app (${"$"}{version})." - - vibrate 300 - notify "Version Mismatch for ${"$"}{label}" "Press Volume Up to mount anyway, or Volume Down to skip." - - ReadVolumeKeys - case ${"$"}{?} in - 2) - echo "User pressed Volume Down. Skipping bind mount." - exit 0 - ;; - *) - echo "User pressed Volume Up. Proceeding with mount." - ;; - esac + if [ ! -f "${"$"}{base_path}" ]; then + echo "Patched APK not found." + exit 1 fi - echo "Setting permissions for ${"$"}{base_path}" - chmod 644 "${"$"}{base_path}" - chown system:system "${"$"}{base_path}" - if echo "${"$"}{base_path}" | grep -q "/system/"; then - chcon u:object_r:system_file:s0 "${"$"}{base_path}" + # Skip install if the app is already present (pm install persists across reboots). + if pm list packages --user 0 | grep -q "^package:${"$"}{package_name}$"; then + echo "Package already installed, skipping." else - chcon u:object_r:apk_data_file:s0 "${"$"}{base_path}" + echo "Installing ${"$"}{base_path}" + pm install -r -d --user 0 "${"$"}{base_path}" + echo "Install exit code: $?" fi - echo "Mounting patched APK over stock path (${"$"}{base_path} => ${"$"}{stock_path})" - mount -o bind "${"$"}{base_path}" "${"$"}{stock_path}" - } >> "${"$"}{DIR}/log" """.trimIndent() diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt index 6cdb8e8..3cb9f1e 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt @@ -11,6 +11,7 @@ import app.revanced.library.installation.installer.Constants.MAGISK_MODULE_PROP import app.revanced.library.installation.installer.Constants.MAGISK_UNINSTALL_SCRIPT import app.revanced.library.installation.installer.Constants.MOUNTED_APK_PATH import app.revanced.library.installation.installer.Constants.MOUNT_APK +import app.revanced.library.installation.installer.Constants.MOUNT_GREP import app.revanced.library.installation.installer.Constants.RESTART import app.revanced.library.installation.installer.Constants.TMP_FILE_PATH import app.revanced.library.installation.installer.Constants.UMOUNT @@ -68,7 +69,6 @@ abstract class MagiskInstaller internal constructor( val serviceScript = INDUCTION_SERVICE_SCRIPT .replace("__PKG_NAME__", packageName) .replace("__VERSION__", apk.version ?: "1.0") - .replace("__LABEL__", apk.label ?: packageName) val serviceScriptPath = "$modulePath/service.sh" serviceScriptPath.write(serviceScript) "chmod +x $serviceScriptPath"().waitFor() @@ -125,10 +125,11 @@ abstract class MagiskInstaller internal constructor( val moduleExists = EXISTS("$modulePath/module.prop")().exitCode == 0 if (!moduleExists) return null + val patchedApkPath = MOUNTED_APK_PATH(packageName) return RootInstallation( INSTALLED_APK_PATH(packageName)().output.ifEmpty { null }, - modulePath, - moduleExists, + patchedApkPath, + MOUNT_GREP(patchedApkPath)().exitCode == 0, ) } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt index df7f0cb..aad9d31 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt @@ -55,7 +55,7 @@ abstract class RootInstaller internal constructor( // Setup files. apk.file.move(TMP_FILE_PATH) - CREATE_INSTALLATION_PATH().waitFor() + CREATE_INSTALLATION_PATH(packageName)().waitFor() MOUNT_APK(packageName)().waitFor() // Install and run. @@ -87,7 +87,7 @@ abstract class RootInstaller internal constructor( val patchedApkPath = MOUNTED_APK_PATH(packageName) val patchedApkExists = EXISTS(patchedApkPath)().exitCode == 0 - if (patchedApkExists) return null + if (!patchedApkExists) return null return RootInstallation( INSTALLED_APK_PATH(packageName)().output.ifEmpty { null }, From 2e64ffdd1d1ad0158c9f25123d8a0d77c278220d Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Thu, 9 Apr 2026 22:08:21 +0100 Subject: [PATCH 24/47] feat: Better module logging --- .../installation/installer/Constants.kt | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index 631ee00..0e09dad 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -162,15 +162,26 @@ object Constants { package_name="__PATCHED_PKG__" version="__VERSION__" - rm -f "${"$"}{DIR}/log" - # Write a boot token so the guard script can detect whether service.sh ran this boot. cp /proc/sys/kernel/random/boot_id "${"$"}{DIR}/.boot_token" + LOG="${"$"}{DIR}/log" + MAX_LOG_LINES=200 + + # Trim log to last MAX_LOG_LINES lines to prevent unbounded growth. + if [ -f "${"$"}{LOG}" ]; then + tail -n "${"$"}{MAX_LOG_LINES}" "${"$"}{LOG}" > "${"$"}{LOG}.tmp" && mv "${"$"}{LOG}.tmp" "${"$"}{LOG}" + fi + { + echo "--- ${"$"}(date '+%Y-%m-%d %H:%M:%S') | pkg=${"$"}{package_name} | ver=${"$"}{version} ---" + until [ "${"$"}(getprop sys.boot_completed)" = 1 ]; do sleep 5; done - sleep 10 + + # Wait for the package manager service to be registered and ready for transactions. + until service check package 2>/dev/null | grep -q "found"; do sleep 3; done + sleep 5 base_path="/data/adb/revanced/__PKG_NAME__/base.apk" @@ -191,12 +202,19 @@ object Constants { if pm list packages --user 0 | grep -q "^package:${"$"}{package_name}$"; then echo "Package already installed, skipping." else - echo "Installing ${"$"}{base_path}" - pm install -r -d --user 0 "${"$"}{base_path}" - echo "Install exit code: $?" + attempt=0 + while [ ${"$"}{attempt} -lt 3 ]; do + attempt=${"$"}((attempt + 1)) + echo "Install attempt ${"$"}{attempt}/3" + pm install -r -d --user 0 "${"$"}{base_path}" + install_exit=$? + echo "Install exit code: ${"$"}{install_exit}" + [ ${"$"}{install_exit} -eq 0 ] && break + [ ${"$"}{attempt} -lt 3 ] && { echo "Retrying in 10s..."; sleep 10; } + done fi - } >> "${"$"}{DIR}/log" + } >> "${"$"}{LOG}" """.trimIndent() /** From e4c06cbab9fc180e719ab9e47232c617af5eeea8 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sun, 12 Apr 2026 15:20:11 +0000 Subject: [PATCH 25/47] Update library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt Clearler command title Co-authored-by: oSumAtrIX --- .../app/revanced/library/installation/installer/Constants.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index 0e09dad..9639ec4 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -25,7 +25,7 @@ object Constants { const val MAGISK_MODULE_PATH = "$MAGISK_MODULES_PATH$MAGISK_MODULE_ID" const val MOVE = "mv $TMP_FILE_PATH $PLACEHOLDER" - const val SET_FILE_PERMISSIONS = "chmod 644 $PLACEHOLDER && chown system:system $PLACEHOLDER && chcon $SELINUX_CONTEXT $PLACEHOLDER" + const val SET_MOUNTING_PERMISSIONS = "chmod 644 $PLACEHOLDER && chown system:system $PLACEHOLDER && chcon $SELINUX_CONTEXT $PLACEHOLDER" /** * Magisk module property template. From c229f11afeca0d2bf66013f4cf54e3a9a2182646 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sun, 12 Apr 2026 16:55:53 +0100 Subject: [PATCH 26/47] refactor: Rename confusing and unused vars --- .../installation/installer/Installer.kt | 3 -- .../installation/installer/MagiskInstaller.kt | 51 ++++++------------- .../installation/installer/RootInstaller.kt | 28 ++++++---- 3 files changed, 33 insertions(+), 49 deletions(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt index e962cbd..cc69117 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt @@ -1,6 +1,5 @@ package app.revanced.library.installation.installer -import app.revanced.library.installation.installer.Installer.Apk import java.io.File import java.util.logging.Logger @@ -52,7 +51,5 @@ abstract class Installer interna class Apk( val file: File, val packageName: String? = null, - val version: String? = null, - val label: String? = null ) } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt index 3cb9f1e..00b574d 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt @@ -3,18 +3,20 @@ package app.revanced.library.installation.installer import app.revanced.library.installation.command.ShellCommandRunner import app.revanced.library.installation.installer.Constants.DELETE import app.revanced.library.installation.installer.Constants.EXISTS -import app.revanced.library.installation.installer.Constants.INDUCTION_SERVICE_SCRIPT import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH import app.revanced.library.installation.installer.Constants.KILL import app.revanced.library.installation.installer.Constants.MAGISK_MODULE_PATH import app.revanced.library.installation.installer.Constants.MAGISK_MODULE_PROP import app.revanced.library.installation.installer.Constants.MAGISK_UNINSTALL_SCRIPT +import app.revanced.library.installation.installer.Constants.MODULE_PROP_FILE +import app.revanced.library.installation.installer.Constants.MODULE_SERVICE_SCRIPT import app.revanced.library.installation.installer.Constants.MOUNTED_APK_PATH -import app.revanced.library.installation.installer.Constants.MOUNT_APK import app.revanced.library.installation.installer.Constants.MOUNT_GREP import app.revanced.library.installation.installer.Constants.RESTART +import app.revanced.library.installation.installer.Constants.SERVICE_SCRIPT_FILE import app.revanced.library.installation.installer.Constants.TMP_FILE_PATH import app.revanced.library.installation.installer.Constants.UMOUNT +import app.revanced.library.installation.installer.Constants.UNINSTALL_SCRIPT_FILE import app.revanced.library.installation.installer.Constants.invoke /** @@ -35,7 +37,7 @@ abstract class MagiskInstaller internal constructor( * The patched APK is staged at the unified source-of-truth path * `/data/adb/revanced//base.apk` (the same location used by the * non-Magisk root installer), and the module ships a `service.sh` that - * bind-mounts that file over the stock APK on every boot. After provisioning, + * installs the patched APK on every boot. After provisioning, * `service.sh` is executed inline so the install takes effect immediately, * without requiring a reboot. * @@ -51,36 +53,28 @@ abstract class MagiskInstaller internal constructor( val modulePath = MAGISK_MODULE_PATH(formattedPackageName) // Stage the patched APK at the unified source-of-truth path. - // MOUNT_APK moves the file from TMP_FILE_PATH and applies permissions/SELinux context. apk.file.move(TMP_FILE_PATH) - MOUNT_APK(packageName)().waitFor() + stageApk(packageName) // Create the Magisk module directory. "mkdir -p $modulePath"().waitFor() // Write module.prop. val moduleProp = MAGISK_MODULE_PROP + .replace("__FORMATTED_PKG__", formattedPackageName) .replace("__PKG_NAME__", packageName) - .replace("__VERSION__", apk.version ?: "1.0") - .replace("__LABEL__", apk.label ?: packageName) - "$modulePath/module.prop".write(moduleProp) + "$modulePath/$MODULE_PROP_FILE".write(moduleProp) - // Write service.sh — Magisk runs this on every boot to bind-mount the patched APK. - val serviceScript = INDUCTION_SERVICE_SCRIPT - .replace("__PKG_NAME__", packageName) - .replace("__VERSION__", apk.version ?: "1.0") - val serviceScriptPath = "$modulePath/service.sh" - serviceScriptPath.write(serviceScript) + // Write service.sh — Magisk runs this on every boot to install the patched APK. + val serviceScriptPath = "$modulePath/$SERVICE_SCRIPT_FILE" + serviceScriptPath.write(MODULE_SERVICE_SCRIPT.replace("__PKG_NAME__", packageName)) "chmod +x $serviceScriptPath"().waitFor() - // Write uninstall.sh — Magisk runs this when the module is removed via the Magisk app, - // cleaning up the unified source APK directory that lives outside the module. - val uninstallScript = MAGISK_UNINSTALL_SCRIPT.replace("__PKG_NAME__", packageName) - val uninstallScriptPath = "$modulePath/uninstall.sh" - uninstallScriptPath.write(uninstallScript) - "chmod +x $uninstallScriptPath"().waitFor() + // Write uninstall.sh — Magisk runs this when the module is removed via the Magisk app. + "$modulePath/$UNINSTALL_SCRIPT_FILE".write(MAGISK_UNINSTALL_SCRIPT.replace("__PKG_NAME__", packageName)) + "chmod +x $modulePath/$UNINSTALL_SCRIPT_FILE"() - // Live trigger: execute service.sh now so the bind-mount is active without a reboot. + // Live trigger: execute service.sh now so the install takes effect without a reboot. "sh $serviceScriptPath"().waitFor() RESTART(packageName)() @@ -92,9 +86,7 @@ abstract class MagiskInstaller internal constructor( * Uninstalls the Magisk module for the given [packageName]. * * Performs an immediate live unmount and removes both the module directory and the - * unified source APK so the rollback is visible without a reboot. The module also - * ships an `uninstall.sh` for the case where the user removes it from the Magisk - * app instead of going through the installer. + * unified source APK so the rollback is visible without a reboot. */ override suspend fun uninstall(packageName: String): RootInstallerResult { logger.info("Uninstalling $packageName Magisk module") @@ -132,15 +124,4 @@ abstract class MagiskInstaller internal constructor( MOUNT_GREP(patchedApkPath)().exitCode == 0, ) } - - /** - * Asserts that the package is installed. - * - * @throws FailedToFindInstalledPackageException If the package is not installed. - */ - private fun String.assertInstalled() { - if (INSTALLED_APK_PATH(this)().output.isEmpty()) { - throw FailedToFindInstalledPackageException(this) - } - } } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt index aad9d31..ce95d0d 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt @@ -8,16 +8,15 @@ import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH import app.revanced.library.installation.installer.Constants.INSTALL_MOUNT_SCRIPT import app.revanced.library.installation.installer.Constants.KILL import app.revanced.library.installation.installer.Constants.MOUNTED_APK_PATH -import app.revanced.library.installation.installer.Constants.MOUNT_APK +import app.revanced.library.installation.installer.Constants.MOUNTED_APK_PATH_LEGACY import app.revanced.library.installation.installer.Constants.MOUNT_GREP +import app.revanced.library.installation.installer.Constants.STAGE_APK import app.revanced.library.installation.installer.Constants.MOUNT_SCRIPT import app.revanced.library.installation.installer.Constants.MOUNT_SCRIPT_PATH import app.revanced.library.installation.installer.Constants.RESTART import app.revanced.library.installation.installer.Constants.TMP_FILE_PATH import app.revanced.library.installation.installer.Constants.UMOUNT import app.revanced.library.installation.installer.Constants.invoke -import app.revanced.library.installation.installer.Installer.Apk -import app.revanced.library.installation.installer.RootInstaller.NoRootPermissionException import java.io.File /** @@ -41,6 +40,12 @@ abstract class RootInstaller internal constructor( if (!shellCommandRunner.hasRootPermission()) throw NoRootPermissionException() } + /** + * Stages the APK from [TMP_FILE_PATH] to the unified source-of-truth path for [packageName], + * creating the directory and applying permissions/SELinux context. + */ + protected fun stageApk(packageName: String) = STAGE_APK(packageName)().waitFor() + /** * Installs the given [apk] by mounting. * @@ -56,7 +61,7 @@ abstract class RootInstaller internal constructor( // Setup files. apk.file.move(TMP_FILE_PATH) CREATE_INSTALLATION_PATH(packageName)().waitFor() - MOUNT_APK(packageName)().waitFor() + stageApk(packageName) // Install and run. TMP_FILE_PATH.write(MOUNT_SCRIPT(packageName)) @@ -74,8 +79,9 @@ abstract class RootInstaller internal constructor( UMOUNT(packageName)() - DELETE(MOUNTED_APK_PATH)(packageName)() - DELETE(MOUNT_SCRIPT_PATH)(packageName)() + DELETE(MOUNTED_APK_PATH(packageName))() + DELETE(MOUNTED_APK_PATH_LEGACY(packageName))() // Remove legacy flat-file path if present. + DELETE(MOUNT_SCRIPT_PATH(packageName))() DELETE(TMP_FILE_PATH)() // Remove residual. KILL(packageName)() @@ -84,10 +90,10 @@ abstract class RootInstaller internal constructor( } override suspend fun getInstallation(packageName: String): RootInstallation? { - val patchedApkPath = MOUNTED_APK_PATH(packageName) - - val patchedApkExists = EXISTS(patchedApkPath)().exitCode == 0 - if (!patchedApkExists) return null + // Check current path first, fall back to legacy flat-file path for existing installations + val patchedApkPath = MOUNTED_APK_PATH(packageName).takeIf { EXISTS(it)().exitCode == 0 } + ?: MOUNTED_APK_PATH_LEGACY(packageName).takeIf { EXISTS(it)().exitCode == 0 } + ?: return null return RootInstallation( INSTALLED_APK_PATH(packageName)().output.ifEmpty { null }, @@ -120,7 +126,7 @@ abstract class RootInstaller internal constructor( * * @throws FailedToFindInstalledPackageException If the package is not installed. */ - private fun String.assertInstalled() { + protected fun String.assertInstalled() { if (INSTALLED_APK_PATH(this)().output.isEmpty()) { throw FailedToFindInstalledPackageException(this) } From 3280af08894b21b1712d89d0787a4323c0fcd408 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sun, 12 Apr 2026 17:48:41 +0100 Subject: [PATCH 27/47] Refactor Magisk provisioning and watchdog Rename guard scripts to watchdog and update provisioning flow. MagiskUtils now references WATCHDOG_SCRIPT_PATH and writeModuleFiles (formerly writeInductionFiles), removing version/label parameters from provisioning APIs. Constants: add legacy mounted path, watchdog path, module filenames, STAGE_APK and INSTALL_MOUNT_SCRIPT templates; simplify module.prop fields. Shell templates improved for robustness: safer quoting/variable expansion, magisk mirror handling, improved unmount logic, pm install uses -S with a retry loop and waits for package manager readiness, log trimming, and other minor fixes. Also adjust mount/unmount commands, permission messages, and path handling in MagiskUtils. --- .../app/revanced/library/MagiskUtils.kt | 50 +++--- .../installation/installer/Constants.kt | 167 +++++++++--------- 2 files changed, 107 insertions(+), 110 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index 53857e3..03907d8 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -63,14 +63,14 @@ object MagiskUtils { else -> throw ShellCommandException("Patched APK not found for $packageName", -1, emptyList(), emptyList()) } - Shell.getShell().newJob().add(""" + Shell.getShell().newJob().add($$""" MIRROR="" if command -v magisk >/dev/null 2>&1; then - if ! MAGISKTMP=${"$"}(magisk --path 2>/dev/null); then MAGISKTMP=/sbin; fi - MIRROR=${"$"}{MAGISKTMP}/.magisk/mirror - [ -d "${"$"}{MIRROR}" ] || MIRROR="" + if ! MAGISKTMP=$(magisk --path 2>/dev/null); then MAGISKTMP=/sbin; fi + MIRROR=${MAGISKTMP}/.magisk/mirror + [ -d "${MIRROR}" ] || MIRROR="" fi - mount -o bind "${"$"}{MIRROR}$patchedApk" "$sourceDir" + mount -o bind "${MIRROR}$$patchedApk" "$$sourceDir" """.trimIndent()).exec().assertSuccess("Failed to mount APK") } @@ -98,11 +98,11 @@ object MagiskUtils { remoteFS.getFile(unifiedPath).deleteRecursively() val formattedPackageName = packageName.replace('.', '_') - val guardScriptPath = Constants.GUARD_SCRIPT_PATH(formattedPackageName) + val watchdogScriptPath = Constants.WATCHDOG_SCRIPT_PATH(formattedPackageName) Shell.getShell().newJob() .add("pm uninstall --user 0 \"$patchedPackageName\"") - .add("rm -f \"$guardScriptPath\"") + .add("rm -f \"$watchdogScriptPath\"") .exec() remoteFS.getFile("$MODULES_PATH/revanced_$formattedPackageName").deleteRecursively() @@ -154,21 +154,19 @@ object MagiskUtils { fun uninstallKeepData(packageName: String) = Shell.getShell().newJob() - .add("pm uninstall -k --user 0 $packageName") + .add("pm uninstall -k --user 0 \"$packageName\"") .exec() fun provisionMagiskModule( remoteFS: FileSystemManager, packageName: String, patchedPackageName: String, - version: String, - label: String, patchedApk: File ) { val formattedPackageName = packageName.replace('.', '_') val modulePath = "$MODULES_PATH/revanced_$formattedPackageName" val unifiedApkPath = Constants.MOUNTED_APK_PATH(packageName) - val guardScriptPath = Constants.GUARD_SCRIPT_PATH(formattedPackageName) + val watchdogScriptPath = Constants.WATCHDOG_SCRIPT_PATH(formattedPackageName) // Ensure directories exist val unifiedDir = unifiedApkPath.substringBeforeLast("/") @@ -176,27 +174,27 @@ object MagiskUtils { .add("mkdir -p \"$modulePath\"") .add("mkdir -p \"$unifiedDir\"") .exec() - .assertSuccess("Failed to create induction directories") + .assertSuccess("Failed to create module directories") - writeInductionFiles(remoteFS, modulePath, packageName, patchedPackageName, version, label) + writeModuleFiles(remoteFS, modulePath, packageName, patchedPackageName) - // Guard script: uninstalls the patched app when the module is disabled or removed. - val guardSh = Constants.GUARD_SCRIPT + // Watchdog script: uninstalls the patched app when the module is disabled or removed. + val watchdogSh = Constants.WATCHDOG_SCRIPT .replace("__PATCHED_PKG__", patchedPackageName) .replace("__FORMATTED_PKG__", formattedPackageName) - remoteFS.getFile(guardScriptPath).newOutputStream().use { it.write(guardSh.toByteArray()) } + remoteFS.getFile(watchdogScriptPath).newOutputStream().use { it.write(watchdogSh.toByteArray()) } // Source of truth APK copyApk(remoteFS, patchedApk, unifiedApkPath) - // Set permissions for unified path + // Set permissions Shell.getShell().newJob() .add("chmod 644 \"$unifiedApkPath\"") .add("chown system:system \"$unifiedApkPath\"") .add("chcon u:object_r:apk_data_file:s0 \"$unifiedApkPath\"") .add("chmod +x \"$modulePath/service.sh\"") .add("chmod +x \"$modulePath/uninstall.sh\"") - .add("chmod +x \"$guardScriptPath\"") + .add("chmod +x \"$watchdogScriptPath\"") .exec() .assertSuccess("Failed to set file permissions") } @@ -204,8 +202,6 @@ object MagiskUtils { fun provisionRootFolder( remoteFS: FileSystemManager, packageName: String, - version: String, - label: String, patchedApk: File ) { val modulePath = "$MODULES_PATH/$packageName-revanced" @@ -217,10 +213,10 @@ object MagiskUtils { .add("mkdir -p \"$modulePath\"") .add("mkdir -p \"$unifiedDir\"") .exec() - .assertSuccess("Failed to create induction directories") + .assertSuccess("Failed to create module directories") // MOUNT type: patched package name == original package name (bind-mount, no rename) - writeInductionFiles(remoteFS, modulePath, packageName, packageName, version, label) + writeModuleFiles(remoteFS, modulePath, packageName, packageName) // Source of truth APK copyApk(remoteFS, patchedApk, unifiedApkPath) @@ -235,26 +231,22 @@ object MagiskUtils { .assertSuccess("Failed to set file permissions") } - private fun writeInductionFiles( + private fun writeModuleFiles( remoteFS: FileSystemManager, modulePath: String, packageName: String, patchedPackageName: String, - version: String, - label: String ) { val formattedPackageName = packageName.replace('.', '_') val moduleProp = Constants.MAGISK_MODULE_PROP .replace("__FORMATTED_PKG__", formattedPackageName) - .replace("__VERSION__", version) - .replace("__LABEL__", label) + .replace("__PKG_NAME__", packageName) remoteFS.getFile("$modulePath/module.prop").newOutputStream().use { it.write(moduleProp.toByteArray()) } - val serviceSh = Constants.INDUCTION_SERVICE_SCRIPT + val serviceSh = Constants.MODULE_SERVICE_SCRIPT .replace("__PKG_NAME__", packageName) .replace("__PATCHED_PKG__", patchedPackageName) - .replace("__VERSION__", version) remoteFS.getFile("$modulePath/service.sh").newOutputStream().use { it.write(serviceSh.toByteArray()) } val uninstallSh = Constants.MAGISK_UNINSTALL_SCRIPT diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index 9639ec4..b50f3b2 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -8,7 +8,9 @@ object Constants { const val TMP_FILE_PATH = "/data/local/tmp/revanced.tmp" const val MOUNT_PATH = "/data/adb/revanced/" const val MOUNTED_APK_PATH = "$MOUNT_PATH$PLACEHOLDER/base.apk" + const val MOUNTED_APK_PATH_LEGACY = "$MOUNT_PATH$PLACEHOLDER.apk" const val MOUNT_SCRIPT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh" + const val WATCHDOG_SCRIPT_PATH = "/data/adb/service.d/revanced_watchdog_$PLACEHOLDER.sh" const val EXISTS = "[[ -f $PLACEHOLDER ]] || exit 1" const val MOUNT_GREP = "grep -F $PLACEHOLDER /proc/mounts" @@ -19,10 +21,22 @@ object Constants { const val INSTALLED_APK_PATH = "pm path $PLACEHOLDER" const val CREATE_INSTALLATION_PATH = "$CREATE_DIR $MOUNT_PATH$PLACEHOLDER" const val GET_SDK_VERSION = "getprop ro.build.version.sdk" + const val MODULE_PROP_FILE = "module.prop" + const val SERVICE_SCRIPT_FILE = "service.sh" + const val UNINSTALL_SCRIPT_FILE = "uninstall.sh" const val MAGISK_MODULES_PATH = "/data/adb/modules/" const val MAGISK_MODULE_ID = "revanced_$PLACEHOLDER" const val MAGISK_MODULE_PATH = "$MAGISK_MODULES_PATH$MAGISK_MODULE_ID" + const val STAGE_APK = + "base_path=\"$MOUNTED_APK_PATH\" && " + + "mkdir -p \"\$(dirname \"\${base_path}\")\" && " + + "mv $TMP_FILE_PATH \"\${base_path}\" && " + + "chmod 644 \"\${base_path}\" && " + + "chown system:system \"\${base_path}\" && " + + "chcon $SELINUX_CONTEXT \"\${base_path}\"" + + const val INSTALL_MOUNT_SCRIPT = "mv $TMP_FILE_PATH $MOUNT_SCRIPT_PATH && chmod +x $MOUNT_SCRIPT_PATH" const val MOVE = "mv $TMP_FILE_PATH $PLACEHOLDER" const val SET_MOUNTING_PERMISSIONS = "chmod 644 $PLACEHOLDER && chown system:system $PLACEHOLDER && chcon $SELINUX_CONTEXT $PLACEHOLDER" @@ -30,15 +44,14 @@ object Constants { /** * Magisk module property template. * The id MUST match the module directory name (revanced___FORMATTED_PKG__) so that - * Magisk/APatch/KernelSU can find the module by id for enable/disable operations. + * Magisk can find the module by ID for enable/disable operations. * * Placeholders: __FORMATTED_PKG__ (original with dots→underscores), __VERSION__, __LABEL__ */ - val MAGISK_MODULE_PROP = - """ + val MAGISK_MODULE_PROP = """ id=revanced___FORMATTED_PKG__ - name=__LABEL__ ReVanced - version=__VERSION__ + name=__PKG_NAME__ ReVanced + version=1.0 versionCode=0 author=ReVanced description=Mounts the patched APK on top of the original one @@ -47,174 +60,166 @@ object Constants { /** * Magisk module uninstall script template. Magisk runs this when the module is * removed via the Magisk app. It cleans up the unified source-of-truth APK and - * the boot-time guard script. + * the boot-time watchdog script. * * Placeholders: __PKG_NAME__ (original), __PATCHED_PKG__ (patched), __FORMATTED_PKG__ (original with dots→underscores) */ - val MAGISK_UNINSTALL_SCRIPT = - """ + val MAGISK_UNINSTALL_SCRIPT = """ #!/system/bin/sh pm uninstall --user 0 "__PATCHED_PKG__" rm -rf "/data/adb/revanced/__PKG_NAME__" - rm -f "/data/adb/service.d/revanced_guard___FORMATTED_PKG__.sh" + rm -f "/data/adb/service.d/revanced_watchdog___FORMATTED_PKG__.sh" """.trimIndent() - const val GUARD_SCRIPT_PATH = "/data/adb/service.d/revanced_guard_$PLACEHOLDER.sh" - /** - * Boot-time guard script. Runs on every boot (via service.d, independent of module state). + * Boot-time watchdog script. Runs on every boot (via service.d, independent of module state). * Uninstalls the patched app when the module is disabled or removed, so the app * disappears when the module is toggled off. * * Placeholders: __PATCHED_PKG__ (patched), __FORMATTED_PKG__ (original with dots→underscores) */ - val GUARD_SCRIPT = - """ + val WATCHDOG_SCRIPT = $$""" #!/system/bin/sh patched_pkg="__PATCHED_PKG__" module_path="/data/adb/modules/revanced___FORMATTED_PKG__" - until [ "${"$"}(getprop sys.boot_completed)" = 1 ]; do sleep 5; done + until [ "$(getprop sys.boot_completed)" = 1 ]; do sleep 5; done sleep 11 # Module was fully removed — uninstall app and self-destruct this script. - if [ ! -d "${"$"}{module_path}" ]; then - pm uninstall --user 0 "${"$"}{patched_pkg}" 2>/dev/null + if [ ! -d "${module_path}" ]; then + pm uninstall --user 0 "${patched_pkg}" 2>/dev/null rm -f "$0" exit 0 fi # If service.sh did not run this boot, the module is disabled — uninstall the app. - current_boot_id=${"$"}(cat /proc/sys/kernel/random/boot_id 2>/dev/null) - stored_boot_id=${"$"}(cat "${"$"}{module_path}/.boot_token" 2>/dev/null) - if [ "${"$"}{stored_boot_id}" != "${"$"}{current_boot_id}" ]; then - pm uninstall --user 0 "${"$"}{patched_pkg}" 2>/dev/null + current_boot_id=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) + stored_boot_id=$(cat "${module_path}/.boot_token" 2>/dev/null) + if [ "${stored_boot_id}" != "${current_boot_id}" ]; then + pm uninstall --user 0 "${patched_pkg}" 2>/dev/null fi """.trimIndent() - const val MOUNT_APK = - "base_path=\"$MOUNTED_APK_PATH\" && " + - "mkdir -p \"${"$"}(dirname \"${"$"}{base_path}\")\" && " + - "mv $TMP_FILE_PATH \"${"$"}{base_path}\" && " + - "chmod 644 \"${"$"}{base_path}\" && " + - "chown system:system \"${"$"}{base_path}\" && " + - "chcon $SELINUX_CONTEXT \"${"$"}{base_path}\"" - - val UMOUNT = - """ - grep $PLACEHOLDER /proc/mounts | - while read -r line; do echo ${"$"}{line} | - cut -d ' ' -f 2 | - sed 's/apk.*/apk/' | + val UMOUNT = $$""" + grep -F "/$$PLACEHOLDER/" /proc/mounts | + while read -r line; do echo ${line} | + cut -d ' ' -f 2 | + sed 's/apk.*/apk/' | xargs -r umount -l; done """.trimIndent() - const val INSTALL_MOUNT_SCRIPT = - "mv $TMP_FILE_PATH $MOUNT_SCRIPT_PATH && chmod +x $MOUNT_SCRIPT_PATH" - - val MOUNT_SCRIPT = - """ + val MOUNT_SCRIPT = $$""" #!/system/bin/sh - until [ "${"$"}( getprop sys.boot_completed )" = 1 ]; do sleep 3; done + until [ "$(getprop sys.boot_completed)" = 1 ]; do sleep 3; done until [ -d "/sdcard/Android" ]; do sleep 1; done - stock_path=${"$"}( pm path $PLACEHOLDER | grep base | sed 's/package://g' ) + stock_path=$(pm path $$PLACEHOLDER | grep base | sed 's/package://g') # Make sure the app is installed. - if [ -z "${"$"}{stock_path}" ]; then + if [ -z "${stock_path}" ]; then exit 1 fi # Unmount any existing installations to prevent multiple unnecessary mounts. - $UMOUNT + $$UMOUNT - base_path="${"$"}{MOUNTED_APK_PATH}" + base_path="$$MOUNTED_APK_PATH" - chcon $SELINUX_CONTEXT ${"$"}{base_path} + chcon $$SELINUX_CONTEXT ${base_path} # Mount using Magisk mirror, if available. if command -v magisk >/dev/null 2>&1; then - if ! MAGISKTMP="${"$"}(magisk --path 2>/dev/null)"; then + if ! MAGISKTMP="$(magisk --path 2>/dev/null)"; then MAGISKTMP=/sbin fi - MIRROR="${"$"}{MAGISKTMP}/.magisk/mirror" - [ -d "${"$"}{MIRROR}" ] || MIRROR="" + MIRROR="${MAGISKTMP}/.magisk/mirror" + [ -d "${MIRROR}" ] || MIRROR="" fi - mount -o bind ${"$"}{MIRROR}${"$"}{base_path} ${"$"}{stock_path} + mount -o bind ${MIRROR}${base_path} ${stock_path} # Kill the app to force it to restart the mounted APK in case it's currently running. - $KILL + $$KILL """.trimIndent() /** * Magisk module service script template. Runs on every boot when the module is enabled. - * Installs the patched APK if not already installed. + * Installs the patched APK as a standalone app if not already installed. * - * Placeholders: __PKG_NAME__ (original, used for APK path), __PATCHED_PKG__ (patched, used for pm commands), __VERSION__ + * Placeholders: __PKG_NAME__ (original, used for APK path), __PATCHED_PKG__ (patched, used for pm commands) */ - val INDUCTION_SERVICE_SCRIPT = - """ + val MODULE_SERVICE_SCRIPT = $$""" #!/system/bin/sh - DIR=${"$"}{0%/*} + DIR=${0%/*} package_name="__PATCHED_PKG__" - version="__VERSION__" - # Write a boot token so the guard script can detect whether service.sh ran this boot. - cp /proc/sys/kernel/random/boot_id "${"$"}{DIR}/.boot_token" + # Write a boot token so the watchdog script can detect whether service.sh ran this boot. + cp /proc/sys/kernel/random/boot_id "${DIR}/.boot_token" - LOG="${"$"}{DIR}/log" + LOG="${DIR}/log" MAX_LOG_LINES=200 # Trim log to last MAX_LOG_LINES lines to prevent unbounded growth. - if [ -f "${"$"}{LOG}" ]; then - tail -n "${"$"}{MAX_LOG_LINES}" "${"$"}{LOG}" > "${"$"}{LOG}.tmp" && mv "${"$"}{LOG}.tmp" "${"$"}{LOG}" + if [ -f "${LOG}" ]; then + tail -n "${MAX_LOG_LINES}" "${LOG}" > "${LOG}.tmp" && mv "${LOG}.tmp" "${LOG}" fi { - echo "--- ${"$"}(date '+%Y-%m-%d %H:%M:%S') | pkg=${"$"}{package_name} | ver=${"$"}{version} ---" + echo "--- $(date '+%Y-%m-%d %H:%M:%S') | pkg=${package_name} ---" - until [ "${"$"}(getprop sys.boot_completed)" = 1 ]; do sleep 5; done + until [ "$(getprop sys.boot_completed)" = 1 ]; do sleep 5; done - # Wait for the package manager service to be registered and ready for transactions. - until service check package 2>/dev/null | grep -q "found"; do sleep 3; done - sleep 5 + # Wait until PM is fully responsive — sys.boot_completed=1 fires before the PM + # binder handles transactions. Poll until it returns at least one package entry. + until pm list packages --user 0 2>/dev/null | grep -q "^package:"; do sleep 5; done base_path="/data/adb/revanced/__PKG_NAME__/base.apk" # Fallback for legacy compatibility. - if [ ! -f "${"$"}{base_path}" ]; then - base_path="${"$"}{DIR}/__PKG_NAME__.apk" + if [ ! -f "${base_path}" ]; then + base_path="${DIR}/__PKG_NAME__.apk" fi - echo "Base path: ${"$"}{base_path}" - echo "Base version: ${"$"}{version}" + echo "Base path: ${base_path}" - if [ ! -f "${"$"}{base_path}" ]; then + if [ ! -f "${base_path}" ]; then echo "Patched APK not found." exit 1 fi # Skip install if the app is already present (pm install persists across reboots). - if pm list packages --user 0 | grep -q "^package:${"$"}{package_name}$"; then + if pm list packages --user 0 | grep -q "^package:${package_name}$"; then echo "Package already installed, skipping." else + # Retry loop — sys.boot_completed=1 fires before the PM binder is stable for + # write transactions, causing "Failed transaction" errors. Pipe-based install + # (pm install -S size < file) uses a simpler code path than session-based + # (install-create/write/commit) and is less prone to early-boot binder failures. + # NOTE: On Xiaomi devices (MIUI/HyperOS), pm install may still fail due to + # package verification restrictions. Manual install via ReVanced Manager + # may be required in that case. + max_retries=3 attempt=0 - while [ ${"$"}{attempt} -lt 3 ]; do - attempt=${"$"}((attempt + 1)) - echo "Install attempt ${"$"}{attempt}/3" - pm install -r -d --user 0 "${"$"}{base_path}" + install_exit=1 + + while [ ${attempt} -lt ${max_retries} ] && [ ${install_exit} -ne 0 ]; do + attempt=$((attempt + 1)) + echo "Install attempt ${attempt}/${max_retries}..." + pm install -r -d --user 0 -S $(stat -c%s "${base_path}") < "${base_path}" install_exit=$? - echo "Install exit code: ${"$"}{install_exit}" - [ ${"$"}{install_exit} -eq 0 ] && break - [ ${"$"}{attempt} -lt 3 ] && { echo "Retrying in 10s..."; sleep 10; } + echo "Install exit code: ${install_exit}" + if [ ${install_exit} -ne 0 ] && [ ${attempt} -lt ${max_retries} ]; then + echo "Retrying in 15s..." + sleep 5 + fi done fi - } >> "${"$"}{LOG}" + } >> "${LOG}" """.trimIndent() /** From d27169d79931e32d90f49373ef59f78fd4d80721 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sun, 12 Apr 2026 22:18:17 +0100 Subject: [PATCH 28/47] revert: Use flat apk path instead of per-package subdirectory --- .../library/installation/installer/Constants.kt | 12 +++--------- .../library/installation/installer/RootInstaller.kt | 7 +------ 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index b50f3b2..f78d958 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -7,8 +7,7 @@ object Constants { const val SELINUX_CONTEXT = "u:object_r:apk_data_file:s0" const val TMP_FILE_PATH = "/data/local/tmp/revanced.tmp" const val MOUNT_PATH = "/data/adb/revanced/" - const val MOUNTED_APK_PATH = "$MOUNT_PATH$PLACEHOLDER/base.apk" - const val MOUNTED_APK_PATH_LEGACY = "$MOUNT_PATH$PLACEHOLDER.apk" + const val MOUNTED_APK_PATH = "$MOUNT_PATH$PLACEHOLDER.apk" const val MOUNT_SCRIPT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh" const val WATCHDOG_SCRIPT_PATH = "/data/adb/service.d/revanced_watchdog_$PLACEHOLDER.sh" @@ -67,7 +66,7 @@ object Constants { val MAGISK_UNINSTALL_SCRIPT = """ #!/system/bin/sh pm uninstall --user 0 "__PATCHED_PKG__" - rm -rf "/data/adb/revanced/__PKG_NAME__" + rm -f "/data/adb/revanced/__PKG_NAME__.apk" rm -f "/data/adb/service.d/revanced_watchdog___FORMATTED_PKG__.sh" """.trimIndent() @@ -177,12 +176,7 @@ object Constants { # binder handles transactions. Poll until it returns at least one package entry. until pm list packages --user 0 2>/dev/null | grep -q "^package:"; do sleep 5; done - base_path="/data/adb/revanced/__PKG_NAME__/base.apk" - - # Fallback for legacy compatibility. - if [ ! -f "${base_path}" ]; then - base_path="${DIR}/__PKG_NAME__.apk" - fi + base_path="/data/adb/revanced/__PKG_NAME__.apk" echo "Base path: ${base_path}" diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt index ce95d0d..1d36ef1 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt @@ -8,7 +8,6 @@ import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH import app.revanced.library.installation.installer.Constants.INSTALL_MOUNT_SCRIPT import app.revanced.library.installation.installer.Constants.KILL import app.revanced.library.installation.installer.Constants.MOUNTED_APK_PATH -import app.revanced.library.installation.installer.Constants.MOUNTED_APK_PATH_LEGACY import app.revanced.library.installation.installer.Constants.MOUNT_GREP import app.revanced.library.installation.installer.Constants.STAGE_APK import app.revanced.library.installation.installer.Constants.MOUNT_SCRIPT @@ -80,7 +79,6 @@ abstract class RootInstaller internal constructor( UMOUNT(packageName)() DELETE(MOUNTED_APK_PATH(packageName))() - DELETE(MOUNTED_APK_PATH_LEGACY(packageName))() // Remove legacy flat-file path if present. DELETE(MOUNT_SCRIPT_PATH(packageName))() DELETE(TMP_FILE_PATH)() // Remove residual. @@ -90,10 +88,7 @@ abstract class RootInstaller internal constructor( } override suspend fun getInstallation(packageName: String): RootInstallation? { - // Check current path first, fall back to legacy flat-file path for existing installations - val patchedApkPath = MOUNTED_APK_PATH(packageName).takeIf { EXISTS(it)().exitCode == 0 } - ?: MOUNTED_APK_PATH_LEGACY(packageName).takeIf { EXISTS(it)().exitCode == 0 } - ?: return null + val patchedApkPath = MOUNTED_APK_PATH(packageName).takeIf { EXISTS(it)().exitCode == 0 } ?: return null return RootInstallation( INSTALLED_APK_PATH(packageName)().output.ifEmpty { null }, From 3119114b84a636441d3dfe476ec554ec5ef63775 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sun, 12 Apr 2026 22:24:28 +0100 Subject: [PATCH 29/47] refactor: Better descriptive module script --- .../kotlin/app/revanced/library/MagiskUtils.kt | 14 +++++++------- .../library/installation/installer/Constants.kt | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index 03907d8..54ab82a 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -98,11 +98,11 @@ object MagiskUtils { remoteFS.getFile(unifiedPath).deleteRecursively() val formattedPackageName = packageName.replace('.', '_') - val watchdogScriptPath = Constants.WATCHDOG_SCRIPT_PATH(formattedPackageName) + val handleDisabledScriptPath = Constants.HANDLE_DISABLED_SCRIPT_PATH(formattedPackageName) Shell.getShell().newJob() .add("pm uninstall --user 0 \"$patchedPackageName\"") - .add("rm -f \"$watchdogScriptPath\"") + .add("rm -f \"$handleDisabledScriptPath\"") .exec() remoteFS.getFile("$MODULES_PATH/revanced_$formattedPackageName").deleteRecursively() @@ -166,7 +166,7 @@ object MagiskUtils { val formattedPackageName = packageName.replace('.', '_') val modulePath = "$MODULES_PATH/revanced_$formattedPackageName" val unifiedApkPath = Constants.MOUNTED_APK_PATH(packageName) - val watchdogScriptPath = Constants.WATCHDOG_SCRIPT_PATH(formattedPackageName) + val handleDisabledScriptPath = Constants.HANDLE_DISABLED_SCRIPT_PATH(formattedPackageName) // Ensure directories exist val unifiedDir = unifiedApkPath.substringBeforeLast("/") @@ -178,11 +178,11 @@ object MagiskUtils { writeModuleFiles(remoteFS, modulePath, packageName, patchedPackageName) - // Watchdog script: uninstalls the patched app when the module is disabled or removed. - val watchdogSh = Constants.WATCHDOG_SCRIPT + // Handle-disabled script: uninstalls the patched app when the module is disabled or removed. + val handleDisabledSh = Constants.HANDLE_DISABLED_SCRIPT .replace("__PATCHED_PKG__", patchedPackageName) .replace("__FORMATTED_PKG__", formattedPackageName) - remoteFS.getFile(watchdogScriptPath).newOutputStream().use { it.write(watchdogSh.toByteArray()) } + remoteFS.getFile(handleDisabledScriptPath).newOutputStream().use { it.write(handleDisabledSh.toByteArray()) } // Source of truth APK copyApk(remoteFS, patchedApk, unifiedApkPath) @@ -194,7 +194,7 @@ object MagiskUtils { .add("chcon u:object_r:apk_data_file:s0 \"$unifiedApkPath\"") .add("chmod +x \"$modulePath/service.sh\"") .add("chmod +x \"$modulePath/uninstall.sh\"") - .add("chmod +x \"$watchdogScriptPath\"") + .add("chmod +x \"$handleDisabledScriptPath\"") .exec() .assertSuccess("Failed to set file permissions") } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index f78d958..1753ad1 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -9,7 +9,7 @@ object Constants { const val MOUNT_PATH = "/data/adb/revanced/" const val MOUNTED_APK_PATH = "$MOUNT_PATH$PLACEHOLDER.apk" const val MOUNT_SCRIPT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh" - const val WATCHDOG_SCRIPT_PATH = "/data/adb/service.d/revanced_watchdog_$PLACEHOLDER.sh" + const val HANDLE_DISABLED_SCRIPT_PATH = "/data/adb/service.d/revanced_handle_disabled_$PLACEHOLDER.sh" const val EXISTS = "[[ -f $PLACEHOLDER ]] || exit 1" const val MOUNT_GREP = "grep -F $PLACEHOLDER /proc/mounts" @@ -59,7 +59,7 @@ object Constants { /** * Magisk module uninstall script template. Magisk runs this when the module is * removed via the Magisk app. It cleans up the unified source-of-truth APK and - * the boot-time watchdog script. + * the boot-time handle-disabled script. * * Placeholders: __PKG_NAME__ (original), __PATCHED_PKG__ (patched), __FORMATTED_PKG__ (original with dots→underscores) */ @@ -67,17 +67,17 @@ object Constants { #!/system/bin/sh pm uninstall --user 0 "__PATCHED_PKG__" rm -f "/data/adb/revanced/__PKG_NAME__.apk" - rm -f "/data/adb/service.d/revanced_watchdog___FORMATTED_PKG__.sh" + rm -f "/data/adb/service.d/revanced_handle_disabled___FORMATTED_PKG__.sh" """.trimIndent() /** - * Boot-time watchdog script. Runs on every boot (via service.d, independent of module state). + * Boot-time handle-disabled script. Runs on every boot (via service.d, independent of module state). * Uninstalls the patched app when the module is disabled or removed, so the app * disappears when the module is toggled off. * * Placeholders: __PATCHED_PKG__ (patched), __FORMATTED_PKG__ (original with dots→underscores) */ - val WATCHDOG_SCRIPT = $$""" + val HANDLE_DISABLED_SCRIPT = $$""" #!/system/bin/sh patched_pkg="__PATCHED_PKG__" module_path="/data/adb/modules/revanced___FORMATTED_PKG__" @@ -155,7 +155,7 @@ object Constants { package_name="__PATCHED_PKG__" - # Write a boot token so the watchdog script can detect whether service.sh ran this boot. + # Write a boot token so the handle-disabled script can detect whether service.sh ran this boot. cp /proc/sys/kernel/random/boot_id "${DIR}/.boot_token" LOG="${DIR}/log" From fe6f102ed71f312a1b7176113630a6c91f18d766 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sun, 12 Apr 2026 22:29:43 +0100 Subject: [PATCH 30/47] enhance: Disable instead of uninstall handler --- .../library/installation/installer/Constants.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index 1753ad1..1e03309 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -92,11 +92,12 @@ object Constants { exit 0 fi - # If service.sh did not run this boot, the module is disabled — uninstall the app. + # If service.sh did not run this boot, the module is disabled — disable the app so it + # disappears from the launcher without losing data. service.sh re-enables it on next boot. current_boot_id=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) stored_boot_id=$(cat "${module_path}/.boot_token" 2>/dev/null) if [ "${stored_boot_id}" != "${current_boot_id}" ]; then - pm uninstall --user 0 "${patched_pkg}" 2>/dev/null + pm disable-user --user 0 "${patched_pkg}" 2>/dev/null fi """.trimIndent() @@ -185,9 +186,11 @@ object Constants { exit 1 fi - # Skip install if the app is already present (pm install persists across reboots). + # Re-enable the app if it was disabled by the handle-disabled script (module was toggled off + # then back on). If not installed at all, fall through to the install block. if pm list packages --user 0 | grep -q "^package:${package_name}$"; then - echo "Package already installed, skipping." + pm enable --user 0 "${package_name}" 2>/dev/null + echo "Package enabled." else # Retry loop — sys.boot_completed=1 fires before the PM binder is stable for # write transactions, causing "Failed transaction" errors. Pipe-based install From 650e952f3bc35c14ddd1eb39d7a3356c3b6fc72b Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sun, 12 Apr 2026 22:34:56 +0100 Subject: [PATCH 31/47] refactor: Moved paths for grouping with other paths --- .../revanced/library/installation/installer/Constants.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index 1e03309..bd4ced4 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -10,6 +10,9 @@ object Constants { const val MOUNTED_APK_PATH = "$MOUNT_PATH$PLACEHOLDER.apk" const val MOUNT_SCRIPT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh" const val HANDLE_DISABLED_SCRIPT_PATH = "/data/adb/service.d/revanced_handle_disabled_$PLACEHOLDER.sh" + const val MAGISK_MODULES_PATH = "/data/adb/modules/" + const val MAGISK_MODULE_ID = "revanced_$PLACEHOLDER" + const val MAGISK_MODULE_PATH = "$MAGISK_MODULES_PATH$MAGISK_MODULE_ID" const val EXISTS = "[[ -f $PLACEHOLDER ]] || exit 1" const val MOUNT_GREP = "grep -F $PLACEHOLDER /proc/mounts" @@ -23,10 +26,6 @@ object Constants { const val MODULE_PROP_FILE = "module.prop" const val SERVICE_SCRIPT_FILE = "service.sh" const val UNINSTALL_SCRIPT_FILE = "uninstall.sh" - - const val MAGISK_MODULES_PATH = "/data/adb/modules/" - const val MAGISK_MODULE_ID = "revanced_$PLACEHOLDER" - const val MAGISK_MODULE_PATH = "$MAGISK_MODULES_PATH$MAGISK_MODULE_ID" const val STAGE_APK = "base_path=\"$MOUNTED_APK_PATH\" && " + "mkdir -p \"\$(dirname \"\${base_path}\")\" && " + From e2760953e1072f4ec0ffe775393e16980326123a Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Mon, 13 Apr 2026 19:07:58 +0000 Subject: [PATCH 32/47] refactor: Unicode fix Co-authored-by: oSumAtrIX --- .../app/revanced/library/installation/installer/Constants.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index bd4ced4..882e1fa 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -84,7 +84,7 @@ object Constants { until [ "$(getprop sys.boot_completed)" = 1 ]; do sleep 5; done sleep 11 - # Module was fully removed — uninstall app and self-destruct this script. + # Module was fully removed. Uninstall app and self-destruct this script. if [ ! -d "${module_path}" ]; then pm uninstall --user 0 "${patched_pkg}" 2>/dev/null rm -f "$0" From d0f432ec723158ee13ec516495e4b73a9010cdea Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 14 Apr 2026 17:15:44 +0100 Subject: [PATCH 33/47] fix: Removed user param for uninstall --- .../androidMain/kotlin/app/revanced/library/MagiskUtils.kt | 4 ++-- .../app/revanced/library/installation/installer/Constants.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index 54ab82a..9959dd1 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -101,7 +101,7 @@ object MagiskUtils { val handleDisabledScriptPath = Constants.HANDLE_DISABLED_SCRIPT_PATH(formattedPackageName) Shell.getShell().newJob() - .add("pm uninstall --user 0 \"$patchedPackageName\"") + .add("pm uninstall \"$patchedPackageName\"") .add("rm -f \"$handleDisabledScriptPath\"") .exec() @@ -154,7 +154,7 @@ object MagiskUtils { fun uninstallKeepData(packageName: String) = Shell.getShell().newJob() - .add("pm uninstall -k --user 0 \"$packageName\"") + .add("pm uninstall -k \"$packageName\"") .exec() fun provisionMagiskModule( diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index 882e1fa..cf98b62 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -64,7 +64,7 @@ object Constants { */ val MAGISK_UNINSTALL_SCRIPT = """ #!/system/bin/sh - pm uninstall --user 0 "__PATCHED_PKG__" + pm uninstall "__PATCHED_PKG__" rm -f "/data/adb/revanced/__PKG_NAME__.apk" rm -f "/data/adb/service.d/revanced_handle_disabled___FORMATTED_PKG__.sh" """.trimIndent() @@ -86,7 +86,7 @@ object Constants { # Module was fully removed. Uninstall app and self-destruct this script. if [ ! -d "${module_path}" ]; then - pm uninstall --user 0 "${patched_pkg}" 2>/dev/null + pm uninstall "${patched_pkg}" 2>/dev/null rm -f "$0" exit 0 fi From 041519ad13296000af6d523d3290b02e347b1e90 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 14 Apr 2026 20:01:43 +0100 Subject: [PATCH 34/47] refactor: Standardize naming to "prepare" --- .../kotlin/app/revanced/library/MagiskUtils.kt | 4 ++-- .../kotlin/app/revanced/library/ApkUtils.kt | 4 ++-- .../library/installation/installer/Constants.kt | 4 ++-- .../installation/installer/MagiskInstaller.kt | 8 ++++---- .../library/installation/installer/RootInstaller.kt | 12 ++++++------ 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index 9959dd1..499a0c3 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -157,7 +157,7 @@ object MagiskUtils { .add("pm uninstall -k \"$packageName\"") .exec() - fun provisionMagiskModule( + fun prepareMagiskModule( remoteFS: FileSystemManager, packageName: String, patchedPackageName: String, @@ -199,7 +199,7 @@ object MagiskUtils { .assertSuccess("Failed to set file permissions") } - fun provisionRootFolder( + fun prepareRootFolder( remoteFS: FileSystemManager, packageName: String, patchedApk: File diff --git a/library/src/commonMain/kotlin/app/revanced/library/ApkUtils.kt b/library/src/commonMain/kotlin/app/revanced/library/ApkUtils.kt index 53b0390..5458239 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/ApkUtils.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/ApkUtils.kt @@ -45,7 +45,7 @@ object ApkUtils { * 2. Delete all resources in the target APK * 3. Merge resources.apk compiled by AAPT. * 4. Write raw resources. - * 5. Delete resources staged for deletion. + * 5. Delete resources marked for deletion. * 6. Realign the APK. * * @param apkFile The file to apply the patched files to. @@ -81,7 +81,7 @@ object ApkUtils { } } - // Delete resources that were staged for deletion. + // Delete resources that were marked for deletion. if (resources.deleteResources.isNotEmpty()) { targetApkZFile.entries().filter { entry -> entry.centralDirectoryHeader.name in resources.deleteResources diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index cf98b62..338ca30 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -26,7 +26,7 @@ object Constants { const val MODULE_PROP_FILE = "module.prop" const val SERVICE_SCRIPT_FILE = "service.sh" const val UNINSTALL_SCRIPT_FILE = "uninstall.sh" - const val STAGE_APK = + const val PREPARE_APK = "base_path=\"$MOUNTED_APK_PATH\" && " + "mkdir -p \"\$(dirname \"\${base_path}\")\" && " + "mv $TMP_FILE_PATH \"\${base_path}\" && " + @@ -34,7 +34,7 @@ object Constants { "chown system:system \"\${base_path}\" && " + "chcon $SELINUX_CONTEXT \"\${base_path}\"" - const val INSTALL_MOUNT_SCRIPT = "mv $TMP_FILE_PATH $MOUNT_SCRIPT_PATH && chmod +x $MOUNT_SCRIPT_PATH" + const val PREPARE_MOUNT_SCRIPT = "mv $TMP_FILE_PATH $MOUNT_SCRIPT_PATH && chmod +x $MOUNT_SCRIPT_PATH" const val MOVE = "mv $TMP_FILE_PATH $PLACEHOLDER" const val SET_MOUNTING_PERMISSIONS = "chmod 644 $PLACEHOLDER && chown system:system $PLACEHOLDER && chcon $SELINUX_CONTEXT $PLACEHOLDER" diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt index 00b574d..7bfff25 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt @@ -34,10 +34,10 @@ abstract class MagiskInstaller internal constructor( /** * Installs the given [apk] as a Magisk module. * - * The patched APK is staged at the unified source-of-truth path + * The patched APK is prepared at the unified source-of-truth path * `/data/adb/revanced//base.apk` (the same location used by the * non-Magisk root installer), and the module ships a `service.sh` that - * installs the patched APK on every boot. After provisioning, + * installs the patched APK on every boot. After preparation, * `service.sh` is executed inline so the install takes effect immediately, * without requiring a reboot. * @@ -52,9 +52,9 @@ abstract class MagiskInstaller internal constructor( val formattedPackageName = packageName.replace('.', '_') val modulePath = MAGISK_MODULE_PATH(formattedPackageName) - // Stage the patched APK at the unified source-of-truth path. + // Prepare the patched APK at the unified source-of-truth path. apk.file.move(TMP_FILE_PATH) - stageApk(packageName) + prepareApk(packageName) // Create the Magisk module directory. "mkdir -p $modulePath"().waitFor() diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt index 1d36ef1..0be0cb4 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt @@ -5,11 +5,11 @@ import app.revanced.library.installation.installer.Constants.CREATE_INSTALLATION import app.revanced.library.installation.installer.Constants.DELETE import app.revanced.library.installation.installer.Constants.EXISTS import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH -import app.revanced.library.installation.installer.Constants.INSTALL_MOUNT_SCRIPT +import app.revanced.library.installation.installer.Constants.PREPARE_MOUNT_SCRIPT import app.revanced.library.installation.installer.Constants.KILL import app.revanced.library.installation.installer.Constants.MOUNTED_APK_PATH import app.revanced.library.installation.installer.Constants.MOUNT_GREP -import app.revanced.library.installation.installer.Constants.STAGE_APK +import app.revanced.library.installation.installer.Constants.PREPARE_APK import app.revanced.library.installation.installer.Constants.MOUNT_SCRIPT import app.revanced.library.installation.installer.Constants.MOUNT_SCRIPT_PATH import app.revanced.library.installation.installer.Constants.RESTART @@ -40,10 +40,10 @@ abstract class RootInstaller internal constructor( } /** - * Stages the APK from [TMP_FILE_PATH] to the unified source-of-truth path for [packageName], + * Prepares the APK from [TMP_FILE_PATH] to the unified source-of-truth path for [packageName], * creating the directory and applying permissions/SELinux context. */ - protected fun stageApk(packageName: String) = STAGE_APK(packageName)().waitFor() + protected fun prepareApk(packageName: String) = PREPARE_APK(packageName)().waitFor() /** * Installs the given [apk] by mounting. @@ -60,11 +60,11 @@ abstract class RootInstaller internal constructor( // Setup files. apk.file.move(TMP_FILE_PATH) CREATE_INSTALLATION_PATH(packageName)().waitFor() - stageApk(packageName) + prepareApk(packageName) // Install and run. TMP_FILE_PATH.write(MOUNT_SCRIPT(packageName)) - INSTALL_MOUNT_SCRIPT(packageName)().waitFor() + PREPARE_MOUNT_SCRIPT(packageName)().waitFor() MOUNT_SCRIPT_PATH(packageName)().waitFor() RESTART(packageName)() From 84738bb50d679fa1dbd7d7daf64dfaa124440154 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 14 Apr 2026 20:11:37 +0100 Subject: [PATCH 35/47] refactor: Inline single-use constants & clarify placeholders --- .../library/installation/installer/Constants.kt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index 338ca30..4b53563 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -10,9 +10,7 @@ object Constants { const val MOUNTED_APK_PATH = "$MOUNT_PATH$PLACEHOLDER.apk" const val MOUNT_SCRIPT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh" const val HANDLE_DISABLED_SCRIPT_PATH = "/data/adb/service.d/revanced_handle_disabled_$PLACEHOLDER.sh" - const val MAGISK_MODULES_PATH = "/data/adb/modules/" - const val MAGISK_MODULE_ID = "revanced_$PLACEHOLDER" - const val MAGISK_MODULE_PATH = "$MAGISK_MODULES_PATH$MAGISK_MODULE_ID" + const val MAGISK_MODULE_PATH = "/data/adb/modules/revanced_$PLACEHOLDER" const val EXISTS = "[[ -f $PLACEHOLDER ]] || exit 1" const val MOUNT_GREP = "grep -F $PLACEHOLDER /proc/mounts" @@ -36,9 +34,6 @@ object Constants { const val PREPARE_MOUNT_SCRIPT = "mv $TMP_FILE_PATH $MOUNT_SCRIPT_PATH && chmod +x $MOUNT_SCRIPT_PATH" - const val MOVE = "mv $TMP_FILE_PATH $PLACEHOLDER" - const val SET_MOUNTING_PERMISSIONS = "chmod 644 $PLACEHOLDER && chown system:system $PLACEHOLDER && chcon $SELINUX_CONTEXT $PLACEHOLDER" - /** * Magisk module property template. * The id MUST match the module directory name (revanced___FORMATTED_PKG__) so that @@ -57,10 +52,11 @@ object Constants { /** * Magisk module uninstall script template. Magisk runs this when the module is - * removed via the Magisk app. It cleans up the unified source-of-truth APK and - * the boot-time handle-disabled script. + * removed via the Magisk app. It cleans up the patched APK file and the + * boot-time handle-disabled script. * - * Placeholders: __PKG_NAME__ (original), __PATCHED_PKG__ (patched), __FORMATTED_PKG__ (original with dots→underscores) + * Placeholders: __PKG_NAME__ (unpatched), __PATCHED_PKG__ (patched), + * __FORMATTED_PKG__ (unpatched package name with dots replaced by underscores, used as Magisk module ID) */ val MAGISK_UNINSTALL_SCRIPT = """ #!/system/bin/sh From 6fded1dcc3b0e7972741dfac5bf3a36b61ba6770 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 14 Apr 2026 20:43:39 +0100 Subject: [PATCH 36/47] refactor: Change 'MAGISK_' prefix in favor of Module --- .../app/revanced/library/MagiskUtils.kt | 4 +-- .../installer/LocalMagiskInstaller.kt | 14 +++++----- .../installer/AdbMagiskInstaller.kt | 8 +++--- .../installation/installer/Constants.kt | 6 ++--- .../installation/installer/MagiskInstaller.kt | 27 +++++++------------ 5 files changed, 26 insertions(+), 33 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index 499a0c3..56a7740 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -239,7 +239,7 @@ object MagiskUtils { ) { val formattedPackageName = packageName.replace('.', '_') - val moduleProp = Constants.MAGISK_MODULE_PROP + val moduleProp = Constants.MODULE_PROP .replace("__FORMATTED_PKG__", formattedPackageName) .replace("__PKG_NAME__", packageName) remoteFS.getFile("$modulePath/module.prop").newOutputStream().use { it.write(moduleProp.toByteArray()) } @@ -249,7 +249,7 @@ object MagiskUtils { .replace("__PATCHED_PKG__", patchedPackageName) remoteFS.getFile("$modulePath/service.sh").newOutputStream().use { it.write(serviceSh.toByteArray()) } - val uninstallSh = Constants.MAGISK_UNINSTALL_SCRIPT + val uninstallSh = Constants.MODULE_UNINSTALL_SCRIPT .replace("__PKG_NAME__", packageName) .replace("__PATCHED_PKG__", patchedPackageName) .replace("__FORMATTED_PKG__", formattedPackageName) diff --git a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt index a214bd1..79940aa 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt @@ -6,10 +6,10 @@ import com.topjohnwu.superuser.ipc.RootService import java.io.Closeable /** - * [LocalMagiskInstaller] for installing and uninstalling [Apk] files locally with root permissions via Magisk modules. + * [LocalMagiskRootInstaller] for installing and uninstalling [Apk] files locally with root permissions via Magisk modules. * * @param context The [Context] to use for binding to the [RootService]. - * @param onReady A callback to be invoked when [LocalMagiskInstaller] is ready to be used. + * @param onReady A callback to be invoked when [LocalMagiskRootInstaller] is ready to be used. * * @throws NoRootPermissionException If the device does not have root permission. * @@ -17,22 +17,22 @@ import java.io.Closeable * @see LocalShellCommandRunner */ @Suppress("unused") -class LocalMagiskInstaller private constructor( +class LocalMagiskRootInstaller private constructor( context: Context, - onReady: LocalMagiskInstaller.() -> Unit, + onReady: LocalMagiskRootInstaller.() -> Unit, private val readyHook: Array<(() -> Unit)?>, -) : MagiskInstaller( +) : MagiskRootInstaller( { LocalShellCommandRunner(context) { readyHook[0]?.invoke() } }, ), Closeable { constructor( context: Context, - onReady: LocalMagiskInstaller.() -> Unit = {}, + onReady: LocalMagiskRootInstaller.() -> Unit = {}, ) : this(context, onReady, arrayOfNulls(1)) init { - // The supplier passed to [MagiskInstaller] runs during super-init, before `this` + // The supplier passed to [MagiskRootInstaller] runs during super-init, before `this` // exists as a subclass reference, so the ready callback cannot capture it directly. // Instead we route through [readyHook], which is populated here — safe because // [LocalShellCommandRunner.onServiceConnected] fires asynchronously after IPC bind. diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbMagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbMagiskInstaller.kt index 62261f3..a3c0484 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbMagiskInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbMagiskInstaller.kt @@ -3,18 +3,18 @@ package app.revanced.library.installation.installer import app.revanced.library.installation.command.AdbShellCommandRunner /** - * [AdbMagiskInstaller] for installing and uninstalling [Apk] files using ADB root permissions via Magisk modules. + * [AdbMagiskRootInstaller] for installing and uninstalling [Apk] files using ADB root permissions via Magisk modules. * * @param deviceSerial The device serial. If null, the first connected device will be used. * * @throws NoRootPermissionException If the device does not have root permission. * - * @see MagiskInstaller + * @see MagiskRootInstaller * @see AdbShellCommandRunner */ -class AdbMagiskInstaller( +class AdbMagiskRootInstaller( deviceSerial: String? = null, -) : MagiskInstaller({ AdbShellCommandRunner(deviceSerial) }) { +) : MagiskRootInstaller({ AdbShellCommandRunner(deviceSerial) }) { init { logger.fine("Connected to $deviceSerial") } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index 4b53563..4849289 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -10,7 +10,7 @@ object Constants { const val MOUNTED_APK_PATH = "$MOUNT_PATH$PLACEHOLDER.apk" const val MOUNT_SCRIPT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh" const val HANDLE_DISABLED_SCRIPT_PATH = "/data/adb/service.d/revanced_handle_disabled_$PLACEHOLDER.sh" - const val MAGISK_MODULE_PATH = "/data/adb/modules/revanced_$PLACEHOLDER" + const val MODULE_PATH = "/data/adb/modules/revanced_$PLACEHOLDER" const val EXISTS = "[[ -f $PLACEHOLDER ]] || exit 1" const val MOUNT_GREP = "grep -F $PLACEHOLDER /proc/mounts" @@ -41,7 +41,7 @@ object Constants { * * Placeholders: __FORMATTED_PKG__ (original with dots→underscores), __VERSION__, __LABEL__ */ - val MAGISK_MODULE_PROP = """ + val MODULE_PROP = """ id=revanced___FORMATTED_PKG__ name=__PKG_NAME__ ReVanced version=1.0 @@ -58,7 +58,7 @@ object Constants { * Placeholders: __PKG_NAME__ (unpatched), __PATCHED_PKG__ (patched), * __FORMATTED_PKG__ (unpatched package name with dots replaced by underscores, used as Magisk module ID) */ - val MAGISK_UNINSTALL_SCRIPT = """ + val MODULE_UNINSTALL_SCRIPT = """ #!/system/bin/sh pm uninstall "__PATCHED_PKG__" rm -f "/data/adb/revanced/__PKG_NAME__.apk" diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt index 7bfff25..75df731 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt @@ -5,9 +5,9 @@ import app.revanced.library.installation.installer.Constants.DELETE import app.revanced.library.installation.installer.Constants.EXISTS import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH import app.revanced.library.installation.installer.Constants.KILL -import app.revanced.library.installation.installer.Constants.MAGISK_MODULE_PATH -import app.revanced.library.installation.installer.Constants.MAGISK_MODULE_PROP -import app.revanced.library.installation.installer.Constants.MAGISK_UNINSTALL_SCRIPT +import app.revanced.library.installation.installer.Constants.MODULE_PATH +import app.revanced.library.installation.installer.Constants.MODULE_PROP +import app.revanced.library.installation.installer.Constants.MODULE_UNINSTALL_SCRIPT import app.revanced.library.installation.installer.Constants.MODULE_PROP_FILE import app.revanced.library.installation.installer.Constants.MODULE_SERVICE_SCRIPT import app.revanced.library.installation.installer.Constants.MOUNTED_APK_PATH @@ -20,27 +20,20 @@ import app.revanced.library.installation.installer.Constants.UNINSTALL_SCRIPT_FI import app.revanced.library.installation.installer.Constants.invoke /** - * [MagiskInstaller] for installing and uninstalling [Apk] files using root permissions via Magisk modules. + * [MagiskRootInstaller] for installing and uninstalling [Apk] files using root permissions via Magisk modules. * * @param shellCommandRunnerSupplier A supplier for the [ShellCommandRunner] to use. * * @throws NoRootPermissionException If the device does not have root permission. */ @Suppress("MemberVisibilityCanBePrivate") -abstract class MagiskInstaller internal constructor( +abstract class MagiskRootInstaller internal constructor( shellCommandRunnerSupplier: () -> ShellCommandRunner, ) : RootInstaller(shellCommandRunnerSupplier) { /** * Installs the given [apk] as a Magisk module. * - * The patched APK is prepared at the unified source-of-truth path - * `/data/adb/revanced//base.apk` (the same location used by the - * non-Magisk root installer), and the module ships a `service.sh` that - * installs the patched APK on every boot. After preparation, - * `service.sh` is executed inline so the install takes effect immediately, - * without requiring a reboot. - * * @param apk The [Apk] to install. * * @throws PackageNameRequiredException If the [Apk] does not have a package name. @@ -50,7 +43,7 @@ abstract class MagiskInstaller internal constructor( val packageName = apk.packageName?.also { it.assertInstalled() } ?: throw PackageNameRequiredException() val formattedPackageName = packageName.replace('.', '_') - val modulePath = MAGISK_MODULE_PATH(formattedPackageName) + val modulePath = MODULE_PATH(formattedPackageName) // Prepare the patched APK at the unified source-of-truth path. apk.file.move(TMP_FILE_PATH) @@ -60,7 +53,7 @@ abstract class MagiskInstaller internal constructor( "mkdir -p $modulePath"().waitFor() // Write module.prop. - val moduleProp = MAGISK_MODULE_PROP + val moduleProp = MODULE_PROP .replace("__FORMATTED_PKG__", formattedPackageName) .replace("__PKG_NAME__", packageName) "$modulePath/$MODULE_PROP_FILE".write(moduleProp) @@ -71,7 +64,7 @@ abstract class MagiskInstaller internal constructor( "chmod +x $serviceScriptPath"().waitFor() // Write uninstall.sh — Magisk runs this when the module is removed via the Magisk app. - "$modulePath/$UNINSTALL_SCRIPT_FILE".write(MAGISK_UNINSTALL_SCRIPT.replace("__PKG_NAME__", packageName)) + "$modulePath/$UNINSTALL_SCRIPT_FILE".write(MODULE_UNINSTALL_SCRIPT.replace("__PKG_NAME__", packageName)) "chmod +x $modulePath/$UNINSTALL_SCRIPT_FILE"() // Live trigger: execute service.sh now so the install takes effect without a reboot. @@ -97,7 +90,7 @@ abstract class MagiskInstaller internal constructor( UMOUNT(packageName)() // Remove the Magisk module directory. - DELETE(MAGISK_MODULE_PATH(formattedPackageName))().waitFor() + DELETE(MODULE_PATH(formattedPackageName))().waitFor() // Remove the unified source APK. DELETE(MOUNTED_APK_PATH(packageName))().waitFor() @@ -112,7 +105,7 @@ abstract class MagiskInstaller internal constructor( override suspend fun getInstallation(packageName: String): RootInstallation? { val formattedPackageName = packageName.replace('.', '_') - val modulePath = MAGISK_MODULE_PATH(formattedPackageName) + val modulePath = MODULE_PATH(formattedPackageName) val moduleExists = EXISTS("$modulePath/module.prop")().exitCode == 0 if (!moduleExists) return null From 3d020fa6eb778fd47c2bf2147d74dab1d1ba917f Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Wed, 15 Apr 2026 14:33:43 +0100 Subject: [PATCH 37/47] refactor: Accept `ShellCommandRunner` directly instead of a supplier --- .../installation/installer/LocalMagiskInstaller.kt | 10 +++++----- .../installation/installer/LocalRootInstaller.kt | 10 +++++----- .../installation/installer/AdbMagiskInstaller.kt | 2 +- .../library/installation/installer/AdbRootInstaller.kt | 2 +- .../library/installation/installer/MagiskInstaller.kt | 6 +++--- .../library/installation/installer/RootInstaller.kt | 9 ++------- 6 files changed, 17 insertions(+), 22 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt index 79940aa..8b3e25e 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt @@ -22,7 +22,7 @@ class LocalMagiskRootInstaller private constructor( onReady: LocalMagiskRootInstaller.() -> Unit, private val readyHook: Array<(() -> Unit)?>, ) : MagiskRootInstaller( - { LocalShellCommandRunner(context) { readyHook[0]?.invoke() } }, + LocalShellCommandRunner(context) { readyHook[0]?.invoke() } ), Closeable { @@ -32,10 +32,10 @@ class LocalMagiskRootInstaller private constructor( ) : this(context, onReady, arrayOfNulls(1)) init { - // The supplier passed to [MagiskRootInstaller] runs during super-init, before `this` - // exists as a subclass reference, so the ready callback cannot capture it directly. - // Instead we route through [readyHook], which is populated here — safe because - // [LocalShellCommandRunner.onServiceConnected] fires asynchronously after IPC bind. + // `this` doesn't exist as a subclass reference until after super-init, so the + // ready callback cannot capture it directly in the constructor argument above. + // Routing through [readyHook] is safe because [LocalShellCommandRunner.onServiceConnected] + // fires asynchronously after IPC bind — well after this init block completes. readyHook[0] = { onReady() } } diff --git a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalRootInstaller.kt b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalRootInstaller.kt index 13e5710..7dde15e 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalRootInstaller.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalRootInstaller.kt @@ -22,7 +22,7 @@ class LocalRootInstaller private constructor( onReady: LocalRootInstaller.() -> Unit, private val readyHook: Array<(() -> Unit)?>, ) : RootInstaller( - { LocalShellCommandRunner(context) { readyHook[0]?.invoke() } }, + LocalShellCommandRunner(context) { readyHook[0]?.invoke() } ), Closeable { @@ -32,10 +32,10 @@ class LocalRootInstaller private constructor( ) : this(context, onReady, arrayOfNulls(1)) init { - // The supplier passed to [RootInstaller] runs during super-init, before `this` - // exists as a subclass reference, so the ready callback cannot capture it directly. - // Instead we route through [readyHook], which is populated here — safe because - // [LocalShellCommandRunner.onServiceConnected] fires asynchronously after IPC bind. + // `this` doesn't exist as a subclass reference until after super-init, so the + // ready callback cannot capture it directly in the constructor argument above. + // Routing through [readyHook] is safe because [LocalShellCommandRunner.onServiceConnected] + // fires asynchronously after IPC bind — well after this init block completes. readyHook[0] = { onReady() } } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbMagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbMagiskInstaller.kt index a3c0484..72d71b3 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbMagiskInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbMagiskInstaller.kt @@ -14,7 +14,7 @@ import app.revanced.library.installation.command.AdbShellCommandRunner */ class AdbMagiskRootInstaller( deviceSerial: String? = null, -) : MagiskRootInstaller({ AdbShellCommandRunner(deviceSerial) }) { +) : MagiskRootInstaller(AdbShellCommandRunner(deviceSerial)) { init { logger.fine("Connected to $deviceSerial") } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbRootInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbRootInstaller.kt index d2d771f..ebc61c9 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbRootInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbRootInstaller.kt @@ -16,7 +16,7 @@ import app.revanced.library.installation.installer.RootInstaller.NoRootPermissio */ class AdbRootInstaller( deviceSerial: String? = null, -) : RootInstaller({ AdbShellCommandRunner(deviceSerial) }) { +) : RootInstaller(AdbShellCommandRunner(deviceSerial)) { init { logger.fine("Connected to $deviceSerial") } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt index 75df731..26c5981 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt @@ -22,14 +22,14 @@ import app.revanced.library.installation.installer.Constants.invoke /** * [MagiskRootInstaller] for installing and uninstalling [Apk] files using root permissions via Magisk modules. * - * @param shellCommandRunnerSupplier A supplier for the [ShellCommandRunner] to use. + * @param shellCommandRunner The [ShellCommandRunner] to use. * * @throws NoRootPermissionException If the device does not have root permission. */ @Suppress("MemberVisibilityCanBePrivate") abstract class MagiskRootInstaller internal constructor( - shellCommandRunnerSupplier: () -> ShellCommandRunner, -) : RootInstaller(shellCommandRunnerSupplier) { + shellCommandRunner: ShellCommandRunner, +) : RootInstaller(shellCommandRunner) { /** * Installs the given [apk] as a Magisk module. diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt index 0be0cb4..89c953f 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt @@ -21,20 +21,15 @@ import java.io.File /** * [RootInstaller] for installing and uninstalling [Apk] files using root permissions by mounting. * - * @param shellCommandRunnerSupplier A supplier for the [ShellCommandRunner] to use. + * @param shellCommandRunner The [ShellCommandRunner] to use. * * @throws NoRootPermissionException If the device does not have root permission. */ @Suppress("MemberVisibilityCanBePrivate") abstract class RootInstaller internal constructor( - shellCommandRunnerSupplier: () -> ShellCommandRunner, + protected val shellCommandRunner: ShellCommandRunner, ) : Installer() { - /** - * The command runner used to run commands on the device. - */ - protected val shellCommandRunner = shellCommandRunnerSupplier() - init { if (!shellCommandRunner.hasRootPermission()) throw NoRootPermissionException() } From 55f752cedd5c62d4fcd256664d96a1636379f95d Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Wed, 15 Apr 2026 14:34:50 +0100 Subject: [PATCH 38/47] fix: `pm uninstall` freshly-installed apps on module uninstall --- .../installation/installer/MagiskInstaller.kt | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt index 26c5981..22c92f1 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt @@ -41,10 +41,13 @@ abstract class MagiskRootInstaller internal constructor( override suspend fun install(apk: Apk): RootInstallerResult { logger.info("Installing ${apk.packageName} as a Magisk module") - val packageName = apk.packageName?.also { it.assertInstalled() } ?: throw PackageNameRequiredException() + val packageName = apk.packageName ?: throw PackageNameRequiredException() val formattedPackageName = packageName.replace('.', '_') val modulePath = MODULE_PATH(formattedPackageName) + // Track whether the app was already on-device so uninstall() knows whether to pm uninstall. + val isPreInstalled = INSTALLED_APK_PATH(packageName)().output.isNotEmpty() + // Prepare the patched APK at the unified source-of-truth path. apk.file.move(TMP_FILE_PATH) prepareApk(packageName) @@ -60,13 +63,21 @@ abstract class MagiskRootInstaller internal constructor( // Write service.sh — Magisk runs this on every boot to install the patched APK. val serviceScriptPath = "$modulePath/$SERVICE_SCRIPT_FILE" - serviceScriptPath.write(MODULE_SERVICE_SCRIPT.replace("__PKG_NAME__", packageName)) + serviceScriptPath.write(MODULE_SERVICE_SCRIPT + .replace("__PKG_NAME__", packageName) + .replace("__PATCHED_PKG__", packageName)) "chmod +x $serviceScriptPath"().waitFor() // Write uninstall.sh — Magisk runs this when the module is removed via the Magisk app. - "$modulePath/$UNINSTALL_SCRIPT_FILE".write(MODULE_UNINSTALL_SCRIPT.replace("__PKG_NAME__", packageName)) + "$modulePath/$UNINSTALL_SCRIPT_FILE".write(MODULE_UNINSTALL_SCRIPT + .replace("__PKG_NAME__", packageName) + .replace("__PATCHED_PKG__", packageName) + .replace("__FORMATTED_PKG__", formattedPackageName)) "chmod +x $modulePath/$UNINSTALL_SCRIPT_FILE"() + // Mark as newly installed so uninstall() also calls pm uninstall. + if (!isPreInstalled) "$modulePath/.newly_installed".write("") + // Live trigger: execute service.sh now so the install takes effect without a reboot. "sh $serviceScriptPath"().waitFor() @@ -85,12 +96,16 @@ abstract class MagiskRootInstaller internal constructor( logger.info("Uninstalling $packageName Magisk module") val formattedPackageName = packageName.replace('.', '_') + val modulePath = MODULE_PATH(formattedPackageName) + + // Read the flag before removing the module directory. + val newlyInstalled = EXISTS("$modulePath/.newly_installed")().exitCode == 0 // Live unmount so the stock APK is restored immediately. UMOUNT(packageName)() // Remove the Magisk module directory. - DELETE(MODULE_PATH(formattedPackageName))().waitFor() + DELETE(modulePath)().waitFor() // Remove the unified source APK. DELETE(MOUNTED_APK_PATH(packageName))().waitFor() @@ -98,6 +113,9 @@ abstract class MagiskRootInstaller internal constructor( // Clean up any residual tmp file. DELETE(TMP_FILE_PATH)() + // If the app was freshly installed by the module, fully remove it. + if (newlyInstalled) "pm uninstall $packageName"() + KILL(packageName)() return RootInstallerResult.SUCCESS From b9d0a1bf8a9ee0605fff735cf2669cecb45940c5 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Wed, 15 Apr 2026 17:20:23 +0100 Subject: [PATCH 39/47] refactor: Comment out unused `extractNativeLibraries` --- .../app/revanced/library/MagiskUtils.kt | 98 ++++++++++++------- 1 file changed, 61 insertions(+), 37 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index 56a7740..c51654d 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -5,7 +5,6 @@ import app.revanced.library.installation.installer.Constants.invoke import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.nio.FileSystemManager import java.io.File -import java.util.zip.ZipFile object MagiskUtils { const val MODULES_PATH = "/data/adb/modules" @@ -109,42 +108,67 @@ object MagiskUtils { .also { if (!it) throw Exception("Failed to delete Magisk module files") } } - fun extractNativeLibraries(apkFile: File, systemAppPath: String, remoteFS: FileSystemManager) { - val libPath = "$systemAppPath/lib" - 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) - } - } - } - } - } + /* + * When bind-mounting a patched APK over the unpatched/stock APK, Android's PM + * does not re-extract native libraries - it reuses whatever it already extracted from + * the unpatched APK at install time. If the patched APK introduces a new .so file that was + * never in the stock APK (i.e. libelements.so added by a Rev YT patch), PM's + * lib directory will never contain it, causing a crash at runtime: + * > UnsatisfiedLinkError: dlopen failed: library "libelements.so" not found + * + * This function addresses that by manually unpacking every .so from the patched APK into + * the app's target lib directory, fixing that issue + * + * This was commented out because: + * 1. Not needed for the Magisk module install path - service.sh calls pm install, so PM + * installs the patched APK fresh and extracts all native libs automatically + * + * 2. This logic would need to be moved since it belongs in RootInstaller.install() + * + * 3. Some potential issue is that writing to [system app's path]/lib would fail on read-only + * system partition (/system/app, /product/app, etc) + * Right now it's assuming it can write to it - Wrong! + * + * Considering the points above, if the bind-mount path needs to support patches + * that introduce new native libraries, this could very well be used + * + * fun extractNativeLibraries(apkFile: File, systemAppPath: String, remoteFS: FileSystemManager) { + * val libPath = "$systemAppPath/lib" + * 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) + * } + * } + * } + * } + * } + */ fun installApk(apkPath: String) = Shell.getShell().newJob() From 995d1552f2244cd51d2842ebcf08b8e77db06e47 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sat, 18 Apr 2026 14:33:37 +0100 Subject: [PATCH 40/47] refactor: Use current user instead of static value --- .../kotlin/app/revanced/library/MagiskUtils.kt | 7 ++++++- .../library/installation/installer/Constants.kt | 10 +++++----- .../library/installation/installer/MagiskInstaller.kt | 3 ++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index c51654d..f6013f1 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -9,6 +9,9 @@ import java.io.File object MagiskUtils { const val MODULES_PATH = "/data/adb/modules" + // Android user (0 for primary, 10+ for secondary/work profiles) via pure public API. + private val currentUserId = android.os.Process.myUid() / 100000 + /* Shell.isAppGrantedRoot() queries libsu's internal state. It returns false until a root shell has actually been built and verified by libsu. @@ -172,7 +175,7 @@ object MagiskUtils { fun installApk(apkPath: String) = Shell.getShell().newJob() - .add("pm install -r -d --user 0 \"$apkPath\"") + .add("pm install -r -d --user $currentUserId \"$apkPath\"") .exec() .assertSuccess("Failed to install APK: $apkPath") @@ -206,6 +209,7 @@ object MagiskUtils { val handleDisabledSh = Constants.HANDLE_DISABLED_SCRIPT .replace("__PATCHED_PKG__", patchedPackageName) .replace("__FORMATTED_PKG__", formattedPackageName) + .replace("__USER_ID__", currentUserId.toString()) remoteFS.getFile(handleDisabledScriptPath).newOutputStream().use { it.write(handleDisabledSh.toByteArray()) } // Source of truth APK @@ -271,6 +275,7 @@ object MagiskUtils { val serviceSh = Constants.MODULE_SERVICE_SCRIPT .replace("__PKG_NAME__", packageName) .replace("__PATCHED_PKG__", patchedPackageName) + .replace("__USER_ID__", currentUserId.toString()) remoteFS.getFile("$modulePath/service.sh").newOutputStream().use { it.write(serviceSh.toByteArray()) } val uninstallSh = Constants.MODULE_UNINSTALL_SCRIPT diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index 4849289..098abbd 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -92,7 +92,7 @@ object Constants { current_boot_id=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) stored_boot_id=$(cat "${module_path}/.boot_token" 2>/dev/null) if [ "${stored_boot_id}" != "${current_boot_id}" ]; then - pm disable-user --user 0 "${patched_pkg}" 2>/dev/null + pm disable-user --user __USER_ID__ "${patched_pkg}" 2>/dev/null fi """.trimIndent() @@ -170,7 +170,7 @@ object Constants { # Wait until PM is fully responsive — sys.boot_completed=1 fires before the PM # binder handles transactions. Poll until it returns at least one package entry. - until pm list packages --user 0 2>/dev/null | grep -q "^package:"; do sleep 5; done + until pm list packages --user __USER_ID__ 2>/dev/null | grep -q "^package:"; do sleep 5; done base_path="/data/adb/revanced/__PKG_NAME__.apk" @@ -183,8 +183,8 @@ object Constants { # Re-enable the app if it was disabled by the handle-disabled script (module was toggled off # then back on). If not installed at all, fall through to the install block. - if pm list packages --user 0 | grep -q "^package:${package_name}$"; then - pm enable --user 0 "${package_name}" 2>/dev/null + if pm list packages --user __USER_ID__ | grep -q "^package:${package_name}$"; then + pm enable --user __USER_ID__ "${package_name}" 2>/dev/null echo "Package enabled." else # Retry loop — sys.boot_completed=1 fires before the PM binder is stable for @@ -201,7 +201,7 @@ object Constants { while [ ${attempt} -lt ${max_retries} ] && [ ${install_exit} -ne 0 ]; do attempt=$((attempt + 1)) echo "Install attempt ${attempt}/${max_retries}..." - pm install -r -d --user 0 -S $(stat -c%s "${base_path}") < "${base_path}" + pm install -r -d --user __USER_ID__ -S $(stat -c%s "${base_path}") < "${base_path}" install_exit=$? echo "Install exit code: ${install_exit}" if [ ${install_exit} -ne 0 ] && [ ${attempt} -lt ${max_retries} ]; then diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt index 22c92f1..0f582a2 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt @@ -65,7 +65,8 @@ abstract class MagiskRootInstaller internal constructor( val serviceScriptPath = "$modulePath/$SERVICE_SCRIPT_FILE" serviceScriptPath.write(MODULE_SERVICE_SCRIPT .replace("__PKG_NAME__", packageName) - .replace("__PATCHED_PKG__", packageName)) + .replace("__PATCHED_PKG__", packageName) + .replace("__USER_ID__", "0")) "chmod +x $serviceScriptPath"().waitFor() // Write uninstall.sh — Magisk runs this when the module is removed via the Magisk app. From b65883fce8b6b696b717d6324c086fc0e65ac60b Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sat, 18 Apr 2026 14:42:19 +0100 Subject: [PATCH 41/47] refactor: Remove obselete shell helper --- .../androidMain/kotlin/app/revanced/library/MagiskUtils.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt index f6013f1..62cce8a 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt @@ -173,12 +173,6 @@ object MagiskUtils { * } */ - fun installApk(apkPath: String) = - Shell.getShell().newJob() - .add("pm install -r -d --user $currentUserId \"$apkPath\"") - .exec() - .assertSuccess("Failed to install APK: $apkPath") - fun uninstallKeepData(packageName: String) = Shell.getShell().newJob() .add("pm uninstall -k \"$packageName\"") From ce3e0e6aa28b5da98f1f0d268920f2b3700b557f Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sat, 18 Apr 2026 13:47:40 +0000 Subject: [PATCH 42/47] refactor: Better comment wording Co-authored-by: oSumAtrIX --- .../app/revanced/library/installation/installer/Constants.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index 4849289..1f9d352 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -151,7 +151,7 @@ object Constants { package_name="__PATCHED_PKG__" - # Write a boot token so the handle-disabled script can detect whether service.sh ran this boot. + # Write a boot token so the handle-disabled script can detect whether the module was enabled this boot. cp /proc/sys/kernel/random/boot_id "${DIR}/.boot_token" LOG="${DIR}/log" From 91c4b87a13bad12de1f9177b9058812d4b71746d Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sat, 18 Apr 2026 14:48:47 +0100 Subject: [PATCH 43/47] refactor: File renamings --- .../{LocalMagiskInstaller.kt => LocalMagiskRootInstaller.kt} | 0 .../{AdbMagiskInstaller.kt => AdbMagiskRootInstaller.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename library/src/androidMain/kotlin/app/revanced/library/installation/installer/{LocalMagiskInstaller.kt => LocalMagiskRootInstaller.kt} (100%) rename library/src/commonMain/kotlin/app/revanced/library/installation/installer/{AdbMagiskInstaller.kt => AdbMagiskRootInstaller.kt} (100%) diff --git a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskRootInstaller.kt similarity index 100% rename from library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskInstaller.kt rename to library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskRootInstaller.kt diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbMagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbMagiskRootInstaller.kt similarity index 100% rename from library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbMagiskInstaller.kt rename to library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbMagiskRootInstaller.kt From dc438d2ceab67a7a1d5985bda13fb0b28145fd8e Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sat, 18 Apr 2026 15:00:53 +0100 Subject: [PATCH 44/47] refactor: Styling and wording --- .../installation/installer/LocalMagiskRootInstaller.kt | 2 +- .../library/installation/installer/LocalRootInstaller.kt | 2 +- .../revanced/library/installation/installer/Constants.kt | 6 +++--- .../library/installation/installer/MagiskInstaller.kt | 4 ++-- .../library/installation/installer/RootInstaller.kt | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskRootInstaller.kt b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskRootInstaller.kt index 8b3e25e..5e36ffd 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskRootInstaller.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalMagiskRootInstaller.kt @@ -35,7 +35,7 @@ class LocalMagiskRootInstaller private constructor( // `this` doesn't exist as a subclass reference until after super-init, so the // ready callback cannot capture it directly in the constructor argument above. // Routing through [readyHook] is safe because [LocalShellCommandRunner.onServiceConnected] - // fires asynchronously after IPC bind — well after this init block completes. + // fires asynchronously after IPC bind - well after this init block completes. readyHook[0] = { onReady() } } diff --git a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalRootInstaller.kt b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalRootInstaller.kt index 7dde15e..0411dc6 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalRootInstaller.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalRootInstaller.kt @@ -35,7 +35,7 @@ class LocalRootInstaller private constructor( // `this` doesn't exist as a subclass reference until after super-init, so the // ready callback cannot capture it directly in the constructor argument above. // Routing through [readyHook] is safe because [LocalShellCommandRunner.onServiceConnected] - // fires asynchronously after IPC bind — well after this init block completes. + // fires asynchronously after IPC bind - well after this init block completes. readyHook[0] = { onReady() } } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index 64c0ba3..8810556 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -87,7 +87,7 @@ object Constants { exit 0 fi - # If service.sh did not run this boot, the module is disabled — disable the app so it + # If service.sh did not run this boot, the module is disabled - disable the app so it # disappears from the launcher without losing data. service.sh re-enables it on next boot. current_boot_id=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) stored_boot_id=$(cat "${module_path}/.boot_token" 2>/dev/null) @@ -168,7 +168,7 @@ object Constants { until [ "$(getprop sys.boot_completed)" = 1 ]; do sleep 5; done - # Wait until PM is fully responsive — sys.boot_completed=1 fires before the PM + # Wait until PM is fully responsive - sys.boot_completed=1 fires before the PM # binder handles transactions. Poll until it returns at least one package entry. until pm list packages --user __USER_ID__ 2>/dev/null | grep -q "^package:"; do sleep 5; done @@ -187,7 +187,7 @@ object Constants { pm enable --user __USER_ID__ "${package_name}" 2>/dev/null echo "Package enabled." else - # Retry loop — sys.boot_completed=1 fires before the PM binder is stable for + # Retry loop - sys.boot_completed=1 fires before the PM binder is stable for # write transactions, causing "Failed transaction" errors. Pipe-based install # (pm install -S size < file) uses a simpler code path than session-based # (install-create/write/commit) and is less prone to early-boot binder failures. diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt index 0f582a2..16bae43 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt @@ -61,7 +61,7 @@ abstract class MagiskRootInstaller internal constructor( .replace("__PKG_NAME__", packageName) "$modulePath/$MODULE_PROP_FILE".write(moduleProp) - // Write service.sh — Magisk runs this on every boot to install the patched APK. + // Write service.sh - Magisk runs this on every boot to install the patched APK. val serviceScriptPath = "$modulePath/$SERVICE_SCRIPT_FILE" serviceScriptPath.write(MODULE_SERVICE_SCRIPT .replace("__PKG_NAME__", packageName) @@ -69,7 +69,7 @@ abstract class MagiskRootInstaller internal constructor( .replace("__USER_ID__", "0")) "chmod +x $serviceScriptPath"().waitFor() - // Write uninstall.sh — Magisk runs this when the module is removed via the Magisk app. + // Write uninstall.sh - Magisk runs this when the module is removed via the Magisk app. "$modulePath/$UNINSTALL_SCRIPT_FILE".write(MODULE_UNINSTALL_SCRIPT .replace("__PKG_NAME__", packageName) .replace("__PATCHED_PKG__", packageName) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt index 89c953f..1959c09 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt @@ -35,8 +35,8 @@ abstract class RootInstaller internal constructor( } /** - * Prepares the APK from [TMP_FILE_PATH] to the unified source-of-truth path for [packageName], - * creating the directory and applying permissions/SELinux context. + * Prepares the APK from tmp file path "[TMP_FILE_PATH]" by saving it to a + * persistent location and applying permissions/SELinux context. */ protected fun prepareApk(packageName: String) = PREPARE_APK(packageName)().waitFor() From 547764da4ac6c053a43f4cdefe333cf52f29c230 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sat, 18 Apr 2026 15:07:46 +0100 Subject: [PATCH 45/47] refactor: Rename `.boot_token` to `.last_boot_id` --- .../app/revanced/library/installation/installer/Constants.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index 8810556..8a72ab7 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -90,7 +90,7 @@ object Constants { # If service.sh did not run this boot, the module is disabled - disable the app so it # disappears from the launcher without losing data. service.sh re-enables it on next boot. current_boot_id=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) - stored_boot_id=$(cat "${module_path}/.boot_token" 2>/dev/null) + stored_boot_id=$(cat "${module_path}/.last_boot_id" 2>/dev/null) if [ "${stored_boot_id}" != "${current_boot_id}" ]; then pm disable-user --user __USER_ID__ "${patched_pkg}" 2>/dev/null fi @@ -152,7 +152,7 @@ object Constants { package_name="__PATCHED_PKG__" # Write a boot token so the handle-disabled script can detect whether the module was enabled this boot. - cp /proc/sys/kernel/random/boot_id "${DIR}/.boot_token" + cp /proc/sys/kernel/random/boot_id "${DIR}/.last_boot_id" LOG="${DIR}/log" MAX_LOG_LINES=200 From d1f1076f0d6a26e550d58f2f50af56cf42268375 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sat, 18 Apr 2026 15:43:26 +0100 Subject: [PATCH 46/47] refactor: Remove redundant step for path creation --- .../revanced/library/installation/installer/RootInstaller.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt index 1959c09..dc6b85a 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt @@ -1,7 +1,6 @@ package app.revanced.library.installation.installer import app.revanced.library.installation.command.ShellCommandRunner -import app.revanced.library.installation.installer.Constants.CREATE_INSTALLATION_PATH import app.revanced.library.installation.installer.Constants.DELETE import app.revanced.library.installation.installer.Constants.EXISTS import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH @@ -54,7 +53,6 @@ abstract class RootInstaller internal constructor( // Setup files. apk.file.move(TMP_FILE_PATH) - CREATE_INSTALLATION_PATH(packageName)().waitFor() prepareApk(packageName) // Install and run. From 2d19f45d2d3d3a3cbe97dde0ef2a2968c6191ca0 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Sat, 18 Apr 2026 15:49:39 +0100 Subject: [PATCH 47/47] fix: Hardcode `mounted=false` in MagiskRootInstaller --- .../library/installation/installer/MagiskInstaller.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt index 16bae43..27663af 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/MagiskInstaller.kt @@ -11,7 +11,6 @@ import app.revanced.library.installation.installer.Constants.MODULE_UNINSTALL_SC import app.revanced.library.installation.installer.Constants.MODULE_PROP_FILE import app.revanced.library.installation.installer.Constants.MODULE_SERVICE_SCRIPT import app.revanced.library.installation.installer.Constants.MOUNTED_APK_PATH -import app.revanced.library.installation.installer.Constants.MOUNT_GREP import app.revanced.library.installation.installer.Constants.RESTART import app.revanced.library.installation.installer.Constants.SERVICE_SCRIPT_FILE import app.revanced.library.installation.installer.Constants.TMP_FILE_PATH @@ -129,11 +128,10 @@ abstract class MagiskRootInstaller internal constructor( val moduleExists = EXISTS("$modulePath/module.prop")().exitCode == 0 if (!moduleExists) return null - val patchedApkPath = MOUNTED_APK_PATH(packageName) return RootInstallation( INSTALLED_APK_PATH(packageName)().output.ifEmpty { null }, - patchedApkPath, - MOUNT_GREP(patchedApkPath)().exitCode == 0, + MOUNTED_APK_PATH(packageName), + false, // Magisk module install uses pm install, not bind-mount. ) } }