Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class DSUInstaller(
private val onCreatePartition: (partition: String) -> Unit,
private val onInstallationStepUpdate: (step: InstallationStep) -> Unit,
private val onInstallationSuccess: () -> Unit,
private val preserveUserdata: Boolean = false,
) : () -> Unit, DynamicSystemImpl() {

private val tag = this.javaClass.simpleName
Expand Down Expand Up @@ -223,13 +224,21 @@ class DSUInstaller(
onInstallationError(InstallationStep.ERROR_ALREADY_RUNNING_DYN_OS, "")
return
}
if (isInstalled) {

// When preserving userdata, we allow updating an installed DSU
if (isInstalled && !preserveUserdata) {
onInstallationError(InstallationStep.ERROR_REQUIRES_DISCARD_DSU, "")
return
}

forceStopDSU()
startInstallation(Constants.DEFAULT_SLOT)
installWritablePartition("userdata", userdataSize)

// Only create userdata partition when preserve is disabled OR DSU is not installed
if (!preserveUserdata || !isInstalled) {
installWritablePartition("userdata", userdataSize)
}

when (dsuInstallation.type) {
Type.SINGLE_SYSTEM_IMAGE -> {
installImage(
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/vegabobo/dsusideloader/model/Session.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import vegabobo.dsusideloader.util.OperationMode
data class InstallationPreferences(
var isUnmountSdCard: Boolean = false,
var useBuiltinInstaller: Boolean = false,
var preserveUserdata: Boolean = false,
)

class UserSelection(
Expand Down
113 changes: 84 additions & 29 deletions app/src/main/java/vegabobo/dsusideloader/ui/cards/UserdataCard.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,50 +17,105 @@ import androidx.compose.ui.unit.sp
import vegabobo.dsusideloader.R
import vegabobo.dsusideloader.ui.components.CardBox
import vegabobo.dsusideloader.ui.components.FileSelectionBox
import vegabobo.dsusideloader.ui.components.PreferenceItem
import vegabobo.dsusideloader.ui.screen.home.UserDataCardState

@Composable
fun UserdataCard(
isEnabled: Boolean,
uiState: UserDataCardState,
isDsuInstalled: Boolean,
modifier: Modifier = Modifier,
onValueChange: (String) -> Unit,
onCheckedChange: (Boolean) -> Unit = {},
onPreserveCheckedChange: (Boolean) -> Unit = {},
) {
CardBox(
modifier = modifier,
cardTitle = stringResource(id = R.string.userdata_size),
addToggle = true,
isToggleEnabled = !isEnabled,
isToggleChecked = uiState.isSelected,
onCheckedChange = onCheckedChange,
) {
AnimatedVisibility(
visible = uiState.isSelected,
enter = expandVertically(),
exit = shrinkVertically(),
// When DSU is installed and not currently installing, show simplified preserve UI
// Note: isEnabled parameter indicates installation is in progress (when true)
// So !isEnabled means installation is NOT in progress
if (isDsuInstalled && !isEnabled) {
// When DSU is installed, show a simpler card with preserve option
CardBox(
modifier = modifier,
cardTitle = stringResource(id = R.string.userdata_size),
addToggle = false,
) {
Column {
FileSelectionBox(
modifier = Modifier.padding(bottom = 4.dp),
// Preserve userdata toggle (default: ON)
PreferenceItem(
title = stringResource(id = R.string.preserve_userdata),
description = stringResource(id = R.string.preserve_userdata_desc),
showToggle = true,
isChecked = uiState.preserveSelected,
isEnabled = !isEnabled,
isError = uiState.isError,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
textFieldValue = uiState.text,
textFieldTitle = stringResource(id = R.string.userdata_size_info),
onValueChange = onValueChange,
onClick = { onPreserveCheckedChange(!uiState.preserveSelected) }
)
AnimatedVisibility(visible = uiState.isError) {
Text(
modifier = Modifier.padding(start = 1.dp),
text = stringResource(
id = R.string.allowed_userdata_allocation,
uiState.maximumAllowed,
),
color = MaterialTheme.colorScheme.error,
lineHeight = 14.sp,
fontSize = 14.sp,

// Show userdata size input only when preserve is OFF
AnimatedVisibility(visible = !uiState.preserveSelected) {
Column {
FileSelectionBox(
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp),
isEnabled = !isEnabled,
isError = uiState.isError,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
textFieldValue = uiState.text,
textFieldTitle = stringResource(id = R.string.userdata_size_info),
onValueChange = onValueChange,
)
AnimatedVisibility(visible = uiState.isError) {
Text(
modifier = Modifier.padding(start = 1.dp),
text = stringResource(
id = R.string.allowed_userdata_allocation,
uiState.maximumAllowed,
),
color = MaterialTheme.colorScheme.error,
lineHeight = 14.sp,
fontSize = 14.sp,
)
}
}
}
Comment on lines +55 to +79
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When DSU is installed and preserve userdata is disabled (preserveSelected = false), the userdata size input field is shown. However, the isSelected property of userDataCard is not being considered. In the normal flow (when DSU is not installed), users must explicitly enable the userdata size option via a toggle before they can input a size. This inconsistency could lead to confusion. Consider whether the isSelected check should also apply to the preserve userdata scenario, or if the current behavior is intentional for updating scenarios.

Copilot uses AI. Check for mistakes.
}
}
} else {
// Normal userdata card when DSU is not installed
CardBox(
modifier = modifier,
cardTitle = stringResource(id = R.string.userdata_size),
addToggle = true,
isToggleEnabled = !isEnabled,
isToggleChecked = uiState.isSelected,
onCheckedChange = onCheckedChange,
) {
AnimatedVisibility(
visible = uiState.isSelected,
enter = expandVertically(),
exit = shrinkVertically(),
) {
Column {
FileSelectionBox(
modifier = Modifier.padding(bottom = 4.dp),
isEnabled = !isEnabled,
isError = uiState.isError,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
textFieldValue = uiState.text,
textFieldTitle = stringResource(id = R.string.userdata_size_info),
onValueChange = onValueChange,
)
AnimatedVisibility(visible = uiState.isError) {
Text(
modifier = Modifier.padding(start = 1.dp),
text = stringResource(
id = R.string.allowed_userdata_allocation,
uiState.maximumAllowed,
),
color = MaterialTheme.colorScheme.error,
lineHeight = 14.sp,
fontSize = 14.sp,
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import vegabobo.dsusideloader.R
import vegabobo.dsusideloader.preparation.InstallationStep
import vegabobo.dsusideloader.ui.cards.installation.content.DsuInstalledCardContent
import vegabobo.dsusideloader.ui.cards.installation.content.NotInstallingCardContent
import vegabobo.dsusideloader.ui.cards.installation.content.ProgressableCardContent
import vegabobo.dsusideloader.ui.screen.home.InstallationCardState
Expand Down Expand Up @@ -35,12 +36,13 @@ fun InstallationCardStep(
onClickInstall = onClickInstall,
)
InstallationStep.DSU_ALREADY_INSTALLED ->
ProgressableCardContent(
text = stringResource(R.string.dsu_already_installed),
textFirstButton = stringResource(id = R.string.reboot_into_dsu),
onClickFirstButton = onClickRebootToDynOS,
textSecondButton = stringResource(id = R.string.discard),
onClickSecondButton = onClickDiscardDsu,
DsuInstalledCardContent(
textFieldInteraction = textFieldInteraction,
uiState = uiState,
onClickClear = onClickClear,
onClickInstall = onClickInstall,
onClickRebootToDynOS = onClickRebootToDynOS,
onClickDiscardDsu = onClickDiscardDsu,
)
InstallationStep.DSU_ALREADY_RUNNING_DYN_OS ->
ProgressableCardContent(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package vegabobo.dsusideloader.ui.cards.installation.content

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import vegabobo.dsusideloader.R
import vegabobo.dsusideloader.ui.components.FileSelectionBox
import vegabobo.dsusideloader.ui.components.buttons.ErrorButton
import vegabobo.dsusideloader.ui.components.buttons.PrimaryButton
import vegabobo.dsusideloader.ui.components.buttons.SecondaryButton
import vegabobo.dsusideloader.ui.screen.home.InstallationCardState

@Composable
fun DsuInstalledCardContent(
textFieldInteraction: MutableInteractionSource,
uiState: InstallationCardState,
onClickClear: () -> Unit,
onClickInstall: () -> Unit,
onClickRebootToDynOS: () -> Unit,
onClickDiscardDsu: () -> Unit,
) {
// Message indicating DSU is already installed
Text(
text = stringResource(R.string.dsu_already_installed),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(bottom = 8.dp),
)

// File selection box (same as NotInstallingCardContent)
FileSelectionBox(
textFieldInteraction = textFieldInteraction,
isEnabled = uiState.isTextFieldEnabled,
isError = uiState.isError,
isReadOnly = true,
textFieldValue = uiState.text,
textFieldTitle = stringResource(id = R.string.select_gsi_info),
)
Spacer(modifier = Modifier.padding(top = 10.dp))

// Install/Clear buttons row
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
AnimatedVisibility(visible = uiState.isError) {
Text(
text = stringResource(id = R.string.file_unsupported),
modifier = Modifier.padding(start = 2.dp),
color = MaterialTheme.colorScheme.error,
)
}
Spacer(modifier = Modifier.weight(1F))
if (uiState.isInstallable) {
SecondaryButton(
text = stringResource(R.string.clear),
onClick = onClickClear,
)
Spacer(modifier = Modifier.padding(end = 6.dp))
}
PrimaryButton(
text = stringResource(R.string.install),
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The button text should display "Update" instead of "Install" when DSU is already installed, to accurately reflect that the operation is updating an existing installation. The "update" string resource is already defined at line 36 of strings.xml but is not being used here.

Suggested change
text = stringResource(R.string.install),
text = stringResource(R.string.update),

Copilot uses AI. Check for mistakes.
onClick = onClickInstall,
isEnabled = uiState.isInstallable,
)
}

Spacer(modifier = Modifier.padding(top = 8.dp))

// Reboot and Discard buttons row
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Spacer(modifier = Modifier.weight(1F))
SecondaryButton(
text = stringResource(id = R.string.reboot_into_dsu),
onClick = onClickRebootToDynOS,
)
Spacer(modifier = Modifier.padding(end = 6.dp))
ErrorButton(
text = stringResource(id = R.string.discard),
onClick = onClickDiscardDsu,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,10 @@ fun Home(
UserdataCard(
isEnabled = uiState.isInstalling(),
uiState = uiState.userDataCard,
isDsuInstalled = uiState.isDsuInstalled,
onCheckedChange = { homeViewModel.onCheckUserdataCard() },
onValueChange = { homeViewModel.updateUserdataSize(it) },
onPreserveCheckedChange = { homeViewModel.onCheckPreserveUserdata(it) },
)
ImageSizeCard(
isEnabled = uiState.isInstalling(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ data class UserDataCardState(
val isError: Boolean = false,
val text: String = "",
val maximumAllowed: Int = 0,
val preserveSelected: Boolean = false,
)

data class ImageSizeCardState(
Expand Down Expand Up @@ -54,8 +55,10 @@ data class HomeUiState(
val installationLogs: String = "",
val passedInitialChecks: Boolean = false,
val shouldKeepScreenOn: Boolean = false,
val isDsuInstalled: Boolean = false,
) {
fun isInstalling(): Boolean {
return installationCard.installationStep != InstallationStep.NOT_INSTALLING
return installationCard.installationStep != InstallationStep.NOT_INSTALLING &&
installationCard.installationStep != InstallationStep.DSU_ALREADY_INSTALLED
}
}
Loading
Loading