Feat: Implement new lock methods (Pattern, PIN)

This commit introduces new lock methods for notes, recipes, and shopping lists, allowing users to choose between Password, Biometric, Pattern, and PIN for item protection.

Changes include:
- Expanded `LockMethod` enum with `PATTERN` and `PIN`.
- Updated `ChooseLockMethodDialog` to display new lock options.
- Added `lockMethod` field to `Note`, `Recipe`, and `ShoppingList` data classes.
- Implemented database migration (version 9 to 10) to support the new `lockMethod` field.
- Modified `AppShell.kt` to correctly handle the selection of new lock methods.
- Created placeholder dialogs (`SetPatternDialog`, `SetPinDialog`) and ViewModel functions for future implementation of pattern and PIN entry UIs.
- Updated `AppTopBar.kt` to show the "Lock" option in the dropdown menu for all item types, triggering the `ChooseLockMethodDialog`.
This commit is contained in:
2025-11-03 14:29:00 +01:00
parent de1d02f31b
commit 06d2b21a11
13 changed files with 240 additions and 32 deletions

View File

@@ -10,7 +10,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
@Database(
entities = [Note::class, ShoppingList::class, ShoppingListItem::class, Recipe::class],
version = 9, // Incremented version
version = 10, // Incremented version
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
@@ -32,7 +32,7 @@ abstract class AppDatabase : RoomDatabase() {
AppDatabase::class.java,
"noteshop_database"
)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10)
.build()
INSTANCE = instance
// return instance
@@ -93,5 +93,13 @@ abstract class AppDatabase : RoomDatabase() {
db.execSQL("ALTER TABLE shopping_lists ADD COLUMN protectionType INTEGER NOT NULL DEFAULT 0")
}
}
private val MIGRATION_9_10 = object : Migration(9, 10) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE notes ADD COLUMN lockMethod INTEGER NOT NULL DEFAULT 0")
db.execSQL("ALTER TABLE recipes ADD COLUMN lockMethod INTEGER NOT NULL DEFAULT 0")
db.execSQL("ALTER TABLE shopping_lists ADD COLUMN lockMethod INTEGER NOT NULL DEFAULT 0")
}
}
}
}

View File

@@ -13,5 +13,6 @@ data class Note(
val content: String,
val displayOrder: Int = 0,
val protectionHash: String = "",
val protectionType: Int = 0
val protectionType: Int = 0,
val lockMethod: Int = 0
)

View File

@@ -13,5 +13,6 @@ data class Recipe(
val content: String,
val displayOrder: Int = 0,
val protectionHash: String = "",
val protectionType: Int = 0
val protectionType: Int = 0,
val lockMethod: Int = 0
)

View File

@@ -13,5 +13,6 @@ data class ShoppingList(
val displayOrder: Int = 0,
val protectionHash: String = "",
val protectionType: Int = 0,
val lockMethod: Int = 0,
val encryptedItems: String? = null
)

View File

@@ -642,6 +642,8 @@ fun AppShell(
var showSetPasswordDialog by rememberSaveable { mutableStateOf(false) }
var showSetRecipePasswordDialog by rememberSaveable { mutableStateOf(false) }
var showSetListPasswordDialog by rememberSaveable { mutableStateOf(false) }
var showSetPatternDialog by rememberSaveable { mutableStateOf(false) }
var showSetPinDialog by rememberSaveable { mutableStateOf(false) }
var showUnlockPasswordDialog by rememberSaveable { mutableStateOf(false) }
var unlockErrorMessage by rememberSaveable { mutableStateOf<String?>(null) }
var itemToUnlockId by rememberSaveable { mutableStateOf<Int?>(null) }
@@ -1253,31 +1255,60 @@ fun AppShell(
itemToLockId?.let { id ->
when (type) {
LockableItemType.SHOPPING_LIST -> {
if (lockMethod == LockMethod.PASSWORD) {
selectedListId = id
showSetListPasswordDialog = true
} else {
// Implement biometric lock for shopping list
// For now, we'll just set a placeholder protection hash
shoppingListsViewModel.setProtectionBiometric(id)
when (lockMethod) {
LockMethod.PASSWORD -> {
selectedListId = id
showSetListPasswordDialog = true
}
LockMethod.BIOMETRIC -> {
shoppingListsViewModel.setProtectionBiometric(id)
}
LockMethod.PATTERN -> {
selectedListId = id
showSetPatternDialog = true
}
LockMethod.PIN -> {
selectedListId = id
showSetPinDialog = true
}
}
}
LockableItemType.NOTE -> {
if (lockMethod == LockMethod.PASSWORD) {
selectedNoteId = id
showSetPasswordDialog = true
} else {
// Implement biometric lock for note
notesViewModel.setProtectionBiometric(id)
when (lockMethod) {
LockMethod.PASSWORD -> {
selectedNoteId = id
showSetPasswordDialog = true
}
LockMethod.BIOMETRIC -> {
notesViewModel.setProtectionBiometric(id)
}
LockMethod.PATTERN -> {
selectedNoteId = id
showSetPatternDialog = true
}
LockMethod.PIN -> {
selectedNoteId = id
showSetPinDialog = true
}
}
}
LockableItemType.RECIPE -> {
if (lockMethod == LockMethod.PASSWORD) {
selectedRecipeId = id
showSetRecipePasswordDialog = true
} else {
// Implement biometric lock for recipe
recipesViewModel.setProtectionBiometric(id)
when (lockMethod) {
LockMethod.PASSWORD -> {
selectedRecipeId = id
showSetRecipePasswordDialog = true
}
LockMethod.BIOMETRIC -> {
recipesViewModel.setProtectionBiometric(id)
}
LockMethod.PATTERN -> {
selectedRecipeId = id
showSetPatternDialog = true
}
LockMethod.PIN -> {
selectedRecipeId = id
showSetPinDialog = true
}
}
}
}
@@ -1287,5 +1318,11 @@ fun AppShell(
itemToLockId = null
itemToLockType = null
},
canUseBiometrics = canUseBiometrics)
canUseBiometrics = canUseBiometrics,
itemToLockType = itemToLockType,
showSetPatternDialog = showSetPatternDialog,
onShowSetPatternDialogChange = { showSetPatternDialog = it },
showSetPinDialog = showSetPinDialog,
onShowSetPinDialogChange = { showSetPinDialog = it }
)
}

View File

@@ -22,7 +22,9 @@ import de.lxtools.noteshop.R
enum class LockMethod {
PASSWORD,
BIOMETRIC
BIOMETRIC,
PATTERN,
PIN
}
@Composable
@@ -65,6 +67,58 @@ fun ChooseLockMethodDialog(
)
Text(text = stringResource(R.string.biometric_lock), modifier = Modifier.padding(start = 8.dp))
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { selectedMethod = LockMethod.PATTERN }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedMethod == LockMethod.PATTERN,
onClick = { selectedMethod = LockMethod.PATTERN }
)
Text(text = stringResource(R.string.pattern), modifier = Modifier.padding(start = 8.dp))
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { selectedMethod = LockMethod.PIN }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedMethod == LockMethod.PIN,
onClick = { selectedMethod = LockMethod.PIN }
)
Text(text = stringResource(R.string.pin), modifier = Modifier.padding(start = 8.dp))
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { selectedMethod = LockMethod.PATTERN }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedMethod == LockMethod.PATTERN,
onClick = { selectedMethod = LockMethod.PATTERN }
)
Text(text = stringResource(R.string.pattern), modifier = Modifier.padding(start = 8.dp))
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { selectedMethod = LockMethod.PIN }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedMethod == LockMethod.PIN,
onClick = { selectedMethod = LockMethod.PIN }
)
Text(text = stringResource(R.string.pin), modifier = Modifier.padding(start = 8.dp))
}
}
},
confirmButton = {

View File

@@ -0,0 +1,44 @@
package de.lxtools.noteshop.ui
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@Composable
fun SetPatternDialog(onDismiss: () -> Unit, onSetPattern: (String) -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(text = "Set Pattern") },
text = { Text(text = "Pattern entry not implemented yet.") },
confirmButton = {
TextButton(onClick = { onSetPattern("") }) {
Text(text = "OK")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(text = "Cancel")
}
}
)
}
@Composable
fun SetPinDialog(onDismiss: () -> Unit, onSetPin: (String) -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(text = "Set PIN") },
text = { Text(text = "PIN entry not implemented yet.") },
confirmButton = {
TextButton(onClick = { onSetPin("") }) {
Text(text = "OK")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(text = "Cancel")
}
}
)
}

View File

@@ -27,6 +27,7 @@ import de.lxtools.noteshop.ui.recipes.RecipesViewModel
import de.lxtools.noteshop.ui.shopping.ShoppingListDetails
import de.lxtools.noteshop.ui.shopping.ShoppingListInputDialog
import de.lxtools.noteshop.ui.ChooseLockMethodDialog
import de.lxtools.noteshop.ui.LockableItemType
import de.lxtools.noteshop.ui.LockMethod
import de.lxtools.noteshop.ui.shopping.ShoppingListsViewModel
import kotlinx.coroutines.launch
@@ -96,7 +97,12 @@ fun AppDialogs(
showChooseLockMethodDialog: Boolean,
onShowChooseLockMethodDialogChange: (Boolean) -> Unit,
onConfirmLockMethod: (LockMethod) -> Unit,
canUseBiometrics: Boolean
canUseBiometrics: Boolean,
itemToLockType: de.lxtools.noteshop.ui.LockableItemType?,
showSetPatternDialog: Boolean,
onShowSetPatternDialogChange: (Boolean) -> Unit,
showSetPinDialog: Boolean,
onShowSetPinDialogChange: (Boolean) -> Unit
) {
val scope = rememberCoroutineScope()
val dynamicRecipeStrings = getDynamicRecipeStrings(recipesTitle)
@@ -203,6 +209,36 @@ fun AppDialogs(
)
}
if (showSetPatternDialog) {
de.lxtools.noteshop.ui.SetPatternDialog(
onDismiss = { onShowSetPatternDialogChange(false) },
onSetPattern = { pattern ->
when (itemToLockType) {
LockableItemType.NOTE -> notesViewModel.setProtectionPattern(pattern)
LockableItemType.RECIPE -> recipesViewModel.setProtectionPattern(pattern)
LockableItemType.SHOPPING_LIST -> shoppingListsViewModel.setProtectionPattern(pattern)
else -> {}
}
onShowSetPatternDialogChange(false)
}
)
}
if (showSetPinDialog) {
de.lxtools.noteshop.ui.SetPinDialog(
onDismiss = { onShowSetPinDialogChange(false) },
onSetPin = { pin ->
when (itemToLockType) {
LockableItemType.NOTE -> notesViewModel.setProtectionPin(pin)
LockableItemType.RECIPE -> recipesViewModel.setProtectionPin(pin)
LockableItemType.SHOPPING_LIST -> shoppingListsViewModel.setProtectionPin(pin)
else -> {}
}
onShowSetPinDialogChange(false)
}
)
}
if (showUnlockPasswordDialog) {
UnlockPasswordDialog(
onDismiss = {

View File

@@ -295,11 +295,11 @@ fun AppTopBar(
)
shoppingListWithItems?.let { listWithItems ->
DropdownMenuItem(
text = { Text(stringResource(R.string.set_password)) },
text = { Text(stringResource(R.string.lock)) },
enabled = hasEncryptionPassword,
onClick = {
showMenu = false
onShowSetListPasswordDialog()
onShowChooseLockMethodDialog(LockableItemType.SHOPPING_LIST, listWithItems.shoppingList.id)
}
)
}
@@ -371,11 +371,11 @@ fun AppTopBar(
)
noteDetails.let {
DropdownMenuItem(
text = { Text(stringResource(R.string.set_password)) },
text = { Text(stringResource(R.string.lock)) },
enabled = hasEncryptionPassword,
onClick = {
showMenu = false
onShowSetPasswordDialog()
onShowChooseLockMethodDialog(LockableItemType.NOTE, it.id)
}
)
}
@@ -449,11 +449,11 @@ fun AppTopBar(
)
recipeDetails.let {
DropdownMenuItem(
text = { Text(stringResource(R.string.set_password)) },
text = { Text(stringResource(R.string.lock)) },
enabled = hasEncryptionPassword,
onClick = {
showMenu = false
onShowSetRecipePasswordDialog()
onShowChooseLockMethodDialog(LockableItemType.RECIPE, it.id)
}
)
}

View File

@@ -136,6 +136,14 @@ class NotesViewModel(private val noteshopRepository: NoteshopRepository) : ViewM
}
}
fun setProtectionPattern(pattern: String) {
Log.d("NotesViewModel", "setProtectionPattern called with pattern: $pattern")
}
fun setProtectionPin(pin: String) {
Log.d("NotesViewModel", "setProtectionPin called with pin: $pin")
}
fun resetNoteDetails() {
_noteDetails.value = NoteDetails()
}

View File

@@ -134,6 +134,14 @@ class RecipesViewModel(private val noteshopRepository: NoteshopRepository) : Vie
}
}
fun setProtectionPattern(pattern: String) {
Log.d("RecipesViewModel", "setProtectionPattern called with pattern: $pattern")
}
fun setProtectionPin(pin: String) {
Log.d("RecipesViewModel", "setProtectionPin called with pin: $pin")
}
fun resetRecipeDetails() {
_recipeDetails.value = RecipeDetails()
}

View File

@@ -188,6 +188,14 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository,
}
}
fun setProtectionPattern(pattern: String) {
Log.d("ShoppingListsViewModel", "setProtectionPattern called with pattern: $pattern")
}
fun setProtectionPin(pin: String) {
Log.d("ShoppingListsViewModel", "setProtectionPin called with pin: $pin")
}
suspend fun deleteList(list: ShoppingList) {
noteshopRepository.deleteShoppingList(list)
}

View File

@@ -329,4 +329,6 @@
<string name="remove_lock">Remove lock</string>
<string name="choose_lock_method">Choose lock method</string>
<string name="ok">OK</string>
<string name="pattern">Pattern</string>
<string name="pin">PIN</string>
</resources>