279 lines
9.7 KiB
Kotlin
279 lines
9.7 KiB
Kotlin
/*
|
|
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
package sh.sar.isodroid.isodrive
|
|
|
|
import android.content.Context
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.withContext
|
|
import sh.sar.isodroid.data.MountOptions
|
|
import sh.sar.isodroid.data.MountStatus
|
|
import sh.sar.isodroid.data.MountType
|
|
import sh.sar.isodroid.root.RootManager
|
|
import java.io.File
|
|
|
|
class IsoDriveManager(private val context: Context) {
|
|
|
|
private val binaryName = "isodrive"
|
|
private var binaryPath: String? = null
|
|
|
|
suspend fun initialize(): Boolean = withContext(Dispatchers.IO) {
|
|
extractBinary()
|
|
}
|
|
|
|
private suspend fun extractBinary(): Boolean = withContext(Dispatchers.IO) {
|
|
try {
|
|
val abi = android.os.Build.SUPPORTED_ABIS.firstOrNull() ?: return@withContext false
|
|
val assetPath = when {
|
|
abi.contains("arm64") -> "bin/arm64-v8a/$binaryName"
|
|
abi.contains("armeabi") -> "bin/armeabi-v7a/$binaryName"
|
|
abi.contains("x86_64") -> "bin/x86_64/$binaryName"
|
|
abi.contains("x86") -> "bin/x86/$binaryName"
|
|
else -> return@withContext false
|
|
}
|
|
|
|
val targetDir = File(context.filesDir, "bin")
|
|
if (!targetDir.exists()) {
|
|
targetDir.mkdirs()
|
|
}
|
|
|
|
val targetFile = File(targetDir, binaryName)
|
|
|
|
// Check if binary already exists
|
|
if (targetFile.exists()) {
|
|
binaryPath = targetFile.absolutePath
|
|
return@withContext true
|
|
}
|
|
|
|
// Extract binary from assets
|
|
try {
|
|
context.assets.open(assetPath).use { input ->
|
|
targetFile.outputStream().use { output ->
|
|
input.copyTo(output)
|
|
}
|
|
}
|
|
targetFile.setExecutable(true, false)
|
|
binaryPath = targetFile.absolutePath
|
|
true
|
|
} catch (e: Exception) {
|
|
// Binary not bundled yet, will use system isodrive if available
|
|
checkSystemBinary()
|
|
}
|
|
} catch (e: Exception) {
|
|
false
|
|
}
|
|
}
|
|
|
|
private suspend fun checkSystemBinary(): Boolean = withContext(Dispatchers.IO) {
|
|
// Check common locations first (more reliable than 'which')
|
|
val commonPaths = listOf(
|
|
"/data/adb/modules/isodrive-magisk/system/bin/isodrive",
|
|
"/data/adb/modules/isodrive/system/bin/isodrive",
|
|
"/system/bin/isodrive",
|
|
"/system/xbin/isodrive",
|
|
"/data/local/tmp/isodrive",
|
|
"/vendor/bin/isodrive"
|
|
)
|
|
for (path in commonPaths) {
|
|
val checkResult = RootManager.executeCommand("test -f $path && echo exists")
|
|
if (checkResult.success && checkResult.output.contains("exists")) {
|
|
binaryPath = path
|
|
return@withContext true
|
|
}
|
|
}
|
|
|
|
// Try 'which' as fallback
|
|
val result = RootManager.executeCommand("which isodrive 2>/dev/null")
|
|
if (result.success && result.output.isNotBlank()) {
|
|
binaryPath = result.output.trim()
|
|
return@withContext true
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
suspend fun isSupported(): SupportStatus = withContext(Dispatchers.IO) {
|
|
// First check if binary is available
|
|
if (binaryPath == null && !extractBinary()) {
|
|
return@withContext SupportStatus.NO_BINARY
|
|
}
|
|
|
|
// Try to mount configfs if not already mounted
|
|
RootManager.executeCommand("mount -t configfs none /sys/kernel/config 2>/dev/null")
|
|
|
|
// Check configfs support - look for usb_gadget directory
|
|
val configfsCheck = RootManager.executeCommand(
|
|
"ls /sys/kernel/config/usb_gadget/ 2>/dev/null"
|
|
)
|
|
if (configfsCheck.success && configfsCheck.output.isNotBlank()) {
|
|
return@withContext SupportStatus.CONFIGFS_SUPPORTED
|
|
}
|
|
|
|
// Alternative configfs check - check if configfs is mounted at all
|
|
val configfsMountCheck = RootManager.executeCommand(
|
|
"mount | grep configfs"
|
|
)
|
|
if (configfsMountCheck.success && configfsMountCheck.output.contains("configfs")) {
|
|
// configfs is mounted, check for usb_gadget support
|
|
val gadgetCheck = RootManager.executeCommand(
|
|
"find /sys/kernel/config -maxdepth 2 -name 'usb_gadget' -type d 2>/dev/null"
|
|
)
|
|
if (gadgetCheck.success && gadgetCheck.output.isNotBlank()) {
|
|
return@withContext SupportStatus.CONFIGFS_SUPPORTED
|
|
}
|
|
}
|
|
|
|
// Check sysfs/usbgadget support (legacy Android USB gadget)
|
|
val sysfsCheck = RootManager.executeCommand(
|
|
"test -d /sys/class/android_usb/android0 && echo supported"
|
|
)
|
|
if (sysfsCheck.success && sysfsCheck.output.contains("supported")) {
|
|
return@withContext SupportStatus.SYSFS_SUPPORTED
|
|
}
|
|
|
|
// If we have the binary, assume the user knows their device supports it
|
|
// Let isodrive itself determine support at mount time
|
|
if (binaryPath != null) {
|
|
return@withContext SupportStatus.CONFIGFS_SUPPORTED
|
|
}
|
|
|
|
SupportStatus.NOT_SUPPORTED
|
|
}
|
|
|
|
suspend fun mount(isoPath: String, options: MountOptions): MountResult = withContext(Dispatchers.IO) {
|
|
if (binaryPath == null) {
|
|
return@withContext MountResult(
|
|
success = false,
|
|
message = "isodrive binary not found"
|
|
)
|
|
}
|
|
|
|
// Validate file exists
|
|
val fileCheck = RootManager.executeCommand("test -f \"$isoPath\" && echo exists")
|
|
if (!fileCheck.success || !fileCheck.output.contains("exists")) {
|
|
return@withContext MountResult(
|
|
success = false,
|
|
message = "File not found: $isoPath"
|
|
)
|
|
}
|
|
|
|
// Validate options
|
|
if (options.cdrom && !options.readOnly) {
|
|
return@withContext MountResult(
|
|
success = false,
|
|
message = "CD-ROM mode requires read-only"
|
|
)
|
|
}
|
|
|
|
// Build command
|
|
val args = options.toCommandArgs().joinToString(" ")
|
|
val command = "$binaryPath \"$isoPath\" $args"
|
|
|
|
val result = RootManager.executeCommand(command)
|
|
|
|
if (result.success) {
|
|
MountResult(
|
|
success = true,
|
|
message = "Mounted successfully"
|
|
)
|
|
} else {
|
|
MountResult(
|
|
success = false,
|
|
message = result.error.ifBlank { result.output.ifBlank { "Mount failed with exit code ${result.exitCode}" } }
|
|
)
|
|
}
|
|
}
|
|
|
|
suspend fun unmount(): MountResult = withContext(Dispatchers.IO) {
|
|
if (binaryPath == null) {
|
|
return@withContext MountResult(
|
|
success = false,
|
|
message = "isodrive binary not found"
|
|
)
|
|
}
|
|
|
|
// Running isodrive without arguments unmounts
|
|
val result = RootManager.executeCommand(binaryPath!!)
|
|
|
|
MountResult(
|
|
success = result.success || result.output.contains("Usage"),
|
|
message = if (result.success || result.output.contains("Usage")) "Unmounted successfully" else result.error.ifBlank { "Unmount failed" }
|
|
)
|
|
}
|
|
|
|
suspend fun getStatus(): MountStatus = withContext(Dispatchers.IO) {
|
|
// Check configfs lun file
|
|
val lunFileResult = RootManager.executeCommand(
|
|
"cat /sys/kernel/config/usb_gadget/*/functions/mass_storage.0/lun.0/file 2>/dev/null"
|
|
)
|
|
|
|
if (lunFileResult.success && lunFileResult.output.isNotBlank()) {
|
|
val path = lunFileResult.output.trim()
|
|
if (path.isNotEmpty()) {
|
|
// Check if it's cdrom mode
|
|
val cdromResult = RootManager.executeCommand(
|
|
"cat /sys/kernel/config/usb_gadget/*/functions/mass_storage.0/lun.0/cdrom 2>/dev/null"
|
|
)
|
|
val isCdrom = cdromResult.output.trim() == "1"
|
|
|
|
// Check if read-only
|
|
val roResult = RootManager.executeCommand(
|
|
"cat /sys/kernel/config/usb_gadget/*/functions/mass_storage.0/lun.0/ro 2>/dev/null"
|
|
)
|
|
val isReadOnly = roResult.output.trim() != "0"
|
|
|
|
return@withContext MountStatus(
|
|
mounted = true,
|
|
path = path,
|
|
type = if (isCdrom) MountType.CDROM else MountType.MASS_STORAGE,
|
|
readOnly = isReadOnly
|
|
)
|
|
}
|
|
}
|
|
|
|
// Check sysfs method
|
|
val sysfsResult = RootManager.executeCommand(
|
|
"cat /sys/class/android_usb/android0/f_mass_storage/lun/file 2>/dev/null"
|
|
)
|
|
|
|
if (sysfsResult.success && sysfsResult.output.isNotBlank()) {
|
|
val path = sysfsResult.output.trim()
|
|
if (path.isNotEmpty()) {
|
|
return@withContext MountStatus(
|
|
mounted = true,
|
|
path = path,
|
|
type = MountType.MASS_STORAGE,
|
|
readOnly = true
|
|
)
|
|
}
|
|
}
|
|
|
|
MountStatus.UNMOUNTED
|
|
}
|
|
|
|
companion object {
|
|
@Volatile
|
|
private var instance: IsoDriveManager? = null
|
|
|
|
fun getInstance(context: Context): IsoDriveManager {
|
|
return instance ?: synchronized(this) {
|
|
instance ?: IsoDriveManager(context.applicationContext).also { instance = it }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
enum class SupportStatus {
|
|
CONFIGFS_SUPPORTED,
|
|
SYSFS_SUPPORTED,
|
|
NOT_SUPPORTED,
|
|
NO_BINARY
|
|
}
|
|
|
|
data class MountResult(
|
|
val success: Boolean,
|
|
val message: String
|
|
)
|