Files
ISODroid/app/src/main/java/sh/sar/isodroid/root/RootManager.kt

97 lines
2.9 KiB
Kotlin

/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.root
import com.topjohnwu.superuser.Shell
import sh.sar.isodroid.BuildConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
object RootManager {
/**
* Escapes a string for safe use in shell commands.
* Uses single quotes and escapes any single quotes within the string.
* This prevents command injection via $(), ``, ;, &&, ||, etc.
*/
fun shellEscape(s: String): String {
// Single quotes prevent all shell interpretation except for single quotes themselves
// To include a single quote, we end the single-quoted string, add an escaped single quote, and start a new single-quoted string
// Example: "test'file" becomes 'test'\''file'
return "'" + s.replace("'", "'\\''") + "'"
}
init {
Shell.enableVerboseLogging = BuildConfig.DEBUG
Shell.setDefaultBuilder(
Shell.Builder.create()
.setFlags(Shell.FLAG_MOUNT_MASTER)
.setTimeout(10)
)
}
/**
* Check if root was previously granted (cached status, no popup).
* Returns true if granted, false if denied, null if unknown.
*/
fun isRootGrantedCached(): Boolean? {
return Shell.isAppGrantedRoot()
}
/**
* Check if root is available. Will trigger Magisk popup if not previously decided.
*/
suspend fun hasRoot(): Boolean = withContext(Dispatchers.IO) {
try {
// getShell() initializes the shell and checks root status with Magisk
// If already granted, Magisk will auto-approve silently
Shell.getShell().isRoot
} catch (e: Exception) {
false
}
}
/**
* Request root access. Will trigger Magisk popup.
*/
suspend fun requestRoot(): Boolean = withContext(Dispatchers.IO) {
try {
Shell.getShell().isRoot
} catch (e: Exception) {
false
}
}
suspend fun executeCommand(command: String): CommandResult = withContext(Dispatchers.IO) {
val result = Shell.cmd(command).exec()
CommandResult(
success = result.isSuccess,
output = result.out.joinToString("\n"),
error = result.err.joinToString("\n"),
exitCode = result.code
)
}
suspend fun executeCommands(vararg commands: String): CommandResult = withContext(Dispatchers.IO) {
val result = Shell.cmd(*commands).exec()
CommandResult(
success = result.isSuccess,
output = result.out.joinToString("\n"),
error = result.err.joinToString("\n"),
exitCode = result.code
)
}
fun getShell(): Shell = Shell.getShell()
}
data class CommandResult(
val success: Boolean,
val output: String,
val error: String,
val exitCode: Int
)