working poc

This commit is contained in:
2026-03-10 00:36:59 +05:00
parent 881dbfb648
commit a319a07440
34 changed files with 2067 additions and 37 deletions

View File

@@ -0,0 +1,273 @@
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
)