13 KiB
Technical Documentation
This document provides an in-depth technical overview of ISO Droid's architecture, components, and implementation details.
Architecture Overview
ISO Droid follows the MVVM (Model-View-ViewModel) architecture pattern with Jetpack Compose for the UI layer.
┌─────────────────────────────────────────────────────────┐
│ UI Layer │
│ (Jetpack Compose Screens & Components) │
└─────────────────────────┬───────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────┐
│ ViewModel │
│ (MainViewModel - State Management) │
└─────────────────────────┬───────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────┐
│ Domain Layer │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ IsoDriveManager │ │ RootManager │ │
│ │ (Mount/Unmount) │ │ (Shell Access) │ │
│ └─────────────────┘ └──────────────────┘ │
└─────────────────────────┬───────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────┐
│ Native Layer │
│ ┌─────────────────────────────────────────────────┐ │
│ │ isodrive binary (per-architecture) │ │
│ │ Interfaces with Linux USB gadget subsystem │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Package Structure
sh.sar.isodroid/
├── MainActivity.kt # Entry point, Compose host
├── ISODroidApp.kt # Application class
├── data/
│ ├── IsoFile.kt # ISO/IMG file model
│ ├── MountOptions.kt # Mount configuration
│ └── MountStatus.kt # Current mount state
├── isodrive/
│ ├── IsoDriveManager.kt # Core mounting logic
│ ├── MountEventBus.kt # Mount state events
│ └── CreateImgEventBus.kt # IMG creation events
├── notification/
│ ├── NotificationHelper.kt # Notification management
│ ├── UnmountReceiver.kt # Unmount broadcast receiver
│ └── CreateImgReceiver.kt # Cancel IMG creation receiver
├── root/
│ └── RootManager.kt # Root shell abstraction
├── ui/
│ ├── components/ # Reusable UI components
│ ├── screens/ # Full-screen composables
│ └── theme/ # Material 3 theming
└── viewmodel/
└── MainViewModel.kt # UI state management
Binary Architecture Selection
The app bundles the isodrive binary for multiple CPU architectures and selects the appropriate one at runtime.
Bundled Architectures
| Architecture | Directory | Target Devices |
|---|---|---|
| arm64-v8a | bin/arm64-v8a/ |
Modern 64-bit ARM (most phones) |
| armeabi-v7a | bin/armeabi-v7a/ |
32-bit ARM (older phones) |
| x86_64 | bin/x86_64/ |
64-bit Intel (emulators) |
| x86 | bin/x86/ |
32-bit Intel (some tablets) |
Selection Process
The architecture selection happens in IsoDriveManager.extractBinary():
val abi = android.os.Build.SUPPORTED_ABIS.firstOrNull()
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
}
Key points:
Build.SUPPORTED_ABISreturns a priority-ordered list of supported ABIs- The first (primary) ABI is used for best performance
- String matching order matters:
x86_64is checked beforex86to avoid false matches - Binary is extracted to
context.filesDir/bin/isodriveand made executable
Fallback Mechanism
If the bundled binary fails to extract, the app searches for a system-installed isodrive:
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"
)
Root Access
libsu Integration
The app uses libsu for root shell access:
Shell.setDefaultBuilder(
Shell.Builder.create()
.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_REDIRECT_STDERR)
.setTimeout(10)
)
Flags:
FLAG_MOUNT_MASTER: Ensures the shell runs in the global mount namespaceFLAG_REDIRECT_STDERR: Captures error output for debugging
Root Status Detection
The app handles three root states:
| State | Description |
|---|---|
true |
Root granted (cached or confirmed) |
false |
Root denied by user |
null |
Unknown (first launch, checking) |
fun isRootGrantedCached(): Boolean? {
return Shell.isAppGrantedRoot() // Returns cached status, no popup
}
suspend fun hasRoot(): Boolean {
return Shell.getShell().isRoot // May trigger Magisk prompt
}
USB Gadget Interface
Supported Backends
ISO Droid supports two Linux USB gadget interfaces:
1. ConfigFS (Modern - Android 8.0+)
The preferred method using the kernel's configfs:
/sys/kernel/config/usb_gadget/*/functions/mass_storage.0/lun.0/
├── file # Path to mounted ISO/IMG
├── cdrom # 1 = CD-ROM mode, 0 = mass storage
├── ro # 1 = read-only, 0 = read-write
└── removable # 1 = removable media
2. Sysfs/UsbGadget (Legacy)
Older Android USB gadget interface:
/sys/class/android_usb/android0/f_mass_storage/lun/
├── file # Path to mounted ISO/IMG
└── ...
Support Detection
suspend fun isSupported(): SupportStatus {
// Try configfs first
val configfsCheck = RootManager.executeCommand(
"ls /sys/kernel/config/usb_gadget/ 2>/dev/null"
)
if (configfsCheck.success && configfsCheck.output.isNotBlank()) {
return SupportStatus.CONFIGFS_SUPPORTED
}
// Fall back to sysfs
val sysfsCheck = RootManager.executeCommand(
"test -d /sys/class/android_usb/android0 && echo supported"
)
if (sysfsCheck.success) {
return SupportStatus.SYSFS_SUPPORTED
}
return SupportStatus.NOT_SUPPORTED
}
Mount Options
Available Options
| Option | Flag | Description |
|---|---|---|
| Read-Only | (default) | File cannot be modified by host PC |
| Read-Write | -rw |
File can be modified by host PC |
| CD-ROM | -cdrom |
Emulates optical drive (always R/O) |
| ConfigFS | -configfs |
Use configfs backend (default) |
| UsbGadget | -usbgadget |
Use legacy sysfs backend |
Command Generation
fun toCommandArgs(): List<String> {
val args = mutableListOf<String>()
if (!readOnly) args.add("-rw")
if (cdrom) args.add("-cdrom")
if (useConfigfs) args.add("-configfs")
else args.add("-usbgadget")
return args
}
Example commands:
# Mount as read-only mass storage
isodrive "/sdcard/isodrive/ubuntu.iso" -configfs
# Mount as writable drive
isodrive "/sdcard/isodrive/drive.img" -rw -configfs
# Mount as CD-ROM
isodrive "/sdcard/isodrive/windows.iso" -cdrom -configfs
Event System
The app uses Kotlin SharedFlow for decoupled event communication.
MountEventBus
Notifies components of mount state changes:
sealed class MountEvent {
data object Mounted : MountEvent()
data object Unmounted : MountEvent()
}
Producers:
UnmountReceiver(notification action)IsoDriveManager.mount()/unmount()
Consumers:
MainViewModel(updates UI state)
CreateImgEventBus
Tracks IMG file creation progress:
sealed class CreateImgEvent {
data class Progress(val bytesWritten: Long, val totalBytes: Long)
data class Complete(val success: Boolean, val filePath: String?)
data object Cancelled
}
IMG File Creation
Process
- User specifies filename and size
- App creates sparse file using
dd:dd if=/dev/zero of="$filePath" bs=1M count=0 seek=$totalBlocks - File is filled in 64MB chunks with progress reporting
- Cancellation is supported via
CreateImgEventBus
Progress Notification
The notification shows real-time progress with a cancel button, implemented via CreateImgReceiver broadcast receiver.
File Operations
Listing Files
The app uses multiple strategies to list ISO/IMG files:
-
find command (most reliable):
find "$path" -maxdepth 1 -type f \( -iname '*.iso' -o -iname '*.img' \) -
ls command (fallback):
ls "$path" -
Direct Java File API (if storage permissions allow)
File Management
- Rename:
mv "$oldPath" "$newPath" - Delete:
rm -f "$path" - Size query:
stat -c %s "$path"
Notification System
Channels
| Channel ID | Purpose | Importance |
|---|---|---|
iso_drive_mount_status |
Persistent mount status | LOW |
iso_drive_progress |
IMG creation progress | LOW |
Mount Notification
Ongoing notification with:
- File name
- Mount type (CD-ROM / Mass Storage)
- Access mode (Read-Only / Read-Write)
- Unmount action button
Data Persistence
DataStore Preferences
private val KEY_ISO_DIRECTORY = stringPreferencesKey("iso_directory")
Stores:
- Custom ISO directory path (default:
/sdcard/isodrive/)
SharedPreferences
getSharedPreferences("iso_drive_prefs", Context.MODE_PRIVATE)
Stores:
root_granted: Whether root was granted in setup wizard
Assets
Structure
assets/
├── bin/ # isodrive binaries (per-arch)
│ ├── arm64-v8a/isodrive
│ ├── armeabi-v7a/isodrive
│ ├── x86/isodrive
│ └── x86_64/isodrive
├── osicons/ # SVG icons for OS distributions
│ ├── ubuntu.svg
│ ├── archlinux.svg
│ └── ...
└── os.json # Download links for OS ISOs
OS Icon Matching
Icons are matched by checking if the ISO filename contains the icon name (case-insensitive):
ubuntu-22.04.isomatchesubuntu.svgarchlinux-2024.01.isomatchesarchlinux.svg
Symlinks handle variations (e.g., mint.svg -> linuxmint.svg).
Error Handling
Mount Failures
Common failure scenarios:
- File not found
- USB gadget not supported
- Another file already mounted
- Permission denied
All errors are captured and displayed via MountResult:
data class MountResult(
val success: Boolean,
val message: String
)
Root Failures
- Cached status checked first (no popup)
- User can retry from settings
- Graceful degradation to "not supported" state