Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
804e4c3ae3
|
||
|
|
b9a95bd12d
|
||
|
|
32406e335a
|
||
|
|
235053eba6
|
||
|
|
8e3b29b5df
|
||
|
|
22d729ce53
|
||
|
|
06b67d64c7
|
@@ -4,6 +4,14 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-03-12T19:54:01.237140412Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=a703e092" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
||||
@@ -5,6 +5,17 @@ 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.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
|
||||
|
||||
@@ -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 |
|
||||
|:--:|:--:|:--:|:--:|:--:|
|
||||
|  |  |  |  |  |
|
||||
|  |  |  |  |  |
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ android {
|
||||
applicationId = "sh.sar.isodroid"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 5
|
||||
versionName = "1.5"
|
||||
versionCode = 6
|
||||
versionName = "1.6"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -49,14 +49,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 +72,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
|
||||
@@ -150,8 +179,10 @@ fun MainScreen(
|
||||
if (uiState.mountStatus.mounted) {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.unmount()
|
||||
showUsbWarningOrProceed(isUnmount = true) {
|
||||
scope.launch {
|
||||
viewModel.unmount()
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
@@ -295,8 +326,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 +374,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,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 +59,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 +117,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) {
|
||||
@@ -246,6 +252,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,
|
||||
|
||||
|
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 |
@@ -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
|
||||
|
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 |