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:
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user