7 Commits
Author SHA1 Message Date
shihaam 804e4c3ae3 new version: 1.6 2026-03-13 01:28:33 +05:00
shihaam b9a95bd12d reorgnize images 2026-03-13 01:28:09 +05:00
shihaam 32406e335a remove deprecated: 'static field FLAG_REDIRECT_STDERR: Int' 2026-03-13 01:19:04 +05:00
shihaam 235053eba6 no clue tf this is 2026-03-13 01:06:01 +05:00
shihaam 8e3b29b5df Add USB services restart warning toggle 2026-03-13 01:05:45 +05:00
shihaam 22d729ce53 Add USB services restart warning 2026-03-13 01:02:48 +05:00
shihaam 06b67d64c7 add Caution about root app 2026-03-13 00:42:50 +05:00
20 changed files with 322 additions and 14 deletions
+8
View File
@@ -4,6 +4,14 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <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> </SelectionState>
</selectionStates> </selectionStates>
</component> </component>
+11
View File
@@ -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/), 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.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
+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 ## 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 ## Features
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "sh.sar.isodroid" applicationId = "sh.sar.isodroid"
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 5 versionCode = 6
versionName = "1.5" versionName = "1.6"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -28,7 +28,7 @@ object RootManager {
Shell.enableVerboseLogging = BuildConfig.DEBUG 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")
}
}
)
}
@@ -49,14 +49,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 +72,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
@@ -150,8 +179,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 = {
@@ -295,8 +326,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 +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.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 +59,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 +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 // Re-check permissions when returning to the app
if (activity != null) { if (activity != null) {
androidx.compose.runtime.DisposableEffect(activity) { androidx.compose.runtime.DisposableEffect(activity) {
@@ -246,6 +252,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")
@@ -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,
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
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