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.)
This commit is contained in:
Richard Macklin
2025-07-26 17:42:39 -07:00
parent 15972efb4f
commit a28d77ba32
6 changed files with 123 additions and 2 deletions

View File

@@ -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')"
]
}
}

View File

@@ -52,6 +52,7 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity 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.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.DeviceUnlockFragment import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
@@ -203,6 +204,13 @@ class MainCredentialActivity : DatabaseModeActivity() {
} }
mForceReadOnly = databaseFileNotExists mForceReadOnly = databaseFileNotExists
// Restore read-only state from database file if not forced
if (!mForceReadOnly) {
databaseFile?.readOnly?.let { savedReadOnlyState ->
mReadOnly = savedReadOnlyState
}
}
invalidateOptionsMenu() invalidateOptionsMenu()
// Post init uri with KeyFile only if needed // Post init uri with KeyFile only if needed
@@ -702,6 +710,12 @@ class MainCredentialActivity : DatabaseModeActivity() {
R.id.menu_open_file_read_mode_key -> { R.id.menu_open_file_read_mode_key -> {
mReadOnly = !mReadOnly mReadOnly = !mReadOnly
changeOpenFileReadIcon(item) 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) else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
} }

View File

@@ -26,10 +26,11 @@ import android.content.Context
import androidx.room.AutoMigration import androidx.room.AutoMigration
@Database( @Database(
version = 2, version = 3,
entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class], entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class],
autoMigrations = [ autoMigrations = [
AutoMigration (from = 1, to = 2) AutoMigration (from = 1, to = 2),
AutoMigration (from = 2, to = 3)
] ]
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {

View File

@@ -49,6 +49,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
databaseUri, databaseUri,
fileDatabaseHistoryEntity?.keyFileUri?.parseUri(), fileDatabaseHistoryEntity?.keyFileUri?.parseUri(),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey), HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey),
fileDatabaseHistoryEntity?.readOnly,
fileDatabaseHistoryEntity?.databaseUri?.decodeUri(), fileDatabaseHistoryEntity?.databaseUri?.decodeUri(),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias
?: ""), ?: ""),
@@ -99,6 +100,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
fileDatabaseHistoryEntity.databaseUri.parseUri(), fileDatabaseHistoryEntity.databaseUri.parseUri(),
fileDatabaseHistoryEntity.keyFileUri?.parseUri(), fileDatabaseHistoryEntity.keyFileUri?.parseUri(),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey), HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey),
fileDatabaseHistoryEntity.readOnly,
fileDatabaseHistoryEntity.databaseUri.decodeUri(), fileDatabaseHistoryEntity.databaseUri.decodeUri(),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias), fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
fileDatabaseInfo.exists, fileDatabaseInfo.exists,
@@ -147,6 +149,8 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
?: "", ?: "",
databaseFileToAddOrUpdate.keyFileUri?.toString(), databaseFileToAddOrUpdate.keyFileUri?.toString(),
databaseFileToAddOrUpdate.hardwareKey?.value, databaseFileToAddOrUpdate.hardwareKey?.value,
databaseFileToAddOrUpdate.readOnly
?: fileDatabaseHistoryRetrieve?.readOnly,
System.currentTimeMillis() System.currentTimeMillis()
) )
@@ -168,6 +172,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
fileDatabaseHistory.databaseUri.parseUri(), fileDatabaseHistory.databaseUri.parseUri(),
fileDatabaseHistory.keyFileUri?.parseUri(), fileDatabaseHistory.keyFileUri?.parseUri(),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey), HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
fileDatabaseHistory.readOnly,
fileDatabaseHistory.databaseUri.decodeUri(), fileDatabaseHistory.databaseUri.decodeUri(),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias), fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
fileDatabaseInfo.exists, fileDatabaseInfo.exists,
@@ -195,6 +200,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
fileDatabaseHistory.databaseUri.parseUri(), fileDatabaseHistory.databaseUri.parseUri(),
fileDatabaseHistory.keyFileUri?.parseUri(), fileDatabaseHistory.keyFileUri?.parseUri(),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey), HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
fileDatabaseHistory.readOnly,
fileDatabaseHistory.databaseUri.decodeUri(), fileDatabaseHistory.databaseUri.decodeUri(),
databaseFileToDelete.databaseAlias databaseFileToDelete.databaseAlias
) )

View File

@@ -38,6 +38,9 @@ data class FileDatabaseHistoryEntity(
@ColumnInfo(name = "hardware_key") @ColumnInfo(name = "hardware_key")
var hardwareKey: String?, var hardwareKey: String?,
@ColumnInfo(name = "read_only")
var readOnly: Boolean?,
@ColumnInfo(name = "updated") @ColumnInfo(name = "updated")
val updated: Long val updated: Long
) { ) {

View File

@@ -6,6 +6,7 @@ import com.kunzisoft.keepass.hardware.HardwareKey
data class DatabaseFile(var databaseUri: Uri? = null, data class DatabaseFile(var databaseUri: Uri? = null,
var keyFileUri: Uri? = null, var keyFileUri: Uri? = null,
var hardwareKey: HardwareKey? = null, var hardwareKey: HardwareKey? = null,
var readOnly: Boolean? = null,
var databaseDecodedPath: String? = null, var databaseDecodedPath: String? = null,
var databaseAlias: String? = null, var databaseAlias: String? = null,
var databaseFileExists: Boolean = false, var databaseFileExists: Boolean = false,