From a28d77ba32ef18df401be75ed83fbd52bc6aa059 Mon Sep 17 00:00:00 2001 From: Richard Macklin Date: Sat, 26 Jul 2025 17:42:39 -0700 Subject: [PATCH] feat: Remember the last read-only state of each database The app has supported a global setting for opening (all) databases in read-only mode. But that's not particularly flexible for the use case where you have one database that should be read-only and one that should be read-write. Previously, to handle this use case you could open one database in read-only mode, but the next time you attempted to open the same database, it would "forget" that, so you would have to toggle it to read-only mode again manually. This commit changes that behavior so that if you toggle a database to read-only mode, it'll be remembered the next time you open the database. (You can still toggle it back to read-write if you change your mind, and that, too, will be remembered the next time you open the database.) --- .../3.json | 96 +++++++++++++++++++ .../activities/MainCredentialActivity.kt | 14 +++ .../keepass/app/database/AppDatabase.kt | 5 +- .../app/database/FileDatabaseHistoryAction.kt | 6 ++ .../app/database/FileDatabaseHistoryEntity.kt | 3 + .../kunzisoft/keepass/model/DatabaseFile.kt | 1 + 6 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 app/schemas/com.kunzisoft.keepass.app.database.AppDatabase/3.json diff --git a/app/schemas/com.kunzisoft.keepass.app.database.AppDatabase/3.json b/app/schemas/com.kunzisoft.keepass.app.database.AppDatabase/3.json new file mode 100644 index 000000000..f9b3b72ef --- /dev/null +++ b/app/schemas/com.kunzisoft.keepass.app.database.AppDatabase/3.json @@ -0,0 +1,96 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "a20aec7cf09664b1102ec659fa51160a", + "entities": [ + { + "tableName": "file_database_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `database_alias` TEXT NOT NULL, `keyfile_uri` TEXT, `hardware_key` TEXT, `read_only` INTEGER, `updated` INTEGER NOT NULL, PRIMARY KEY(`database_uri`))", + "fields": [ + { + "fieldPath": "databaseUri", + "columnName": "database_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "databaseAlias", + "columnName": "database_alias", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyFileUri", + "columnName": "keyfile_uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hardwareKey", + "columnName": "hardware_key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readOnly", + "columnName": "read_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updated", + "columnName": "updated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "database_uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "cipher_database", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `encrypted_value` TEXT NOT NULL, `specs_parameters` TEXT NOT NULL, PRIMARY KEY(`database_uri`))", + "fields": [ + { + "fieldPath": "databaseUri", + "columnName": "database_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedValue", + "columnName": "encrypted_value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "specParameters", + "columnName": "specs_parameters", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "database_uri" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a20aec7cf09664b1102ec659fa51160a')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt index de0991f5e..8077c9b19 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -52,6 +52,7 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity +import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.biometric.DeviceUnlockFragment @@ -203,6 +204,13 @@ class MainCredentialActivity : DatabaseModeActivity() { } mForceReadOnly = databaseFileNotExists + // Restore read-only state from database file if not forced + if (!mForceReadOnly) { + databaseFile?.readOnly?.let { savedReadOnlyState -> + mReadOnly = savedReadOnlyState + } + } + invalidateOptionsMenu() // Post init uri with KeyFile only if needed @@ -702,6 +710,12 @@ class MainCredentialActivity : DatabaseModeActivity() { R.id.menu_open_file_read_mode_key -> { mReadOnly = !mReadOnly changeOpenFileReadIcon(item) + // Save the read-only state to database + mDatabaseFileUri?.let { databaseUri -> + FileDatabaseHistoryAction.getInstance(applicationContext).addOrUpdateDatabaseFile( + DatabaseFile(databaseUri = databaseUri, readOnly = mReadOnly) + ) + } } else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item) } diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/AppDatabase.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/AppDatabase.kt index 16d1a190c..117e8b67a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/AppDatabase.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/AppDatabase.kt @@ -26,10 +26,11 @@ import android.content.Context import androidx.room.AutoMigration @Database( - version = 2, + version = 3, entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class], autoMigrations = [ - AutoMigration (from = 1, to = 2) + AutoMigration (from = 1, to = 2), + AutoMigration (from = 2, to = 3) ] ) abstract class AppDatabase : RoomDatabase() { diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryAction.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryAction.kt index 1818f8f6e..6f649f362 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryAction.kt @@ -49,6 +49,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) { databaseUri, fileDatabaseHistoryEntity?.keyFileUri?.parseUri(), HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey), + fileDatabaseHistoryEntity?.readOnly, fileDatabaseHistoryEntity?.databaseUri?.decodeUri(), fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""), @@ -99,6 +100,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) { fileDatabaseHistoryEntity.databaseUri.parseUri(), fileDatabaseHistoryEntity.keyFileUri?.parseUri(), HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey), + fileDatabaseHistoryEntity.readOnly, fileDatabaseHistoryEntity.databaseUri.decodeUri(), fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias), fileDatabaseInfo.exists, @@ -147,6 +149,8 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) { ?: "", databaseFileToAddOrUpdate.keyFileUri?.toString(), databaseFileToAddOrUpdate.hardwareKey?.value, + databaseFileToAddOrUpdate.readOnly + ?: fileDatabaseHistoryRetrieve?.readOnly, System.currentTimeMillis() ) @@ -168,6 +172,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) { fileDatabaseHistory.databaseUri.parseUri(), fileDatabaseHistory.keyFileUri?.parseUri(), HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey), + fileDatabaseHistory.readOnly, fileDatabaseHistory.databaseUri.decodeUri(), fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias), fileDatabaseInfo.exists, @@ -195,6 +200,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) { fileDatabaseHistory.databaseUri.parseUri(), fileDatabaseHistory.keyFileUri?.parseUri(), HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey), + fileDatabaseHistory.readOnly, fileDatabaseHistory.databaseUri.decodeUri(), databaseFileToDelete.databaseAlias ) diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryEntity.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryEntity.kt index 96fd5df08..c406a7468 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryEntity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryEntity.kt @@ -38,6 +38,9 @@ data class FileDatabaseHistoryEntity( @ColumnInfo(name = "hardware_key") var hardwareKey: String?, + @ColumnInfo(name = "read_only") + var readOnly: Boolean?, + @ColumnInfo(name = "updated") val updated: Long ) { diff --git a/database/src/main/java/com/kunzisoft/keepass/model/DatabaseFile.kt b/database/src/main/java/com/kunzisoft/keepass/model/DatabaseFile.kt index 92470c2fe..fa2320fe9 100644 --- a/database/src/main/java/com/kunzisoft/keepass/model/DatabaseFile.kt +++ b/database/src/main/java/com/kunzisoft/keepass/model/DatabaseFile.kt @@ -6,6 +6,7 @@ import com.kunzisoft.keepass.hardware.HardwareKey data class DatabaseFile(var databaseUri: Uri? = null, var keyFileUri: Uri? = null, var hardwareKey: HardwareKey? = null, + var readOnly: Boolean? = null, var databaseDecodedPath: String? = null, var databaseAlias: String? = null, var databaseFileExists: Boolean = false,