Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e9dfbce049
|
|||
|
ccb2af558b
|
|||
|
d0817240ec
|
|||
|
b1abee3579
|
|||
|
148b494926
|
|||
|
cdba2582fa
|
|||
|
3068939d93
|
|||
|
dc6f72bcc5
|
|||
|
804e4c3ae3
|
|||
|
b9a95bd12d
|
|||
|
32406e335a
|
|||
|
235053eba6
|
|||
|
8e3b29b5df
|
|||
|
22d729ce53
|
|||
|
06b67d64c7
|
|||
|
f15882aea2
|
|||
|
f3dc0b65d6
|
|||
|
4b22871ab4
|
|||
|
d15b56c9c0
|
|||
|
88ed16f89e
|
|||
|
71842b01d4
|
|||
|
fb74e3663d
|
|||
|
f031a96193
|
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "isodrive"]
|
||||||
|
path = isodrive
|
||||||
|
url = https://git.shihaam.dev/shihaam/isodrive
|
||||||
1
.idea/vcs.xml
generated
@@ -2,5 +2,6 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/isodrive" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
17
CHANGELOG.md
@@ -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/),
|
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).
|
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
|
## [1.5] - 2026-03-12
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
# ISO Droid
|
# ISO Droid
|
||||||
|
|
||||||
|
> **Note:** This app requires root access and was developed with AI assistance (Claude).
|
||||||
|
> I use it on my own devices, but as with any root tool, understand what you're running and keep backups.
|
||||||
|
>
|
||||||
|
> See [full disclaimer](docs/DISCLAIMER.md) | [Report issues](https://git.sargit.com/sargit/ISODroid/issues)
|
||||||
|
|
||||||
Android app for mounting ISO/IMG files as USB mass storage or CD-ROM devices on rooted Android devices.
|
Android app for mounting ISO/IMG files as USB mass storage or CD-ROM devices on rooted Android devices.
|
||||||
|
|
||||||
## Screenshots
|
## 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 |
|
||||||
|:--:|:--:|:--:|:--:|:--:|
|
|:--:|:--:|:--:|:--:|:--:|
|
||||||
|  |  |  |  |  |
|
|  |  |  |  |  |
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,127 @@
|
|||||||
|
import javax.inject.Inject
|
||||||
|
import org.gradle.process.ExecOperations
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.compose)
|
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 {
|
android {
|
||||||
namespace = "sh.sar.isodroid"
|
namespace = "sh.sar.isodroid"
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
@@ -12,8 +130,8 @@ android {
|
|||||||
applicationId = "sh.sar.isodroid"
|
applicationId = "sh.sar.isodroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 5
|
versionCode = 7
|
||||||
versionName = "1.5"
|
versionName = "1.7"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class ISODroidApp : Application() {
|
|||||||
companion object {
|
companion object {
|
||||||
init {
|
init {
|
||||||
// Set settings before the main shell can be created
|
// Set settings before the main shell can be created
|
||||||
Shell.enableVerboseLogging = true
|
Shell.enableVerboseLogging = BuildConfig.DEBUG
|
||||||
Shell.setDefaultBuilder(
|
Shell.setDefaultBuilder(
|
||||||
Shell.Builder.create()
|
Shell.Builder.create()
|
||||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||||
|
|||||||
@@ -58,42 +58,14 @@ class IsoDriveManager(private val context: Context) {
|
|||||||
binaryPath = targetFile.absolutePath
|
binaryPath = targetFile.absolutePath
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Binary not bundled yet, will use system isodrive if available
|
// Binary extraction failed
|
||||||
checkSystemBinary()
|
false
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
false
|
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) {
|
suspend fun isSupported(): SupportStatus = withContext(Dispatchers.IO) {
|
||||||
// First check if binary is available
|
// First check if binary is available
|
||||||
if (binaryPath == null && !extractBinary()) {
|
if (binaryPath == null && !extractBinary()) {
|
||||||
@@ -150,8 +122,9 @@ class IsoDriveManager(private val context: Context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate file exists
|
// Validate file exists using shell-safe escaping
|
||||||
val fileCheck = RootManager.executeCommand("test -f \"$isoPath\" && echo exists")
|
val safePath = RootManager.shellEscape(isoPath)
|
||||||
|
val fileCheck = RootManager.executeCommand("test -f $safePath && echo exists")
|
||||||
if (!fileCheck.success || !fileCheck.output.contains("exists")) {
|
if (!fileCheck.success || !fileCheck.output.contains("exists")) {
|
||||||
return@withContext MountResult(
|
return@withContext MountResult(
|
||||||
success = false,
|
success = false,
|
||||||
@@ -167,9 +140,9 @@ class IsoDriveManager(private val context: Context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build command
|
// Build command with safe path escaping
|
||||||
val args = options.toCommandArgs().joinToString(" ")
|
val args = options.toCommandArgs().joinToString(" ")
|
||||||
val command = "$binaryPath \"$isoPath\" $args"
|
val command = "$binaryPath $safePath $args"
|
||||||
|
|
||||||
val result = RootManager.executeCommand(command)
|
val result = RootManager.executeCommand(command)
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,29 @@
|
|||||||
package sh.sar.isodroid.root
|
package sh.sar.isodroid.root
|
||||||
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import sh.sar.isodroid.BuildConfig
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
object RootManager {
|
object RootManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes a string for safe use in shell commands.
|
||||||
|
* Uses single quotes and escapes any single quotes within the string.
|
||||||
|
* This prevents command injection via $(), ``, ;, &&, ||, etc.
|
||||||
|
*/
|
||||||
|
fun shellEscape(s: String): String {
|
||||||
|
// Single quotes prevent all shell interpretation except for single quotes themselves
|
||||||
|
// To include a single quote, we end the single-quoted string, add an escaped single quote, and start a new single-quoted string
|
||||||
|
// Example: "test'file" becomes 'test'\''file'
|
||||||
|
return "'" + s.replace("'", "'\\''") + "'"
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Shell.enableVerboseLogging = true
|
Shell.enableVerboseLogging = BuildConfig.DEBUG
|
||||||
Shell.setDefaultBuilder(
|
Shell.setDefaultBuilder(
|
||||||
Shell.Builder.create()
|
Shell.Builder.create()
|
||||||
.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_REDIRECT_STDERR)
|
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||||
.setTimeout(10)
|
.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.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -22,8 +24,8 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
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.Download
|
||||||
import androidx.compose.material.icons.filled.OpenInNew
|
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -68,9 +70,9 @@ private fun loadOsDownloads(context: Context): List<OsDownload> {
|
|||||||
OsDownload(
|
OsDownload(
|
||||||
name = obj.getString("name"),
|
name = obj.getString("name"),
|
||||||
category = obj.getString("category"),
|
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", ""),
|
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")
|
url = obj.getString("url")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -101,6 +103,7 @@ fun DownloadsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
contentWindowInsets = WindowInsets(0),
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("Download ISOs") },
|
title = { Text("Download ISOs") },
|
||||||
@@ -124,6 +127,7 @@ fun DownloadsScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
|
.navigationBarsPadding()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
groupedDownloads.forEach { (category, osList) ->
|
groupedDownloads.forEach { (category, osList) ->
|
||||||
@@ -280,7 +284,7 @@ private fun DownloadItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.OpenInNew,
|
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
|
||||||
contentDescription = "Open in browser",
|
contentDescription = "Open in browser",
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(20.dp)
|
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.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -49,14 +51,17 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import sh.sar.isodroid.data.IsoFile
|
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.CreateImgDialog
|
||||||
import sh.sar.isodroid.ui.components.FileContextMenu
|
import sh.sar.isodroid.ui.components.FileContextMenu
|
||||||
import sh.sar.isodroid.ui.components.FileItemCard
|
import sh.sar.isodroid.ui.components.FileItemCard
|
||||||
import sh.sar.isodroid.ui.components.MountDialog
|
import sh.sar.isodroid.ui.components.MountDialog
|
||||||
import sh.sar.isodroid.ui.components.StatusCard
|
import sh.sar.isodroid.ui.components.StatusCard
|
||||||
|
import sh.sar.isodroid.ui.components.UsbWarningDialog
|
||||||
import sh.sar.isodroid.viewmodel.MainViewModel
|
import sh.sar.isodroid.viewmodel.MainViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -69,12 +74,38 @@ fun MainScreen(
|
|||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
var selectedFile by remember { mutableStateOf<IsoFile?>(null) }
|
var selectedFile by remember { mutableStateOf<IsoFile?>(null) }
|
||||||
var showMountDialog by remember { mutableStateOf(false) }
|
var showMountDialog by remember { mutableStateOf(false) }
|
||||||
var showCreateImgDialog by remember { mutableStateOf(false) }
|
var showCreateImgDialog by remember { mutableStateOf(false) }
|
||||||
var contextMenuFile by remember { mutableStateOf<IsoFile?>(null) }
|
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()
|
val pullToRefreshState = rememberPullToRefreshState()
|
||||||
|
|
||||||
// Handle pull-to-refresh
|
// Handle pull-to-refresh
|
||||||
@@ -112,6 +143,7 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
contentWindowInsets = WindowInsets(0),
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("ISO Droid") },
|
title = { Text("ISO Droid") },
|
||||||
@@ -150,8 +182,10 @@ fun MainScreen(
|
|||||||
if (uiState.mountStatus.mounted) {
|
if (uiState.mountStatus.mounted) {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
showUsbWarningOrProceed(isUnmount = true) {
|
||||||
viewModel.unmount()
|
scope.launch {
|
||||||
|
viewModel.unmount()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon = {
|
icon = {
|
||||||
@@ -173,6 +207,7 @@ fun MainScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
|
.navigationBarsPadding()
|
||||||
.nestedScroll(pullToRefreshState.nestedScrollConnection)
|
.nestedScroll(pullToRefreshState.nestedScrollConnection)
|
||||||
) {
|
) {
|
||||||
if (uiState.isLoading && !pullToRefreshState.isRefreshing) {
|
if (uiState.isLoading && !pullToRefreshState.isRefreshing) {
|
||||||
@@ -295,8 +330,14 @@ fun MainScreen(
|
|||||||
val filePath = file.path // Capture path before clearing state
|
val filePath = file.path // Capture path before clearing state
|
||||||
showMountDialog = false
|
showMountDialog = false
|
||||||
selectedFile = null
|
selectedFile = null
|
||||||
scope.launch {
|
showUsbWarningOrProceed(
|
||||||
viewModel.mount(filePath, options)
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
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.Info
|
||||||
import androidx.compose.material.icons.filled.Notifications
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
import androidx.compose.material.icons.filled.Security
|
import androidx.compose.material.icons.filled.Security
|
||||||
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
@@ -58,6 +61,7 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
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
|
// Re-check permissions when returning to the app
|
||||||
if (activity != null) {
|
if (activity != null) {
|
||||||
androidx.compose.runtime.DisposableEffect(activity) {
|
androidx.compose.runtime.DisposableEffect(activity) {
|
||||||
@@ -145,6 +153,7 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
contentWindowInsets = WindowInsets(0),
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("Settings") },
|
title = { Text("Settings") },
|
||||||
@@ -168,6 +177,7 @@ fun SettingsScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
|
.navigationBarsPadding()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
// Storage section
|
// Storage section
|
||||||
@@ -246,6 +256,61 @@ fun SettingsScreen(
|
|||||||
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
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
|
// About section
|
||||||
SectionHeader(title = "About")
|
SectionHeader(title = "About")
|
||||||
|
|
||||||
@@ -456,9 +521,10 @@ private fun DirectoryBrowserDialog(
|
|||||||
fun loadContents(path: String) {
|
fun loadContents(path: String) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
val safePath = RootManager.shellEscape(path)
|
||||||
// Load directories
|
// Load directories
|
||||||
val dirResult = RootManager.executeCommand(
|
val dirResult = RootManager.executeCommand(
|
||||||
"find \"$path\" -maxdepth 1 -mindepth 1 -type d 2>/dev/null"
|
"find $safePath -maxdepth 1 -mindepth 1 -type d 2>/dev/null"
|
||||||
)
|
)
|
||||||
val directories = if (dirResult.success && dirResult.output.isNotBlank()) {
|
val directories = if (dirResult.success && dirResult.output.isNotBlank()) {
|
||||||
dirResult.output.lines()
|
dirResult.output.lines()
|
||||||
@@ -467,8 +533,9 @@ private fun DirectoryBrowserDialog(
|
|||||||
.filter { !it.substringAfterLast("/").startsWith(".") }
|
.filter { !it.substringAfterLast("/").startsWith(".") }
|
||||||
.map { dirPath ->
|
.map { dirPath ->
|
||||||
// Check if this directory was created by the app (has .isodroiddir marker)
|
// Check if this directory was created by the app (has .isodroiddir marker)
|
||||||
|
val safeDirPath = RootManager.shellEscape(dirPath)
|
||||||
val markerCheck = RootManager.executeCommand(
|
val markerCheck = RootManager.executeCommand(
|
||||||
"test -f \"$dirPath/.isodroiddir\" && echo 'yes' || echo 'no'"
|
"test -f $safeDirPath/.isodroiddir && echo 'yes' || echo 'no'"
|
||||||
)
|
)
|
||||||
val isDeletable = markerCheck.output.trim() == "yes"
|
val isDeletable = markerCheck.output.trim() == "yes"
|
||||||
BrowserItem(dirPath.substringAfterLast("/"), true, dirPath, isDeletable)
|
BrowserItem(dirPath.substringAfterLast("/"), true, dirPath, isDeletable)
|
||||||
@@ -479,7 +546,7 @@ private fun DirectoryBrowserDialog(
|
|||||||
|
|
||||||
// Load ISO/IMG files
|
// Load ISO/IMG files
|
||||||
val fileResult = RootManager.executeCommand(
|
val fileResult = RootManager.executeCommand(
|
||||||
"find \"$path\" -maxdepth 1 -type f \\( -iname '*.iso' -o -iname '*.img' \\) 2>/dev/null"
|
"find $safePath -maxdepth 1 -type f \\( -iname '*.iso' -o -iname '*.img' \\) 2>/dev/null"
|
||||||
)
|
)
|
||||||
val files = if (fileResult.success && fileResult.output.isNotBlank()) {
|
val files = if (fileResult.success && fileResult.output.isNotBlank()) {
|
||||||
fileResult.output.lines()
|
fileResult.output.lines()
|
||||||
@@ -501,9 +568,10 @@ private fun DirectoryBrowserDialog(
|
|||||||
if (trimmedName.isEmpty()) return@launch
|
if (trimmedName.isEmpty()) return@launch
|
||||||
|
|
||||||
val newPath = "$currentPath/$trimmedName"
|
val newPath = "$currentPath/$trimmedName"
|
||||||
RootManager.executeCommand("mkdir -p \"$newPath\"")
|
val safeNewPath = RootManager.shellEscape(newPath)
|
||||||
|
RootManager.executeCommand("mkdir -p $safeNewPath")
|
||||||
// Create marker file to indicate this folder was created by the app
|
// Create marker file to indicate this folder was created by the app
|
||||||
RootManager.executeCommand("touch \"$newPath/.isodroiddir\"")
|
RootManager.executeCommand("touch $safeNewPath/.isodroiddir")
|
||||||
// Auto-navigate into the new folder
|
// Auto-navigate into the new folder
|
||||||
currentPath = newPath
|
currentPath = newPath
|
||||||
}
|
}
|
||||||
@@ -511,7 +579,8 @@ private fun DirectoryBrowserDialog(
|
|||||||
|
|
||||||
fun deleteFolder(path: String) {
|
fun deleteFolder(path: String) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
RootManager.executeCommand("rm -rf \"$path\"")
|
val safePath = RootManager.shellEscape(path)
|
||||||
|
RootManager.executeCommand("rm -rf $safePath")
|
||||||
loadContents(currentPath)
|
loadContents(currentPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import androidx.compose.material.icons.filled.Check
|
|||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Notifications
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
import androidx.compose.material.icons.filled.Security
|
import androidx.compose.material.icons.filled.Security
|
||||||
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
@@ -69,15 +70,18 @@ fun SetupWizardScreen(
|
|||||||
0 -> WelcomeStep(
|
0 -> WelcomeStep(
|
||||||
onNext = { currentStep = 1 }
|
onNext = { currentStep = 1 }
|
||||||
)
|
)
|
||||||
1 -> RootAccessStep(
|
1 -> DisclaimerStep(
|
||||||
onNext = { currentStep = 2 },
|
onNext = { currentStep = 2 }
|
||||||
onSkip = { currentStep = 2 }
|
|
||||||
)
|
)
|
||||||
2 -> NotificationStep(
|
2 -> RootAccessStep(
|
||||||
onNext = { currentStep = 3 },
|
onNext = { currentStep = 3 },
|
||||||
onSkip = { currentStep = 3 }
|
onSkip = { currentStep = 3 }
|
||||||
)
|
)
|
||||||
3 -> CompleteStep(
|
3 -> NotificationStep(
|
||||||
|
onNext = { currentStep = 4 },
|
||||||
|
onSkip = { currentStep = 4 }
|
||||||
|
)
|
||||||
|
4 -> CompleteStep(
|
||||||
onFinish = onSetupComplete
|
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
|
@Composable
|
||||||
private fun RootAccessStep(
|
private fun RootAccessStep(
|
||||||
onNext: () -> Unit,
|
onNext: () -> Unit,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
package sh.sar.isodroid.ui.theme
|
package sh.sar.isodroid.ui.theme
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -14,11 +13,7 @@ import androidx.compose.material3.dynamicDarkColorScheme
|
|||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
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.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
private val DarkColorScheme = darkColorScheme(
|
||||||
primary = Purple80,
|
primary = Purple80,
|
||||||
@@ -47,15 +42,6 @@ fun ISODroidTheme(
|
|||||||
else -> LightColorScheme
|
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(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = Typography,
|
typography = Typography,
|
||||||
|
|||||||
@@ -205,9 +205,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
// Create directory if it doesn't exist
|
// Create directory if it doesn't exist
|
||||||
if (!directory.exists()) {
|
if (!directory.exists()) {
|
||||||
RootManager.executeCommand("mkdir -p \"$currentPath\"")
|
val safePath = RootManager.shellEscape(currentPath)
|
||||||
|
RootManager.executeCommand("mkdir -p $safePath")
|
||||||
// Create marker file to indicate this folder was created by the app
|
// Create marker file to indicate this folder was created by the app
|
||||||
RootManager.executeCommand("touch \"$currentPath/.isodroiddir\"")
|
RootManager.executeCommand("touch $safePath/.isodroiddir")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try multiple methods to list files
|
// Try multiple methods to list files
|
||||||
@@ -220,8 +221,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
private suspend fun loadFilesViaFind(currentPath: String): List<IsoFile>? {
|
private suspend fun loadFilesViaFind(currentPath: String): List<IsoFile>? {
|
||||||
// Use find command - more reliable for getting full paths
|
// Use find command - more reliable for getting full paths
|
||||||
|
val safePath = RootManager.shellEscape(currentPath)
|
||||||
val result = RootManager.executeCommand(
|
val result = RootManager.executeCommand(
|
||||||
"find \"$currentPath\" -maxdepth 1 -type f \\( -iname '*.iso' -o -iname '*.img' \\) 2>/dev/null"
|
"find $safePath -maxdepth 1 -type f \\( -iname '*.iso' -o -iname '*.img' \\) 2>/dev/null"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!result.success || result.output.isBlank()) return null
|
if (!result.success || result.output.isBlank()) return null
|
||||||
@@ -231,8 +233,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
.mapNotNull { filePath ->
|
.mapNotNull { filePath ->
|
||||||
val file = File(filePath.trim())
|
val file = File(filePath.trim())
|
||||||
val name = file.name
|
val name = file.name
|
||||||
// Get file size via stat
|
// Get file size via stat with safe escaping
|
||||||
val sizeResult = RootManager.executeCommand("stat -c %s \"$filePath\" 2>/dev/null")
|
val safeFilePath = RootManager.shellEscape(filePath.trim())
|
||||||
|
val sizeResult = RootManager.executeCommand("stat -c %s $safeFilePath 2>/dev/null")
|
||||||
val size = sizeResult.output.trim().toLongOrNull() ?: 0L
|
val size = sizeResult.output.trim().toLongOrNull() ?: 0L
|
||||||
IsoFile(
|
IsoFile(
|
||||||
path = filePath.trim(),
|
path = filePath.trim(),
|
||||||
@@ -246,8 +249,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
private suspend fun loadFilesViaLs(currentPath: String): List<IsoFile>? {
|
private suspend fun loadFilesViaLs(currentPath: String): List<IsoFile>? {
|
||||||
// Simple ls command - just get filenames
|
// Simple ls command - just get filenames
|
||||||
|
val safePath = RootManager.shellEscape(currentPath)
|
||||||
val result = RootManager.executeCommand(
|
val result = RootManager.executeCommand(
|
||||||
"ls \"$currentPath\" 2>/dev/null"
|
"ls $safePath 2>/dev/null"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!result.success || result.output.isBlank()) return null
|
if (!result.success || result.output.isBlank()) return null
|
||||||
@@ -259,8 +263,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
.mapNotNull { name ->
|
.mapNotNull { name ->
|
||||||
val filePath = "$currentPath/$name"
|
val filePath = "$currentPath/$name"
|
||||||
// Get file size via stat
|
// Get file size via stat with safe escaping
|
||||||
val sizeResult = RootManager.executeCommand("stat -c %s \"$filePath\" 2>/dev/null")
|
val safeFilePath = RootManager.shellEscape(filePath)
|
||||||
|
val sizeResult = RootManager.executeCommand("stat -c %s $safeFilePath 2>/dev/null")
|
||||||
val size = sizeResult.output.trim().toLongOrNull() ?: 0L
|
val size = sizeResult.output.trim().toLongOrNull() ?: 0L
|
||||||
IsoFile(
|
IsoFile(
|
||||||
path = filePath,
|
path = filePath,
|
||||||
@@ -444,13 +449,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val blockSize = 1024 * 1024L // 1MB blocks
|
val blockSize = 1024 * 1024L // 1MB blocks
|
||||||
val totalBlocks = totalBytes / blockSize
|
val totalBlocks = totalBytes / blockSize
|
||||||
var writtenBlocks = 0L
|
var writtenBlocks = 0L
|
||||||
|
val safeFilePath = RootManager.shellEscape(filePath)
|
||||||
|
|
||||||
// Create file with dd in background, checking for cancellation
|
// Create file with dd in background, checking for cancellation
|
||||||
val result = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
|
val result = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
// First, create the file with truncate to reserve space indication
|
// First, create the file with truncate to reserve space indication
|
||||||
val createResult = RootManager.executeCommand(
|
val createResult = RootManager.executeCommand(
|
||||||
"dd if=/dev/zero of=\"$filePath\" bs=1M count=0 seek=$totalBlocks 2>/dev/null"
|
"dd if=/dev/zero of=$safeFilePath bs=1M count=0 seek=$totalBlocks 2>/dev/null"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!createResult.success) {
|
if (!createResult.success) {
|
||||||
@@ -461,18 +467,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
while (writtenBlocks < totalBlocks) {
|
while (writtenBlocks < totalBlocks) {
|
||||||
if (CreateImgEventBus.isCancelRequested()) {
|
if (CreateImgEventBus.isCancelRequested()) {
|
||||||
// Clean up partial file
|
// Clean up partial file
|
||||||
RootManager.executeCommand("rm -f \"$filePath\"")
|
RootManager.executeCommand("rm -f $safeFilePath")
|
||||||
return@withContext false
|
return@withContext false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write a chunk (up to 64MB at a time for efficiency)
|
// Write a chunk (up to 64MB at a time for efficiency)
|
||||||
val chunksToWrite = minOf(64, totalBlocks - writtenBlocks)
|
val chunksToWrite = minOf(64, totalBlocks - writtenBlocks)
|
||||||
val chunkResult = RootManager.executeCommand(
|
val chunkResult = RootManager.executeCommand(
|
||||||
"dd if=/dev/zero of=\"$filePath\" bs=1M count=$chunksToWrite seek=$writtenBlocks conv=notrunc 2>/dev/null"
|
"dd if=/dev/zero of=$safeFilePath bs=1M count=$chunksToWrite seek=$writtenBlocks conv=notrunc 2>/dev/null"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!chunkResult.success) {
|
if (!chunkResult.success) {
|
||||||
RootManager.executeCommand("rm -f \"$filePath\"")
|
RootManager.executeCommand("rm -f $safeFilePath")
|
||||||
return@withContext false
|
return@withContext false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,7 +490,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
RootManager.executeCommand("rm -f \"$filePath\"")
|
RootManager.executeCommand("rm -f $safeFilePath")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -508,14 +514,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val oldPath = file.path
|
val oldPath = file.path
|
||||||
val newPath = "${oldPath.substringBeforeLast("/")}/$newName"
|
val newPath = "${oldPath.substringBeforeLast("/")}/$newName"
|
||||||
|
|
||||||
|
// Use shell-safe escaping to prevent command injection
|
||||||
|
val safeOldPath = RootManager.shellEscape(oldPath)
|
||||||
|
val safeNewPath = RootManager.shellEscape(newPath)
|
||||||
|
|
||||||
// Check if new file already exists
|
// Check if new file already exists
|
||||||
val checkResult = RootManager.executeCommand("test -f \"$newPath\" && echo exists")
|
val checkResult = RootManager.executeCommand("test -f $safeNewPath && echo exists")
|
||||||
if (checkResult.output.trim() == "exists") {
|
if (checkResult.output.trim() == "exists") {
|
||||||
_uiState.update { it.copy(errorMessage = "File already exists: $newName") }
|
_uiState.update { it.copy(errorMessage = "File already exists: $newName") }
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val result = RootManager.executeCommand("mv \"$oldPath\" \"$newPath\"")
|
val result = RootManager.executeCommand("mv $safeOldPath $safeNewPath")
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
_uiState.update { it.copy(successMessage = "Renamed to $newName") }
|
_uiState.update { it.copy(successMessage = "Renamed to $newName") }
|
||||||
loadFiles()
|
loadFiles()
|
||||||
@@ -527,7 +537,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
fun deleteFile(file: IsoFile) {
|
fun deleteFile(file: IsoFile) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = RootManager.executeCommand("rm -f \"${file.path}\"")
|
val safePath = RootManager.shellEscape(file.path)
|
||||||
|
val result = RootManager.executeCommand("rm -f $safePath")
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
_uiState.update { it.copy(successMessage = "Deleted ${file.name}") }
|
_uiState.update { it.copy(successMessage = "Deleted ${file.name}") }
|
||||||
loadFiles()
|
loadFiles()
|
||||||
|
|||||||
@@ -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
|
|
||||||
38
docs/DISCLAIMER.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Disclaimer
|
||||||
|
|
||||||
|
## About This Project
|
||||||
|
|
||||||
|
ISO Droid was developed with AI assistance (Claude by Anthropic). This is mentioned for transparency, regardless of who or what wrote it, it can contain bugs.
|
||||||
|
|
||||||
|
Reasonable precautions have been taken to prevent unintended behavior.
|
||||||
|
|
||||||
|
## Root Access
|
||||||
|
|
||||||
|
This app requires root access to function. Root access gives applications elevated privileges on your device, which means:
|
||||||
|
|
||||||
|
- Operations can affect system-level functionality
|
||||||
|
- Mistakes can potentially cause issues that require recovery
|
||||||
|
- You should understand what an operation does before executing it
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
- **Keep backups** of important data (good practice regardless)
|
||||||
|
- **Understand the operations** before running them
|
||||||
|
- **Test on non-critical setups first** if you're unsure
|
||||||
|
- **Report bugs** if you encounter them
|
||||||
|
|
||||||
|
## My Usage
|
||||||
|
|
||||||
|
I (the developer) use this app on my own devices regularly. It works for my use cases, but your device, kernel, and setup may differ, and there may be edge cases I haven't encountered.
|
||||||
|
|
||||||
|
## No Warranty
|
||||||
|
|
||||||
|
This software is provided "as is" without warranty of any kind. See the [LICENSE](../LICENSE) file for full details (GPL-3.0).
|
||||||
|
|
||||||
|
## Bug Reports & Contributions
|
||||||
|
|
||||||
|
Found a bug? Have a suggestion? Please open an issue:
|
||||||
|
|
||||||
|
- **Issues**: [git.sargit.com/sargit/ISODroid/issues](https://git.sargit.com/sargit/ISODroid/issues)
|
||||||
|
|
||||||
|
Contributions are welcome. If you fix something or improve the app, consider submitting a pull request.
|
||||||
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 108 KiB |
5
fastlane/metadata/android/en-US/changelogs/6.txt
Normal file
@@ -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
|
||||||
2
fastlane/metadata/android/en-US/changelogs/7.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- Always use app-bundled isodrive binary instead of system binary
|
||||||
|
- Prep for edge-to-edge compatibility
|
||||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 145 KiB |
@@ -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
|
|
||||||