Refactor: State management in AppShell.kt

This commit is contained in:
2025-11-06 17:24:08 +01:00
parent fd04f9b5d9
commit cf799eba12
2 changed files with 167 additions and 168 deletions

View File

@@ -37,8 +37,7 @@ Noteshop is a versatile and privacy-focused application for managing your notes,
1. Clone the repository:
```sh
git clone YOUR_GITEA_REPOSITORY_URL_HERE
cd noteshop
git clone https://git.ilunix.de/punix/noteshop.git && cd noteshop
```
2. Build the app using Gradle:
```sh

View File

@@ -2,6 +2,7 @@ package de.lxtools.noteshop.ui
import android.content.Context
import android.content.Intent
import android.util.Base64
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@@ -41,7 +42,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -74,12 +74,11 @@ import de.lxtools.noteshop.ui.recipes.RecipesViewModel
import de.lxtools.noteshop.ui.shopping.ShoppingListsViewModel
import de.lxtools.noteshop.ui.theme.ColorTheme
import de.lxtools.noteshop.ui.webapp.WebAppIntegrationViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.FileOutputStream
import javax.crypto.SecretKey
import android.util.Base64
import javax.crypto.spec.SecretKeySpec
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -98,10 +97,10 @@ fun AppShell(
val context = LocalContext.current
val scope = rememberCoroutineScope()
var showStartupPasswordDialog by rememberSaveable { mutableStateOf(false) }
var startupUnlockErrorMessage by rememberSaveable { mutableStateOf<String?>(null) }
val (showStartupPasswordDialog, setShowStartupPasswordDialog) = rememberSaveable { mutableStateOf(false) }
val (startupUnlockErrorMessage, setStartupUnlockErrorMessage) = rememberSaveable { mutableStateOf<String?>(null) }
var syncFolderUriString by rememberSaveable {
val (syncFolderUriString, setSyncFolderUriString) = rememberSaveable {
mutableStateOf(
sharedPrefs.getString(
"sync_folder_uri",
@@ -114,7 +113,7 @@ fun AppShell(
val listener =
android.content.SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
if (key == "sync_folder_uri") {
syncFolderUriString = sharedPreferences.getString(key, null)
setSyncFolderUriString(sharedPreferences.getString(key, null))
}
}
sharedPrefs.registerOnSharedPreferenceChangeListener(listener)
@@ -128,7 +127,7 @@ fun AppShell(
val keyManager = remember { KeyManager(context, canUseBiometrics) }
val fileEncryptor = remember { FileEncryptor() }
var isEncryptionEnabled by rememberSaveable {
val (isEncryptionEnabled, setIsEncryptionEnabled) = rememberSaveable {
mutableStateOf(
sharedPrefs.getBoolean(
"encryption_enabled",
@@ -136,9 +135,9 @@ fun AppShell(
)
)
}
var hasEncryptionPassword by rememberSaveable { mutableStateOf(keyManager.hasKey()) }
var showPasswordDialog by rememberSaveable { mutableStateOf(false) }
var showDeleteConfirmationDialog by remember { mutableStateOf(false) }
val (hasEncryptionPassword, setHasEncryptionPassword) = rememberSaveable { mutableStateOf(keyManager.hasKey()) }
val (showPasswordDialog, setShowPasswordDialog) = rememberSaveable { mutableStateOf(false) }
val (showDeleteConfirmationDialog, setShowDeleteConfirmationDialog) = remember { mutableStateOf(false) }
val onEncryptionToggle: (Boolean) -> Unit = { enabled ->
if (!enabled) { // Turning OFF
@@ -149,8 +148,8 @@ fun AppShell(
if (cipher == null) {
// Fallback for safety, though this state should be unlikely if hasEncryptionPassword is true
keyManager.deleteKey()
hasEncryptionPassword = false
isEncryptionEnabled = false
setHasEncryptionPassword(false)
setIsEncryptionEnabled(false)
sharedPrefs.edit {
putBoolean("encryption_enabled", false)
remove("biometric_unlock_enabled")
@@ -174,8 +173,8 @@ fun AppShell(
// Now, safely delete the key and disable the feature
keyManager.deleteKey()
hasEncryptionPassword = false
isEncryptionEnabled = false
setHasEncryptionPassword(false)
setIsEncryptionEnabled(false)
sharedPrefs.edit {
putBoolean("encryption_enabled", false)
remove("biometric_unlock_enabled")
@@ -188,13 +187,13 @@ fun AppShell(
)
} else {
// No password was ever set, just turn the switch off.
isEncryptionEnabled = false
setIsEncryptionEnabled(false)
sharedPrefs.edit {
putBoolean("encryption_enabled", false)
}
}
} else { // Turning ON
isEncryptionEnabled = true
setIsEncryptionEnabled(true)
sharedPrefs.edit {
putBoolean("encryption_enabled", true)
}
@@ -202,7 +201,7 @@ fun AppShell(
}
val onSetEncryptionPassword: () -> Unit = {
showPasswordDialog = true
setShowPasswordDialog(true)
}
val onRemoveEncryption: () -> Unit = {
@@ -210,8 +209,8 @@ fun AppShell(
val cipher = keyManager.getDecryptionCipher()
if (cipher == null) {
// This can happen if no key is set. In this case, just disable the feature.
hasEncryptionPassword = false
isEncryptionEnabled = false
setHasEncryptionPassword(false)
setIsEncryptionEnabled(false)
sharedPrefs.edit {
putBoolean("encryption_enabled", false)
remove("biometric_unlock_enabled")
@@ -233,8 +232,8 @@ fun AppShell(
// Now it's safe to delete the key
keyManager.deleteKey()
hasEncryptionPassword = false
isEncryptionEnabled = false
setHasEncryptionPassword(false)
setIsEncryptionEnabled(false)
sharedPrefs.edit {
putBoolean("encryption_enabled", false)
remove("biometric_unlock_enabled")
@@ -515,8 +514,8 @@ fun AppShell(
}
val lifecycleOwner = LocalLifecycleOwner.current
var secretKey by rememberSaveable(stateSaver = SecretKeySaver) { mutableStateOf(null) }
var isDecryptionAttempted by rememberSaveable { mutableStateOf(false) }
val (secretKey, setSecretKey) = rememberSaveable(stateSaver = SecretKeySaver) { mutableStateOf<SecretKey?>(null) }
val (isDecryptionAttempted, setIsDecryptionAttempted) = rememberSaveable { mutableStateOf(false) }
DisposableEffect(lifecycleOwner, context, scope) {
val observer = LifecycleEventObserver { _, event ->
@@ -541,32 +540,33 @@ fun AppShell(
onSuccess = { result ->
result.cryptoObject?.cipher?.let { authenticatedCipher ->
scope.launch {
secretKey =
setSecretKey(
keyManager.getSecretKeyFromAuthenticatedCipher(
authenticatedCipher
)
isDecryptionAttempted = true
)
setIsDecryptionAttempted(true)
}
}
},
onFailed = {},
onError = { _, _ ->
showStartupPasswordDialog = true
setShowStartupPasswordDialog(true)
}
)
} else {
isDecryptionAttempted = true // No key/cipher, proceed
setIsDecryptionAttempted(true) // No key/cipher, proceed
}
} catch (e: Exception) {
Log.e("AppShell", "Error during decryption prompt: ${e.message}")
isDecryptionAttempted = true // Proceed without key
setIsDecryptionAttempted(true) // Proceed without key
}
}
} else {
showStartupPasswordDialog = true
setShowStartupPasswordDialog(true)
}
} else {
isDecryptionAttempted = true // No encryption, proceed
setIsDecryptionAttempted(true) // No encryption, proceed
}
}
@@ -594,7 +594,7 @@ fun AppShell(
}
}
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
var currentScreen: Screen by rememberSaveable {
val (currentScreen, setCurrentScreen) = rememberSaveable {
val defaultStartScreenRoute = sharedPrefs.getString("default_start_screen", "last_screen")
val initialScreen = when (defaultStartScreenRoute) {
Screen.ShoppingLists.route -> Screen.ShoppingLists
@@ -624,58 +624,58 @@ fun AppShell(
LaunchedEffect(Unit) {
val firstLaunchCompleted = sharedPrefs.getBoolean("first_launch_completed", false)
if (!firstLaunchCompleted) {
currentScreen = Screen.GuidedTour
setCurrentScreen(Screen.GuidedTour)
sharedPrefs.edit { putBoolean("first_launch_completed", true) }
}
}
var selectedListId: Int? by rememberSaveable { mutableStateOf(null) }
var selectedNoteId: Int? by rememberSaveable { mutableStateOf(null) }
var selectedRecipeId: Int? by rememberSaveable { mutableStateOf(null) }
val (selectedListId, setSelectedListId) = rememberSaveable { mutableStateOf<Int?>(null) }
val (selectedNoteId, setSelectedNoteId) = rememberSaveable { mutableStateOf<Int?>(null) }
val (selectedRecipeId, setSelectedRecipeId) = rememberSaveable { mutableStateOf<Int?>(null) }
var itemWasInitiallyLocked by rememberSaveable { mutableStateOf(false) }
val (itemWasInitiallyLocked, setItemWasInitiallyLocked) = rememberSaveable { mutableStateOf(false) }
LaunchedEffect(selectedNoteId, selectedListId, selectedRecipeId) {
if (selectedNoteId == null && selectedListId == null && selectedRecipeId == null) {
itemWasInitiallyLocked = false
setItemWasInitiallyLocked(false)
}
}
var showListDialog by rememberSaveable { mutableStateOf(false) }
val (showListDialog, setShowListDialog) = rememberSaveable { mutableStateOf(false) }
val listDetails by shoppingListsViewModel.listDetails.collectAsState()
var showNoteDialog by rememberSaveable { mutableStateOf(false) }
val (showNoteDialog, setShowNoteDialog) = rememberSaveable { mutableStateOf(false) }
val noteDetails by notesViewModel.noteDetails.collectAsState()
var showRecipeDialog by rememberSaveable { mutableStateOf(false) }
val (showRecipeDialog, setShowRecipeDialog) = rememberSaveable { mutableStateOf(false) }
val recipeDetails by recipesViewModel.recipeDetails.collectAsState()
var showJsonDialog by rememberSaveable { mutableStateOf(false) }
var showSetPasswordDialog by rememberSaveable { mutableStateOf(false) }
var showSetRecipePasswordDialog by rememberSaveable { mutableStateOf(false) }
var showSetListPasswordDialog by rememberSaveable { mutableStateOf(false) }
val (showJsonDialog, setShowJsonDialog) = rememberSaveable { mutableStateOf(false) }
val (showSetPasswordDialog, setShowSetPasswordDialog) = rememberSaveable { mutableStateOf(false) }
val (showSetRecipePasswordDialog, setShowSetRecipePasswordDialog) = rememberSaveable { mutableStateOf(false) }
val (showSetListPasswordDialog, setShowSetListPasswordDialog) = rememberSaveable { mutableStateOf(false) }
var showUnlockPasswordDialog by rememberSaveable { mutableStateOf(false) }
var unlockErrorMessage by rememberSaveable { mutableStateOf<String?>(null) }
var itemToUnlockId by rememberSaveable { mutableStateOf<Int?>(null) }
var itemToUnlockType by rememberSaveable { mutableStateOf<Screen?>(null) }
val (showUnlockPasswordDialog, setShowUnlockPasswordDialog) = rememberSaveable { mutableStateOf(false) }
val (unlockErrorMessage, setUnlockErrorMessage) = rememberSaveable { mutableStateOf<String?>(null) }
val (itemToUnlockId, setItemToUnlockId) = rememberSaveable { mutableStateOf<Int?>(null) }
val (itemToUnlockType, setItemToUnlockType) = rememberSaveable { mutableStateOf<Screen?>(null) }
var showChooseLockMethodDialog by rememberSaveable { mutableStateOf(false) }
var itemToLockId: Int? by rememberSaveable { mutableStateOf(null) }
var itemToLockType: LockableItemType? by rememberSaveable { mutableStateOf(null) }
val (showChooseLockMethodDialog, setShowChooseLockMethodDialog) = rememberSaveable { mutableStateOf(false) }
val (itemToLockId, setItemToLockId) = rememberSaveable { mutableStateOf<Int?>(null) }
val (itemToLockType, setItemToLockType) = rememberSaveable { mutableStateOf<LockableItemType?>(null) }
val onUnlockItem: (Int, Screen, Int) -> Unit = { id, type, protectionType ->
when (protectionType) {
1 -> { // Password protected
itemToUnlockId = id
itemToUnlockType = type
showUnlockPasswordDialog = true
setItemToUnlockId(id)
setItemToUnlockType(type)
setShowUnlockPasswordDialog(true)
}
2 -> { // Biometric protected
biometricAuthenticator.promptBiometricAuth(
@@ -684,25 +684,25 @@ fun AppShell(
fragmentActivity = context.findActivity() as FragmentActivity,
onSuccess = { _ ->
when (type) {
is Screen.ShoppingListDetail -> selectedListId = id
is Screen.NoteDetail -> selectedNoteId = id
is Screen.RecipeDetail -> selectedRecipeId = id
is Screen.ShoppingListDetail -> setSelectedListId(id)
is Screen.NoteDetail -> setSelectedNoteId(id)
is Screen.RecipeDetail -> setSelectedRecipeId(id)
else -> {}
}
itemWasInitiallyLocked = true
currentScreen = type
setItemWasInitiallyLocked(true)
setCurrentScreen(type)
},
onFailed = {
// Biometric failed, offer password as fallback if desired
itemToUnlockId = id
itemToUnlockType = type
showUnlockPasswordDialog = true
setItemToUnlockId(id)
setItemToUnlockType(type)
setShowUnlockPasswordDialog(true)
},
onError = { _, _ ->
// Biometric error, offer password as fallback if desired
itemToUnlockId = id
itemToUnlockType = type
showUnlockPasswordDialog = true
setItemToUnlockId(id)
setItemToUnlockType(type)
setShowUnlockPasswordDialog(true)
}
)
}
@@ -716,7 +716,7 @@ fun AppShell(
)
.collectAsState(initial = null)
var shoppingListsTitle by remember {
val (shoppingListsTitle, setShoppingListsTitle) = remember {
mutableStateOf(
sharedPrefs.getString(
"shopping_lists_title",
@@ -724,9 +724,9 @@ fun AppShell(
) ?: context.getString(R.string.menu_shopping_lists)
)
}
var showRenameDialog by rememberSaveable { mutableStateOf(false) }
val (showRenameDialog, setShowRenameDialog) = rememberSaveable { mutableStateOf(false) }
var recipesTitle by remember {
val (recipesTitle, setRecipesTitle) = remember {
mutableStateOf(
sharedPrefs.getString(
"recipes_title",
@@ -734,9 +734,9 @@ fun AppShell(
) ?: context.getString(R.string.menu_recipes)
)
}
var showRecipeRenameDialog by rememberSaveable { mutableStateOf(false) }
val (showRecipeRenameDialog, setShowRecipeRenameDialog) = rememberSaveable { mutableStateOf(false) }
var startScreen by remember {
val (startScreen, setStartScreen) = remember {
mutableStateOf(
sharedPrefs.getString(
"default_start_screen",
@@ -764,7 +764,7 @@ fun AppShell(
shoppingListsViewModel.importListFromTxt(listName, content)
}
}
showListDialog = false // Close dialog on success
setShowListDialog(false) // Close dialog on success
}
}
)
@@ -942,19 +942,19 @@ fun AppShell(
AppDrawer(
drawerState = drawerState,
currentScreen = currentScreen,
onScreenChange = { currentScreen = it },
onScreenChange = { setCurrentScreen(it) },
shoppingListsViewModel = shoppingListsViewModel,
notesViewModel = notesViewModel,
shoppingListsTitle = shoppingListsTitle,
recipesTitle = recipesTitle,
onShowRenameDialog = { showRenameDialog = true },
onShowRecipeRenameDialog = { showRecipeRenameDialog = true },
onShowJsonDialog = { showJsonDialog = true },
onShowRenameDialog = { setShowRenameDialog(true) },
onShowRecipeRenameDialog = { setShowRecipeRenameDialog(true) },
onShowJsonDialog = { setShowJsonDialog(true) },
syncFolderUriString = syncFolderUriString,
onShowDeleteConfirmationDialog = { showDeleteConfirmationDialog = true },
onShowDeleteConfirmationDialog = { setShowDeleteConfirmationDialog(true) },
onLaunchSyncFolderChooser = { syncFolderLauncher.launch(null) },
onSetSelectedListId = { selectedListId = it },
onSetSelectedNoteId = { selectedNoteId = it }
onSetSelectedListId = { setSelectedListId(it) },
onSetSelectedNoteId = { setSelectedNoteId(it) }
)
}
) {
@@ -965,31 +965,31 @@ fun AppShell(
topBar = {
AppTopBar(
currentScreen = currentScreen,
onScreenChange = { currentScreen = it },
onScreenChange = { setCurrentScreen(it) },
drawerState = drawerState,
shoppingListsViewModel = shoppingListsViewModel,
notesViewModel = notesViewModel,
recipesViewModel = recipesViewModel,
shoppingListsTitle = shoppingListsTitle,
recipesTitle = recipesTitle,
onShowListDialog = { showListDialog = true },
onShowNoteDialog = { showNoteDialog = true },
onShowRecipeDialog = { showRecipeDialog = true },
onShowListDialog = { setShowListDialog(true) },
onShowNoteDialog = { setShowNoteDialog(true) },
onShowRecipeDialog = { setShowRecipeDialog(true) },
selectedListId = selectedListId,
onSetSelectedListId = { selectedListId = it },
onSetSelectedNoteId = { selectedNoteId = it },
onSetSelectedRecipeId = { selectedRecipeId = it },
onSetSelectedListId = { setSelectedListId(it) },
onSetSelectedNoteId = { setSelectedNoteId(it) },
onSetSelectedRecipeId = { setSelectedRecipeId(it) },
exportLauncher = exportLauncher,
noteExportLauncher = noteExportLauncher,
recipeExportLauncher = recipeExportLauncher,
hasEncryptionPassword = hasEncryptionPassword,
onShowSetPasswordDialog = { showSetPasswordDialog = true },
onShowSetRecipePasswordDialog = { showSetRecipePasswordDialog = true },
onShowSetListPasswordDialog = { showSetListPasswordDialog = true },
onShowSetPasswordDialog = { setShowSetPasswordDialog(true) },
onShowSetRecipePasswordDialog = { setShowSetRecipePasswordDialog(true) },
onShowSetListPasswordDialog = { setShowSetListPasswordDialog(true) },
onShowChooseLockMethodDialog = { type, id ->
itemToLockType = type
itemToLockId = id
showChooseLockMethodDialog = true
setItemToLockType(type)
setItemToLockId(id)
setShowChooseLockMethodDialog(true)
}
)
},
@@ -1073,19 +1073,19 @@ fun AppShell(
AppContent(
innerPadding = innerPadding,
currentScreen = currentScreen,
onScreenChange = { currentScreen = it },
onScreenChange = { setCurrentScreen(it) },
notesViewModel = notesViewModel,
shoppingListsViewModel = shoppingListsViewModel,
recipesViewModel = recipesViewModel,
selectedListId = selectedListId,
onSetSelectedListId = { selectedListId = it },
onShowListDialog = { showListDialog = true },
onSetSelectedListId = { setSelectedListId(it) },
onShowListDialog = { setShowListDialog(true) },
selectedNoteId = selectedNoteId,
onSetSelectedNoteId = { selectedNoteId = it },
onShowNoteDialog = { showNoteDialog = true },
onSetSelectedNoteId = { setSelectedNoteId(it) },
onShowNoteDialog = { setShowNoteDialog(true) },
selectedRecipeId = selectedRecipeId,
onSetSelectedRecipeId = { selectedRecipeId = it },
onShowRecipeDialog = { showRecipeDialog = true },
onSetSelectedRecipeId = { setSelectedRecipeId(it) },
onShowRecipeDialog = { setShowRecipeDialog(true) },
itemWasInitiallyLocked = itemWasInitiallyLocked,
onUnlockItem = onUnlockItem,
shoppingListsTitle = shoppingListsTitle,
@@ -1102,16 +1102,16 @@ fun AppShell(
sharedPrefs = sharedPrefs,
onResetShoppingListsTitle = {
sharedPrefs.edit { remove("shopping_lists_title") }
shoppingListsTitle = context.getString(R.string.menu_shopping_lists)
setShoppingListsTitle(context.getString(R.string.menu_shopping_lists))
},
onResetRecipesTitle = {
sharedPrefs.edit { remove("recipes_title") }
recipesTitle = context.getString(R.string.menu_recipes)
setRecipesTitle(context.getString(R.string.menu_recipes))
},
startScreen = startScreen,
onStartScreenChange = {
sharedPrefs.edit { putString("default_start_screen", it) }
startScreen = it
setStartScreen(it)
},
canUseBiometrics = canUseBiometrics,
webAppIntegrationViewModel = webAppIntegrationViewModel,
@@ -1125,7 +1125,7 @@ fun AppShell(
shoppingListsViewModel = shoppingListsViewModel,
recipesViewModel = recipesViewModel,
showListDialog = showListDialog,
onShowListDialogChange = { showListDialog = it },
onShowListDialogChange = { setShowListDialog(it) },
listDetails = listDetails,
onListDetailsChange = { shoppingListsViewModel.updateListDetails(it) },
onSaveList = { scope.launch { shoppingListsViewModel.saveList() } },
@@ -1133,71 +1133,71 @@ fun AppShell(
onSetListProtection = { password ->
selectedListId?.let { listId ->
shoppingListsViewModel.setProtection(listId, password)
currentScreen = Screen.ShoppingLists
setCurrentScreen(Screen.ShoppingLists)
}
selectedListId = null
showSetListPasswordDialog = false
setSelectedListId(null)
setShowSetListPasswordDialog(false)
},
onSetListProtectionBiometric = { id ->
shoppingListsViewModel.setProtectionBiometric(id)
currentScreen = Screen.ShoppingLists
setCurrentScreen(Screen.ShoppingLists)
},
txtImportLauncher = txtImportLauncher,
showNoteDialog = showNoteDialog,
onShowNoteDialogChange = { showNoteDialog = it },
onShowNoteDialogChange = { setShowNoteDialog(it) },
noteDetails = noteDetails,
onNoteDetailsChange = { notesViewModel.updateNoteDetails(it) },
onSaveNote = { scope.launch { notesViewModel.saveNote() } },
onResetNoteDetails = { notesViewModel.resetNoteDetails() },
onSetNoteProtectionBiometric = { id ->
notesViewModel.setProtectionBiometric(id)
currentScreen = Screen.Notes
setCurrentScreen(Screen.Notes)
},
noteImportLauncher = noteImportLauncher,
showRecipeDialog = showRecipeDialog,
onShowRecipeDialogChange = { showRecipeDialog = it },
onShowRecipeDialogChange = { setShowRecipeDialog(it) },
recipeDetails = recipeDetails,
onRecipeDetailsChange = { recipesViewModel.updateRecipeDetails(it) },
onSaveRecipe = { scope.launch { recipesViewModel.saveRecipe() } },
onResetRecipeDetails = { recipesViewModel.resetRecipeDetails() },
onSetRecipeProtectionBiometric = { id ->
recipesViewModel.setProtectionBiometric(id)
currentScreen = Screen.Recipes
setCurrentScreen(Screen.Recipes)
},
recipeImportLauncher = recipeImportLauncher,
recipesTitle = recipesTitle,
showJsonDialog = showJsonDialog,
onShowJsonDialogChange = { showJsonDialog = it },
onShowJsonDialogChange = { setShowJsonDialog(it) },
shoppingListsTitle = shoppingListsTitle,
fileEncryptor = fileEncryptor,
keyManager = keyManager,
biometricAuthenticator = biometricAuthenticator,
showSetPasswordDialog = showSetPasswordDialog,
onShowSetPasswordDialogChange = { showSetPasswordDialog = it },
onShowSetPasswordDialogChange = { setShowSetPasswordDialog(it) },
onSetNotePassword = { password ->
notesViewModel.setProtectionPassword(password)
selectedNoteId = null
showSetPasswordDialog = false
currentScreen = Screen.Notes
setSelectedNoteId(null)
setShowSetPasswordDialog(false)
setCurrentScreen(Screen.Notes)
},
showSetRecipePasswordDialog = showSetRecipePasswordDialog,
onShowSetRecipePasswordDialogChange = { showSetRecipePasswordDialog = it },
onShowSetRecipePasswordDialogChange = { setShowSetRecipePasswordDialog(it) },
onSetRecipePassword = { password ->
recipesViewModel.setProtectionPassword(password)
selectedRecipeId = null
showSetRecipePasswordDialog = false
currentScreen = Screen.Recipes
setSelectedRecipeId(null)
setShowSetRecipePasswordDialog(false)
setCurrentScreen(Screen.Recipes)
},
showSetListPasswordDialog = showSetListPasswordDialog,
onShowSetListPasswordDialogChange = { showSetListPasswordDialog = it },
onShowSetListPasswordDialogChange = { setShowSetListPasswordDialog(it) },
showUnlockPasswordDialog = showUnlockPasswordDialog,
onShowUnlockPasswordDialogChange = {
showUnlockPasswordDialog = it
setShowUnlockPasswordDialog(it)
if (!it) {
unlockErrorMessage = null
itemToUnlockId = null
itemToUnlockType = null
setUnlockErrorMessage(null)
setItemToUnlockId(null)
setItemToUnlockType(null)
}
},
onUnlock = { password ->
@@ -1209,44 +1209,44 @@ fun AppShell(
val note = notesViewModel.uiState.value.noteList.find { it.id == itemToUnlockId }
if (note != null && note.protectionHash == hashedPassword) {
isPasswordCorrect = true
selectedNoteId = itemToUnlockId
setSelectedNoteId(itemToUnlockId)
}
}
Screen.RecipeDetail -> {
val recipe = recipesViewModel.uiState.value.recipeList.find { it.id == itemToUnlockId }
if (recipe != null && recipe.protectionHash == hashedPassword) {
isPasswordCorrect = true
selectedRecipeId = itemToUnlockId
setSelectedRecipeId(itemToUnlockId)
}
}
Screen.ShoppingListDetail -> {
val listWithItems = shoppingListsViewModel.uiState.value.shoppingLists.find { it.shoppingList.id == itemToUnlockId }
if (listWithItems != null && listWithItems.shoppingList.protectionHash == hashedPassword) {
isPasswordCorrect = true
selectedListId = itemToUnlockId
setSelectedListId(itemToUnlockId)
}
}
else -> {} // Should not happen
}
if (isPasswordCorrect) {
unlockErrorMessage = null
itemWasInitiallyLocked = true // Mark as initially locked for content decryption
currentScreen = itemToUnlockType!!
itemToUnlockId = null
itemToUnlockType = null
showUnlockPasswordDialog = false
setUnlockErrorMessage(null)
setItemWasInitiallyLocked(true) // Mark as initially locked for content decryption
setCurrentScreen(itemToUnlockType!!)
setItemToUnlockId(null)
setItemToUnlockType(null)
setShowUnlockPasswordDialog(false)
} else {
unlockErrorMessage = context.getString(R.string.incorrect_password)
setUnlockErrorMessage(context.getString(R.string.incorrect_password))
}
},
unlockErrorMessage = unlockErrorMessage,
showPasswordDialog = showPasswordDialog,
onShowPasswordDialogChange = { showPasswordDialog = it },
onHasEncryptionPasswordChange = { hasEncryptionPassword = it },
onShowPasswordDialogChange = { setShowPasswordDialog(it) },
onHasEncryptionPasswordChange = { setHasEncryptionPassword(it) },
sharedPrefs = sharedPrefs,
showDeleteConfirmationDialog = showDeleteConfirmationDialog,
onShowDeleteConfirmationDialogChange = { showDeleteConfirmationDialog = it },
onShowDeleteConfirmationDialogChange = { setShowDeleteConfirmationDialog(it) },
onDeleteSyncFolder = {
val syncFolderUriString = sharedPrefs.getString("sync_folder_uri", null)
if (syncFolderUriString != null) {
@@ -1259,19 +1259,19 @@ fun AppShell(
}
},
showRenameDialog = showRenameDialog,
onShowRenameDialogChange = { showRenameDialog = it },
onShowRenameDialogChange = { setShowRenameDialog(it) },
onRenameShoppingListsTitle = { newTitle ->
sharedPrefs.edit { putString("shopping_lists_title", newTitle) }
shoppingListsTitle = newTitle
setShoppingListsTitle(newTitle)
},
showRecipeRenameDialog = showRecipeRenameDialog,
onShowRecipeRenameDialogChange = { showRecipeRenameDialog = it },
onShowRecipeRenameDialogChange = { setShowRecipeRenameDialog(it) },
onRenameRecipesTitle = { newTitle ->
sharedPrefs.edit { putString("recipes_title", newTitle) }
recipesTitle = newTitle
setRecipesTitle(newTitle)
},
showChooseLockMethodDialog = showChooseLockMethodDialog,
onShowChooseLockMethodDialogChange = { showChooseLockMethodDialog = it },
onShowChooseLockMethodDialogChange = { setShowChooseLockMethodDialog(it) },
onConfirmLockMethod = { lockMethod ->
itemToLockType?.let { type ->
itemToLockId?.let { id ->
@@ -1279,63 +1279,63 @@ fun AppShell(
LockableItemType.SHOPPING_LIST -> {
when (lockMethod) {
LockMethod.PASSWORD -> {
selectedListId = id
showSetListPasswordDialog = true
setSelectedListId(id)
setShowSetListPasswordDialog(true)
}
LockMethod.BIOMETRIC -> {
shoppingListsViewModel.setProtectionBiometric(id)
currentScreen = Screen.ShoppingLists
setCurrentScreen(Screen.ShoppingLists)
}
}
}
LockableItemType.NOTE -> {
when (lockMethod) {
LockMethod.PASSWORD -> {
selectedNoteId = id
showSetPasswordDialog = true
setSelectedNoteId(id)
setShowSetPasswordDialog(true)
}
LockMethod.BIOMETRIC -> {
notesViewModel.setProtectionBiometric(id)
currentScreen = Screen.Notes
setCurrentScreen(Screen.Notes)
}
}
}
LockableItemType.RECIPE -> {
when (lockMethod) {
LockMethod.PASSWORD -> {
selectedRecipeId = id
showSetRecipePasswordDialog = true
setSelectedRecipeId(id)
setShowSetRecipePasswordDialog(true)
}
LockMethod.BIOMETRIC -> {
recipesViewModel.setProtectionBiometric(id)
currentScreen = Screen.Recipes
setCurrentScreen(Screen.Recipes)
}
}
}
}
}
}
showChooseLockMethodDialog = false
itemToLockId = null
itemToLockType = null
setShowChooseLockMethodDialog(false)
setItemToLockId(null)
setItemToLockType(null)
},
canUseBiometrics = canUseBiometrics,
itemToLockType = itemToLockType,
showStartupPasswordDialog = showStartupPasswordDialog,
onShowStartupPasswordDialogChange = { showStartupPasswordDialog = it },
onShowStartupPasswordDialogChange = { setShowStartupPasswordDialog(it) },
onUnlockEncryption = { password ->
scope.launch {
try {
val decryptionCipher = keyManager.getDecryptionCipher()
if (decryptionCipher == null) {
startupUnlockErrorMessage = context.getString(R.string.unlock_failed)
setStartupUnlockErrorMessage(context.getString(R.string.unlock_failed))
return@launch
}
val sharedPrefs = context.getSharedPreferences("encryption_prefs", Context.MODE_PRIVATE)
val encryptedDerivedKeyString = sharedPrefs.getString("encrypted_derived_key", null)
if (encryptedDerivedKeyString == null) {
startupUnlockErrorMessage = context.getString(R.string.unlock_failed)
setStartupUnlockErrorMessage(context.getString(R.string.unlock_failed))
return@launch
}
val encryptedDerivedKey = Base64.decode(encryptedDerivedKeyString, Base64.DEFAULT)
@@ -1348,19 +1348,19 @@ fun AppShell(
Log.d("AppShell_Unlock", "Current PBE Key: ${currentPbeKey.encoded.joinToString()}")
if (decryptedStoredKeyBytes.contentEquals(currentPbeKey.encoded)) {
secretKey = SecretKeySpec(decryptedStoredKeyBytes, "AES")
isDecryptionAttempted = true
showStartupPasswordDialog = false
startupUnlockErrorMessage = null
setSecretKey(SecretKeySpec(decryptedStoredKeyBytes, "AES"))
setIsDecryptionAttempted(true)
setShowStartupPasswordDialog(false)
setStartupUnlockErrorMessage(null)
} else {
startupUnlockErrorMessage = context.getString(R.string.incorrect_password)
setStartupUnlockErrorMessage(context.getString(R.string.incorrect_password))
}
} catch (e: Exception) {
Log.e("AppShell", "Failed to unlock encryption with password", e)
startupUnlockErrorMessage = context.getString(R.string.unlock_failed)
setStartupUnlockErrorMessage(context.getString(R.string.unlock_failed))
}
}
},
startupUnlockErrorMessage = startupUnlockErrorMessage
)
}
}