15 Commits
28 changed files with 463 additions and 157 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "isodrive"]
path = isodrive
url = https://git.shihaam.dev/shihaam/isodrive
Generated
+1
View File
@@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/isodrive" vcs="Git" />
</component>
</project>
+17
View File
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.7] - 2026-03-13
### Changed
- Always use app-bundled isodrive binary instead of system binary
- Prep for edge-to-edge compatibility
## [1.6] - 2026-03-13
### Added
- Disclaimer screen in welcome wizard about user responsibility
- USB services restart warning dialog before mount/unmount operations
- Toggle in Settings to enable/disable USB restart warning
### Fixed
- Prevent shell escape exploits in shell commands
- Disable logcat on release builds
## [1.5] - 2026-03-12
### Added
+2 -2
View File
@@ -9,9 +9,9 @@ Android app for mounting ISO/IMG files as USB mass storage or CD-ROM devices on
## Screenshots
| OS images listing | Mounted Status | Mount Options Dialog | Create IMG Dialog | Download OS ISOs |
| OS images listing | Mount Options Dialog | Mounted Status | Create IMG Dialog | Download OS ISOs |
|:--:|:--:|:--:|:--:|:--:|
| ![File Browser](docs/screenshots/list_images_home.jpg) | ![Mounted Status](docs/screenshots/mounted_status_home.jpg) | ![Mount Options](docs/screenshots/mount_options_dialogbox.jpg) | ![Create IMG](docs/screenshots/create_img_digalogbox.jpg) | ![Download ISOs](docs/screenshots/list_listing_download.jpg) |
| ![File Browser](docs/screenshots/list_images_home.jpg) | ![Mount Options](docs/screenshots/mount_options_dialogbox.jpg) | ![Mounted Status](docs/screenshots/mounted_status_home.jpg) | ![Create IMG](docs/screenshots/create_img_digalogbox.jpg) | ![Download ISOs](docs/screenshots/list_listing_download.jpg) |
## Features
+120 -2
View File
@@ -1,9 +1,127 @@
import javax.inject.Inject
import org.gradle.process.ExecOperations
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
// Build isodrive native binary from submodule (only if binaries are missing)
abstract class BuildIsodriveTask @Inject constructor(
private val execOps: ExecOperations
) : DefaultTask() {
@get:InputDirectory
abstract val isodriveDir: DirectoryProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun build() {
val isodrivePath = isodriveDir.get().asFile
val outputPath = outputDir.get().asFile
if (!isodrivePath.resolve("src").exists()) {
throw GradleException("isodrive submodule not initialized. Run: git submodule update --init")
}
val sourceFiles = listOf(
"src/util.cpp",
"src/configfsisomanager.cpp",
"src/androidusbisomanager.cpp",
"src/main.cpp"
)
val srcs = sourceFiles.joinToString(" ") { "$isodrivePath/$it" }
val cflags = "-I$isodrivePath/src/include -static-libstdc++ -Os -s"
val compilers = mapOf(
"arm64-v8a" to "aarch64-linux-android26-clang++",
"armeabi-v7a" to "armv7a-linux-androideabi26-clang++",
"x86_64" to "x86_64-linux-android26-clang++",
"x86" to "i686-linux-android26-clang++"
)
val useNixShell = File("/etc/NIXOS").exists() ||
Runtime.getRuntime().exec(arrayOf("which", "nix-shell")).waitFor() == 0
if (useNixShell) {
println("NixOS detected, using nix-shell for NDK...")
val buildScript = compilers.entries.joinToString("\n") { (arch, compiler) ->
val archDir = outputPath.resolve(arch)
val output = archDir.resolve("isodrive")
if (output.exists()) {
"echo 'isodrive for $arch already exists, skipping'"
} else {
"""
mkdir -p "$archDir"
echo "Building isodrive for $arch..."
"${'$'}TOOLCHAIN/$compiler" $cflags $srcs -o "$output"
""".trimIndent()
}
}
execOps.exec {
environment("NIXPKGS_ALLOW_UNFREE", "1")
commandLine("nix-shell", "-p", "androidenv.androidPkgs.ndk-bundle", "--run", """
SDK_ROOT=${'$'}(find /nix/store -maxdepth 1 -name "*android-sdk-ndk*" -type d 2>/dev/null | head -1)
NDK=${'$'}(ls -d "${'$'}SDK_ROOT/libexec/android-sdk/ndk/"* | head -1)
TOOLCHAIN="${'$'}NDK/toolchains/llvm/prebuilt/linux-x86_64/bin"
echo "Using NDK: ${'$'}NDK"
$buildScript
echo "Done building isodrive"
""".trimIndent())
}
} else {
val ndkDir = listOfNotNull(
System.getenv("ANDROID_NDK_HOME"),
System.getenv("ANDROID_NDK")
).firstOrNull { File(it).exists() }
?: throw GradleException("Android NDK not found. Set ANDROID_NDK_HOME or install NDK via SDK Manager.")
val toolchain = "$ndkDir/toolchains/llvm/prebuilt/linux-x86_64/bin"
if (!File(toolchain).exists()) {
throw GradleException("NDK toolchain not found at: $toolchain")
}
compilers.forEach { (arch, compiler) ->
val archDir = outputPath.resolve(arch)
val output = archDir.resolve("isodrive")
if (output.exists()) {
println("isodrive for $arch already exists, skipping")
return@forEach
}
archDir.mkdirs()
println("Building isodrive for $arch...")
execOps.exec {
commandLine("sh", "-c", "$toolchain/$compiler $cflags $srcs -o $output")
}
}
println("Done building isodrive")
}
}
}
val buildIsodrive by tasks.registering(BuildIsodriveTask::class) {
group = "native"
description = "Build isodrive binary for all Android architectures"
isodriveDir.set(rootProject.file("isodrive"))
outputDir.set(file("src/main/assets/bin"))
val architectures = listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
val outDir = file("src/main/assets/bin")
onlyIf { !architectures.all { arch -> outDir.resolve("$arch/isodrive").exists() } }
}
tasks.matching { it.name.startsWith("merge") && it.name.endsWith("Assets") }.configureEach {
dependsOn(buildIsodrive)
}
android {
namespace = "sh.sar.isodroid"
compileSdk = 36
@@ -12,8 +130,8 @@ android {
applicationId = "sh.sar.isodroid"
minSdk = 26
targetSdk = 36
versionCode = 5
versionName = "1.5"
versionCode = 7
versionName = "1.7"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -58,42 +58,14 @@ class IsoDriveManager(private val context: Context) {
binaryPath = targetFile.absolutePath
true
} catch (e: Exception) {
// Binary not bundled yet, will use system isodrive if available
checkSystemBinary()
// Binary extraction failed
false
}
} 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()) {
@@ -28,7 +28,7 @@ object RootManager {
Shell.enableVerboseLogging = BuildConfig.DEBUG
Shell.setDefaultBuilder(
Shell.Builder.create()
.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_REDIRECT_STDERR)
.setFlags(Shell.FLAG_MOUNT_MASTER)
.setTimeout(10)
)
}
@@ -0,0 +1,100 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun UsbWarningDialog(
isUnmount: Boolean,
onDismiss: () -> Unit,
onConfirm: (dontShowAgain: Boolean) -> Unit
) {
var dontShowAgain by remember { mutableStateOf(false) }
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = "USB Services Will Restart",
style = MaterialTheme.typography.headlineSmall
)
},
text = {
Column {
Text(
text = if (isUnmount) {
"Unmounting will restart USB services on your device, including:"
} else {
"Mounting will restart USB services on your device, including:"
},
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "• MTP file transfer\n• USB ADB\n• USB tethering\n• Other USB functions",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "Active file transfers may be interrupted and could result in data corruption. Make sure no transfers are in progress.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.height(16.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = dontShowAgain,
onCheckedChange = { dontShowAgain = it }
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Don't show this again",
style = MaterialTheme.typography.bodyMedium
)
}
}
},
confirmButton = {
TextButton(
onClick = { onConfirm(dontShowAgain) }
) {
Text(if (isUnmount) "Unmount" else "Mount")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
@@ -12,6 +12,8 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -22,8 +24,8 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -68,9 +70,9 @@ private fun loadOsDownloads(context: Context): List<OsDownload> {
OsDownload(
name = obj.getString("name"),
category = obj.getString("category"),
subcategory = if (obj.isNull("subcategory")) null else obj.optString("subcategory", null),
subcategory = if (obj.isNull("subcategory")) null else obj.optString("subcategory"),
description = obj.optString("description", ""),
icon = if (obj.isNull("icon")) null else obj.optString("icon", null),
icon = if (obj.isNull("icon")) null else obj.optString("icon"),
url = obj.getString("url")
)
)
@@ -101,6 +103,7 @@ fun DownloadsScreen(
}
Scaffold(
contentWindowInsets = WindowInsets(0),
topBar = {
TopAppBar(
title = { Text("Download ISOs") },
@@ -124,6 +127,7 @@ fun DownloadsScreen(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.navigationBarsPadding()
.verticalScroll(rememberScrollState())
) {
groupedDownloads.forEach { (category, osList) ->
@@ -280,7 +284,7 @@ private fun DownloadItem(
}
}
Icon(
imageVector = Icons.Default.OpenInNew,
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = "Open in browser",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp)
@@ -10,6 +10,8 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -49,14 +51,17 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import sh.sar.isodroid.data.IsoFile
import sh.sar.isodroid.data.MountOptions
import sh.sar.isodroid.ui.components.CreateImgDialog
import sh.sar.isodroid.ui.components.FileContextMenu
import sh.sar.isodroid.ui.components.FileItemCard
import sh.sar.isodroid.ui.components.MountDialog
import sh.sar.isodroid.ui.components.StatusCard
import sh.sar.isodroid.ui.components.UsbWarningDialog
import sh.sar.isodroid.viewmodel.MainViewModel
@OptIn(ExperimentalMaterial3Api::class)
@@ -69,12 +74,38 @@ fun MainScreen(
val uiState by viewModel.uiState.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val context = LocalContext.current
var selectedFile by remember { mutableStateOf<IsoFile?>(null) }
var showMountDialog by remember { mutableStateOf(false) }
var showCreateImgDialog by remember { mutableStateOf(false) }
var contextMenuFile by remember { mutableStateOf<IsoFile?>(null) }
// USB warning dialog state
var showUsbWarning by remember { mutableStateOf(false) }
var pendingMountPath by remember { mutableStateOf<String?>(null) }
var pendingMountOptions by remember { mutableStateOf<MountOptions?>(null) }
var isUnmountWarning by remember { mutableStateOf(false) }
val prefs = remember { context.getSharedPreferences("iso_drive_prefs", android.content.Context.MODE_PRIVATE) }
val skipUsbWarning = remember { mutableStateOf(prefs.getBoolean("skip_usb_warning", false)) }
fun showUsbWarningOrProceed(
isUnmount: Boolean,
mountPath: String? = null,
mountOptions: MountOptions? = null,
onProceed: () -> Unit
) {
if (skipUsbWarning.value) {
onProceed()
} else {
isUnmountWarning = isUnmount
pendingMountPath = mountPath
pendingMountOptions = mountOptions
showUsbWarning = true
}
}
val pullToRefreshState = rememberPullToRefreshState()
// Handle pull-to-refresh
@@ -112,6 +143,7 @@ fun MainScreen(
}
Scaffold(
contentWindowInsets = WindowInsets(0),
topBar = {
TopAppBar(
title = { Text("ISO Droid") },
@@ -150,8 +182,10 @@ fun MainScreen(
if (uiState.mountStatus.mounted) {
ExtendedFloatingActionButton(
onClick = {
scope.launch {
viewModel.unmount()
showUsbWarningOrProceed(isUnmount = true) {
scope.launch {
viewModel.unmount()
}
}
},
icon = {
@@ -173,6 +207,7 @@ fun MainScreen(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.navigationBarsPadding()
.nestedScroll(pullToRefreshState.nestedScrollConnection)
) {
if (uiState.isLoading && !pullToRefreshState.isRefreshing) {
@@ -295,8 +330,14 @@ fun MainScreen(
val filePath = file.path // Capture path before clearing state
showMountDialog = false
selectedFile = null
scope.launch {
viewModel.mount(filePath, options)
showUsbWarningOrProceed(
isUnmount = false,
mountPath = filePath,
mountOptions = options
) {
scope.launch {
viewModel.mount(filePath, options)
}
}
}
)
@@ -337,4 +378,39 @@ fun MainScreen(
}
)
}
// USB warning dialog
if (showUsbWarning) {
UsbWarningDialog(
isUnmount = isUnmountWarning,
onDismiss = {
showUsbWarning = false
pendingMountPath = null
pendingMountOptions = null
},
onConfirm = { dontShowAgain ->
if (dontShowAgain) {
prefs.edit().putBoolean("skip_usb_warning", true).apply()
skipUsbWarning.value = true
}
// Capture values before clearing state
val isUnmount = isUnmountWarning
val path = pendingMountPath
val options = pendingMountOptions
showUsbWarning = false
pendingMountPath = null
pendingMountOptions = null
scope.launch {
if (isUnmount) {
viewModel.unmount()
} else if (path != null && options != null) {
viewModel.mount(path, options)
}
}
}
)
}
}
@@ -22,6 +22,8 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -47,6 +49,7 @@ import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -58,6 +61,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
@@ -115,6 +119,10 @@ fun SettingsScreen(
)
}
// USB warning preference
val prefs = remember { context.getSharedPreferences("iso_drive_prefs", android.content.Context.MODE_PRIVATE) }
var showUsbWarning by remember { mutableStateOf(!prefs.getBoolean("skip_usb_warning", false)) }
// Re-check permissions when returning to the app
if (activity != null) {
androidx.compose.runtime.DisposableEffect(activity) {
@@ -145,6 +153,7 @@ fun SettingsScreen(
}
Scaffold(
contentWindowInsets = WindowInsets(0),
topBar = {
TopAppBar(
title = { Text("Settings") },
@@ -168,6 +177,7 @@ fun SettingsScreen(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.navigationBarsPadding()
.verticalScroll(rememberScrollState())
) {
// Storage section
@@ -246,6 +256,61 @@ fun SettingsScreen(
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
// Warnings section
SectionHeader(title = "Warnings")
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f)
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
text = "USB restart warning",
style = MaterialTheme.typography.titleSmall
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Show warning before mount/unmount about USB service interruption",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Switch(
checked = showUsbWarning,
onCheckedChange = { enabled ->
showUsbWarning = enabled
prefs.edit().putBoolean("skip_usb_warning", !enabled).apply()
}
)
}
}
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
// About section
SectionHeader(title = "About")
@@ -28,6 +28,7 @@ import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -69,15 +70,18 @@ fun SetupWizardScreen(
0 -> WelcomeStep(
onNext = { currentStep = 1 }
)
1 -> RootAccessStep(
onNext = { currentStep = 2 },
onSkip = { currentStep = 2 }
1 -> DisclaimerStep(
onNext = { currentStep = 2 }
)
2 -> NotificationStep(
2 -> RootAccessStep(
onNext = { currentStep = 3 },
onSkip = { currentStep = 3 }
)
3 -> CompleteStep(
3 -> NotificationStep(
onNext = { currentStep = 4 },
onSkip = { currentStep = 4 }
)
4 -> CompleteStep(
onFinish = onSetupComplete
)
}
@@ -131,6 +135,53 @@ private fun WelcomeStep(
}
}
@Composable
private fun DisclaimerStep(
onNext: () -> Unit
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.tertiary
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Before You Continue",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "This app requires root access and performs system-level operations. While reasonable precautions have been taken, you are responsible for understanding what you're doing.",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "Keep backups of important data. This software is provided \"as is\" without warranty.",
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(48.dp))
Button(
onClick = onNext,
modifier = Modifier.fillMaxWidth()
) {
Text("Continue")
}
}
@Composable
private fun RootAccessStep(
onNext: () -> Unit,
@@ -5,7 +5,6 @@
package sh.sar.isodroid.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
@@ -14,11 +13,7 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
@@ -47,15 +42,6 @@ fun ISODroidTheme(
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = android.graphics.Color.TRANSPARENT
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
-77
View File
@@ -1,77 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Build isodrive from source for all Android architectures
# Requires: Android NDK (or runs via nix-shell on NixOS)
SCRIPT_DIR=$(dirname "$(realpath "$0")")
ISODRIVE_DIR="/tmp/isodrive"
OUTPUT_DIR="$SCRIPT_DIR/app/src/main/assets/bin"
# Clone isodrive source
if [[ -d "$ISODRIVE_DIR" ]]; then
echo "Updating isodrive source..."
git -C "$ISODRIVE_DIR" pull
else
echo "Cloning isodrive..."
git clone --depth 1 https://github.com/nitanmarcel/isodrive "$ISODRIVE_DIR"
fi
SRCS="$ISODRIVE_DIR/src/util.cpp $ISODRIVE_DIR/src/configfsisomanager.cpp $ISODRIVE_DIR/src/androidusbisomanager.cpp $ISODRIVE_DIR/src/main.cpp"
CFLAGS="-I$ISODRIVE_DIR/src/include -static-libstdc++ -Os -s"
build_all() {
local NDK="$1"
local TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin"
echo "Building arm64-v8a..."
"$TOOLCHAIN/aarch64-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/arm64-v8a/isodrive"
echo "Building armeabi-v7a..."
"$TOOLCHAIN/armv7a-linux-androideabi26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/armeabi-v7a/isodrive"
echo "Building x86_64..."
"$TOOLCHAIN/x86_64-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/x86_64/isodrive"
echo "Building x86..."
"$TOOLCHAIN/i686-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/x86/isodrive"
echo "Done! Built isodrive for all architectures."
ls -la "$OUTPUT_DIR"/*/isodrive
}
# On NixOS, prefer nix-shell (local Android SDK has /bin/bash issues)
if command -v nix-shell &>/dev/null; then
echo "Using nix-shell to get Android NDK..."
export SRCS CFLAGS OUTPUT_DIR
NIXPKGS_ALLOW_UNFREE=1 nix-shell -p androidenv.androidPkgs.ndk-bundle --run '
SDK_ROOT=$(find /nix/store -maxdepth 1 -name "*android-sdk-ndk*" -type d 2>/dev/null | head -1)
NDK=$(ls -d "$SDK_ROOT/libexec/android-sdk/ndk/"* | head -1)
TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin"
echo "Using NDK: $NDK"
echo "Building arm64-v8a..."
"$TOOLCHAIN/aarch64-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/arm64-v8a/isodrive"
echo "Building armeabi-v7a..."
"$TOOLCHAIN/armv7a-linux-androideabi26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/armeabi-v7a/isodrive"
echo "Building x86_64..."
"$TOOLCHAIN/x86_64-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/x86_64/isodrive"
echo "Building x86..."
"$TOOLCHAIN/i686-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/x86/isodrive"
echo "Done!"
ls -la "$OUTPUT_DIR"/*/isodrive
'
elif [[ -n "${ANDROID_NDK_HOME:-}" ]]; then
build_all "$ANDROID_NDK_HOME"
elif [[ -n "${ANDROID_NDK:-}" ]]; then
build_all "$ANDROID_NDK"
else
echo "Error: Android NDK not found."
echo "Set ANDROID_NDK_HOME or ANDROID_NDK, or install nix-shell."
exit 1
fi
Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 108 KiB

@@ -0,0 +1,5 @@
• Added disclaimer screen in welcome wizard about user responsibility
• Added USB services restart warning before mount/unmount (with "Don't show again" option)
• Added toggle in Settings to enable/disable USB restart warning
• Fixed shell escape exploits in shell commands
• Disabled logcat on release builds
@@ -0,0 +1,2 @@
- Always use app-bundled isodrive binary instead of system binary
- Prep for edge-to-edge compatibility
Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 145 KiB

-18
View File
@@ -1,18 +0,0 @@
#!/usr/bin/env bash
# Get the directory where this script lives
SCRIPT_DIR=$(dirname "$(realpath "$0")")
ISODRIVE_VERSION=$(curl -sI https://github.com/nitanmarcel/isodrive-magisk/releases/latest | grep -i ^location | grep -oP 'v\K[\d.]+')
curl -sL https://github.com/nitanmarcel/isodrive-magisk/releases/download/v$ISODRIVE_VERSION/isodrive-magisk-v$ISODRIVE_VERSION.zip -o /tmp/isodrive-magisk.zip
unzip -q /tmp/isodrive-magisk.zip -d /tmp/isodrive-magisk
# Move the isodrive binary for each architecture
mv /tmp/isodrive-magisk/libs/arm64-v8a/isodrive $SCRIPT_DIR/app/src/main/assets/bin/arm64-v8a/
mv /tmp/isodrive-magisk/libs/armeabi-v7a/isodrive $SCRIPT_DIR/app/src/main/assets/bin/armeabi-v7a/
mv /tmp/isodrive-magisk/libs/x86/isodrive $SCRIPT_DIR/app/src/main/assets/bin/x86/
mv /tmp/isodrive-magisk/libs/x86_64/isodrive $SCRIPT_DIR/app/src/main/assets/bin/x86_64/
# Clean up temp files
rm -rf /tmp/isodrive-magisk /tmp/isodrive-magisk.zip
Submodule
+1
Submodule isodrive added at 2e9f28bd90