some biggerish
This commit is contained in:
401
docs/TECHNICAL.md
Normal file
401
docs/TECHNICAL.md
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
# 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()`:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
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:**
|
||||||
|
1. `Build.SUPPORTED_ABIS` returns a priority-ordered list of supported ABIs
|
||||||
|
2. The first (primary) ABI is used for best performance
|
||||||
|
3. String matching order matters: `x86_64` is checked before `x86` to avoid false matches
|
||||||
|
4. Binary is extracted to `context.filesDir/bin/isodrive` and made executable
|
||||||
|
|
||||||
|
### Fallback Mechanism
|
||||||
|
|
||||||
|
If the bundled binary fails to extract, the app searches for a system-installed `isodrive`:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
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](https://github.com/topjohnwu/libsu) for root shell access:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
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 namespace
|
||||||
|
- `FLAG_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) |
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
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
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
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
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
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:**
|
||||||
|
```bash
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
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:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
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
|
||||||
|
|
||||||
|
1. User specifies filename and size
|
||||||
|
2. App creates sparse file using `dd`:
|
||||||
|
```bash
|
||||||
|
dd if=/dev/zero of="$filePath" bs=1M count=0 seek=$totalBlocks
|
||||||
|
```
|
||||||
|
3. File is filled in 64MB chunks with progress reporting
|
||||||
|
4. 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:
|
||||||
|
|
||||||
|
1. **find command** (most reliable):
|
||||||
|
```bash
|
||||||
|
find "$path" -maxdepth 1 -type f \( -iname '*.iso' -o -iname '*.img' \)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **ls command** (fallback):
|
||||||
|
```bash
|
||||||
|
ls "$path"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **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
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private val KEY_ISO_DIRECTORY = stringPreferencesKey("iso_directory")
|
||||||
|
```
|
||||||
|
|
||||||
|
Stores:
|
||||||
|
- Custom ISO directory path (default: `/sdcard/isodrive/`)
|
||||||
|
|
||||||
|
### SharedPreferences
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
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.iso` matches `ubuntu.svg`
|
||||||
|
- `archlinux-2024.01.iso` matches `archlinux.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`:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user