/* * SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman * 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 )