Skip to content
Open
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
85dd550
feat: Magisk module installer
secp192k1 Apr 7, 2026
9ba4e3e
fix: Ensure dex streams are closed to prevent resource leaks
secp192k1 Apr 7, 2026
a283be1
feat: Migration of magisk logic from `manager` to `library`
secp192k1 Apr 7, 2026
b180aea
docs: Explain boot wait loop and sync induction behavior
secp192k1 Apr 7, 2026
6299737
refactor: Improve mount safety and path detection
secp192k1 Apr 7, 2026
647e7d6
refactor: Better `service.sh` logging
secp192k1 Apr 7, 2026
bba8eb3
fix: Add missing check for already overlayed app
secp192k1 Apr 7, 2026
3e9dbc9
refactor: Proceed with remount if already mounted/overlayed
secp192k1 Apr 7, 2026
1d972f1
refactor: Human readable logging
secp192k1 Apr 7, 2026
f6e0783
feat: Add interactive volume key support and SELinux permission enfor…
secp192k1 Apr 7, 2026
fa4e883
refactor: Moved service to `Constants.kt`
secp192k1 Apr 7, 2026
b026668
refactor: Optimization and reuse of operations
secp192k1 Apr 7, 2026
c2d57b7
refactor: Removed redundant code
secp192k1 Apr 7, 2026
35ab7dc
refactor: Generalize induction constants and optimize installer to us…
secp192k1 Apr 7, 2026
2c4a8d6
refactor: Implement unified source-and-trigger architecture for root …
secp192k1 Apr 7, 2026
7b86534
refactor: Consolidate induction templates and synchronize Magisk inst…
secp192k1 Apr 7, 2026
08ac5dd
refactor: Simplify `ShellCommandRunner` supplier and improve naming c…
secp192k1 Apr 7, 2026
ed0c7a0
feat: Add Magisk module uninstall script template
secp192k1 Apr 7, 2026
dda483b
refactor: Align `MagiskInstaller` with unified source-and-trigger arc…
secp192k1 Apr 7, 2026
e022803
fix: Restore `LocalMagiskInstaller` build after supplier signature ch…
secp192k1 Apr 7, 2026
09b39f8
fix: Restore `LocalRootInstaller` build after supplier signature change
secp192k1 Apr 7, 2026
277e368
feat: Write `uninstall.sh` when provisioning Magisk and root modules
secp192k1 Apr 7, 2026
754bddf
fix: Module integration and migration impl
secp192k1 Apr 9, 2026
2e64ffd
feat: Better module logging
secp192k1 Apr 9, 2026
e4c06cb
Update library/src/commonMain/kotlin/app/revanced/library/installatio…
secp192k1 Apr 12, 2026
c229f11
refactor: Rename confusing and unused vars
secp192k1 Apr 12, 2026
3280af0
Refactor Magisk provisioning and watchdog
secp192k1 Apr 12, 2026
d27169d
revert: Use flat apk path instead of per-package subdirectory
secp192k1 Apr 12, 2026
3119114
refactor: Better descriptive module script
secp192k1 Apr 12, 2026
fe6f102
enhance: Disable instead of uninstall handler
secp192k1 Apr 12, 2026
650e952
refactor: Moved paths for grouping with other paths
secp192k1 Apr 12, 2026
e276095
refactor: Unicode fix
secp192k1 Apr 13, 2026
d0f432e
fix: Removed user param for uninstall
secp192k1 Apr 14, 2026
041519a
refactor: Standardize naming to "prepare"
secp192k1 Apr 14, 2026
84738bb
refactor: Inline single-use constants & clarify placeholders
secp192k1 Apr 14, 2026
6fded1d
refactor: Change 'MAGISK_' prefix in favor of Module
secp192k1 Apr 14, 2026
3d020fa
refactor: Accept `ShellCommandRunner` directly instead of a supplier
secp192k1 Apr 15, 2026
55f752c
fix: `pm uninstall` freshly-installed apps on module uninstall
secp192k1 Apr 15, 2026
b9d0a1b
refactor: Comment out unused `extractNativeLibraries`
secp192k1 Apr 15, 2026
995d155
refactor: Use current user instead of static value
secp192k1 Apr 18, 2026
b65883f
refactor: Remove obselete shell helper
secp192k1 Apr 18, 2026
ce3e0e6
refactor: Better comment wording
secp192k1 Apr 18, 2026
91c4b87
refactor: File renamings
secp192k1 Apr 18, 2026
86887b2
Merge branch 'feat/magisk-module' of https://github.com/secp192k1/rev…
secp192k1 Apr 18, 2026
dc438d2
refactor: Styling and wording
secp192k1 Apr 18, 2026
547764d
refactor: Rename `.boot_token` to `.last_boot_id`
secp192k1 Apr 18, 2026
d1f1076
refactor: Remove redundant step for path creation
secp192k1 Apr 18, 2026
2d19f45
fix: Hardcode `mounted=false` in MagiskRootInstaller
secp192k1 Apr 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
329 changes: 329 additions & 0 deletions library/src/androidMain/kotlin/app/revanced/library/MagiskUtils.kt
Comment thread
secp192k1 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
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

object MagiskUtils {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be internal no?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually looks like these are APIs for callsite? Why are they needed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got an error while testing patching with youtube, it failed to load since there were missing native libraries, so this basically includes the necessary libs

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And MagiskUtils is used directly by the manager, so it can't be internal

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APIs in library should only exist in library if they can/should be shared with cli too, but i cant see how this would apply for a couple of apis introduced here.

Copy link
Copy Markdown
Member Author

@secp192k1 secp192k1 Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be internal no?

MagiskUtils is called directly from the manager's RootInstaller for live mount & unmount operations in a coroutine scope, so it must stay public

APIs in library should only exist in library if they can/should be shared with cli too, but i cant see how this would apply for a couple of apis introduced here.

Understandable but for android specific APIs like prepareMagiskModule & extractNativeLibraries don't make sense for CLI imo, but we could always move it into manager as a specialized internal helper if you want

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MagiskUtils is called directly from the manager's RootInstaller for live mount & unmount operations in a coroutine scope, so it must stay public

Manager should not require any live mount/unmount operations at all, this is business logic of the library installer that manager should use and not reimplement. So The APIs here should be designed to reflect that.

Understandable but for android specific APIs like prepareMagiskModule & extractNativeLibraries don't make sense for CLI imo, but we could always move it into manager as a specialized internal helper if you want

Why not? CLI can prepare a magisk module via ADB too?

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 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) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this function needed? Installers already implement it

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like all these utils functions below

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are called from the manager's own RootInstaller for live mounts outside the library's install lifecycle (re-mounting on app launch without reinstalling). The library installers handle their own mounting on the installation flow

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-mounting on app launch without reinstalling

I dont think manager should have this feature, in which case the utils function should be removable no?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-mounting on app launch without reinstalling

I dont think manager should have this feature, in which case the utils function should be removable no?

Sorry but I dont get what you meant here, I guess you want to get rid of the mount & unmount from the manager and directly call those 2 functions on library?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry but I dont get what you meant here, I guess you want to get rid of the mount & unmount from the manager and directly call those 2 functions on library?

Basically, manager should not provide in-place mount & unmount options. Installation and uninstallation over mount is handled by installers which manager is supposed to call.

if (Shell.getShell().newJob().add("mount | grep -q \"$sourceDir\"").exec().isSuccess) {
unmount(sourceDir)
}

val patchedApkPath = Constants.MOUNTED_APK_PATH(packageName)
val fallbackPath = "$MODULES_PATH/$packageName-revanced/$packageName.apk"

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($$"""
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) {
// 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()
.assertSuccess("Failed to unmount APK")
}

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, patchedPackageName: String, remoteFS: FileSystemManager) {
val unifiedPath = Constants.MOUNTED_APK_PATH(packageName).substringBeforeLast("/")
remoteFS.getFile(unifiedPath).deleteRecursively()

val formattedPackageName = packageName.replace('.', '_')
val handleDisabledScriptPath = Constants.HANDLE_DISABLED_SCRIPT_PATH(formattedPackageName)

Shell.getShell().newJob()
.add("pm uninstall \"$patchedPackageName\"")
.add("rm -f \"$handleDisabledScriptPath\"")
.exec()

remoteFS.getFile("$MODULES_PATH/revanced_$formattedPackageName").deleteRecursively()
.also { if (!it) throw Exception("Failed to delete Magisk module files") }
}

/*
* When bind-mounting a patched APK over the unpatched/stock APK, Android's PM
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this commented out (this should be handled by root installer if not already)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this commented out (this should be handled by root installer if not already)

First, no, its not being handled in by root installer (.install())

For the Magisk module path this is fine since pm install lets the PM extract native libs automatically, however, for the bind-mount path, if a patch introduces a new .so native lib that wasnt in the stock APK, it will crash with UnsatisfiedLinkError (logcat shown before)

I left that function commented-out as a reference for when that's addressed, but when it is, it will belong in root install. But before addressing any of this, remember that writing to the app's lib directory will fail on read-only system partitions (/system/app, /product/app, etc)

* 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()
.add("pm install -r -d --user 0 \"$apkPath\"")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

@secp192k1 secp192k1 Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've implemented that same user id fetching method on my Shizuku implementation here secp192k1@d8a979b#diff-88608bd83b41bf6ff10291c36c0cb227e766cb43d0a8599885cce7a7cd79dac0R16
But eventually just scrapped the idea

@oSumAtrIX what do you think? Keep or replace to dynamic userid?
FYI => #127 (comment)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to specify the user when installing though? Doesnt PM inherit the user that launched the ADB shell/process automatically?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to specify the user when installing though? Doesnt PM inherit the user that launched the ADB shell/process automatically?

If you have multiple users (private space, work profile, etc) this gives issues big time, since pm install tries to install to all users, gotten this "bug" multiple times, this is a race condition that affects production where signatures mismatch OR says app is already installed - yes on main one it isnt, but its still on private space

Plus id say its good practice to define this argument since it would be inherited from where the manager is being ran (Process.myUid() / 100000)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good, in this case use Process.myUid(). This way it will only install for the current user that runs manager. However, then, this should be implemented in the installer constants too.

Why do we need an installApk utility function for manager inside library? The only place where manager does installations is for its own updates or for patched apps. For patched apps it should use the installer classes, for manager updates the functionality should be implemented inside manager repo and not with a shell in library.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good, in this case use Process.myUid(). This way it will only install for the current user that runs manager. However, then, this should be implemented in the installer constants too.

👍 995d155

Why do we need an installApk utility function for manager inside library? The only place where manager does installations is for its own updates or for patched apps. For patched apps it should use the installer classes, for manager updates the functionality should be implemented inside manager repo and not with a shell in library.

👍 b65883f

.exec()
.assertSuccess("Failed to install APK: $apkPath")

fun uninstallKeepData(packageName: String) =
Shell.getShell().newJob()
.add("pm uninstall -k \"$packageName\"")
.exec()

fun prepareMagiskModule(
remoteFS: FileSystemManager,
packageName: String,
patchedPackageName: String,
patchedApk: File
) {
val formattedPackageName = packageName.replace('.', '_')
val modulePath = "$MODULES_PATH/revanced_$formattedPackageName"
val unifiedApkPath = Constants.MOUNTED_APK_PATH(packageName)
val handleDisabledScriptPath = Constants.HANDLE_DISABLED_SCRIPT_PATH(formattedPackageName)

// Ensure directories exist
val unifiedDir = unifiedApkPath.substringBeforeLast("/")
Shell.getShell().newJob()
.add("mkdir -p \"$modulePath\"")
.add("mkdir -p \"$unifiedDir\"")
.exec()
.assertSuccess("Failed to create module directories")

writeModuleFiles(remoteFS, modulePath, packageName, patchedPackageName)

// 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(handleDisabledScriptPath).newOutputStream().use { it.write(handleDisabledSh.toByteArray()) }

// Source of truth APK
copyApk(remoteFS, patchedApk, unifiedApkPath)

// 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 \"$handleDisabledScriptPath\"")
.exec()
.assertSuccess("Failed to set file permissions")
}

fun prepareRootFolder(
remoteFS: FileSystemManager,
packageName: String,
patchedApk: File
) {
val modulePath = "$MODULES_PATH/$packageName-revanced"
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 module directories")

// MOUNT type: patched package name == original package name (bind-mount, no rename)
writeModuleFiles(remoteFS, modulePath, packageName, packageName)

// Source of truth APK
copyApk(remoteFS, patchedApk, unifiedApkPath)

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\"")
.exec()
.assertSuccess("Failed to set file permissions")
}

private fun writeModuleFiles(
remoteFS: FileSystemManager,
modulePath: String,
packageName: String,
patchedPackageName: String,
) {
val formattedPackageName = packageName.replace('.', '_')

val moduleProp = Constants.MODULE_PROP
.replace("__FORMATTED_PKG__", formattedPackageName)
.replace("__PKG_NAME__", packageName)
remoteFS.getFile("$modulePath/module.prop").newOutputStream().use { it.write(moduleProp.toByteArray()) }

val serviceSh = Constants.MODULE_SERVICE_SCRIPT
.replace("__PKG_NAME__", packageName)
.replace("__PATCHED_PKG__", patchedPackageName)
remoteFS.getFile("$modulePath/service.sh").newOutputStream().use { it.write(serviceSh.toByteArray()) }

val uninstallSh = Constants.MODULE_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()) }
}

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)
}
}
}

class ShellCommandException(
val userMessage: String,
val exitCode: Int,
val stdout: List<String>,
val stderr: List<String>
) : Exception(format(userMessage, exitCode, stdout, stderr)) {
companion object {
private fun format(
message: String,
exitCode: Int,
stdout: List<String>,
stderr: List<String>
): 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)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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

/**
* [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 [LocalMagiskRootInstaller] is ready to be used.
*
* @throws NoRootPermissionException If the device does not have root permission.
*
* @see Installer
* @see LocalShellCommandRunner
*/
@Suppress("unused")
class LocalMagiskRootInstaller private constructor(
context: Context,
onReady: LocalMagiskRootInstaller.() -> Unit,
private val readyHook: Array<(() -> Unit)?>,
) : MagiskRootInstaller(
LocalShellCommandRunner(context) { readyHook[0]?.invoke() }
),
Closeable {

constructor(
context: Context,
onReady: LocalMagiskRootInstaller.() -> Unit = {},
) : this(context, onReady, arrayOfNulls(1))

init {
// `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() }
}

override fun close() = (shellCommandRunner as LocalShellCommandRunner).close()
}
Loading
Loading