From 77500e7b496306e8a6ae3b76a1cea36c421d85f2 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 22 Oct 2025 15:47:54 -0300 Subject: [PATCH] feat: Add `lastUsed` field to application entity This commit introduces a `lastUsed` timestamp to the `ApplicationEntity`. This new field tracks the last time an application was used and is updated every time a new history event is recorded. The changes include: * Updating the Room database schema to version 14. * Adding a database migration to include the new `lastUsed` column. * Populating the `lastUsed` field on application creation and when adding history. * Refactoring the application list query to sort by `lastUsed` directly, removing the need for a join with the history table. --- .../14.json | 350 ++++++++++++++++++ .../nostrsigner/database/AppDatabase.kt | 9 +- .../nostrsigner/database/ApplicationDao.kt | 23 +- .../nostrsigner/database/ApplicationEntity.kt | 2 + .../nostrsigner/service/BunkerRequestUtils.kt | 2 + .../nostrsigner/service/IntentUtils.kt | 50 +-- .../nostrsigner/ui/ApplicationsScreen.kt | 12 +- .../nostrsigner/ui/NewNsecBunkerScreen.kt | 25 +- .../ui/components/MultiEventHomeScreen.kt | 4 + 9 files changed, 418 insertions(+), 59 deletions(-) create mode 100644 app/schemas/com.greenart7c3.nostrsigner.database.AppDatabase/14.json diff --git a/app/schemas/com.greenart7c3.nostrsigner.database.AppDatabase/14.json b/app/schemas/com.greenart7c3.nostrsigner.database.AppDatabase/14.json new file mode 100644 index 00000000..98b78c6d --- /dev/null +++ b/app/schemas/com.greenart7c3.nostrsigner.database.AppDatabase/14.json @@ -0,0 +1,350 @@ +{ + "formatVersion": 1, + "database": { + "version": 14, + "identityHash": "110b460ba682c08e02d7a25bd4b620e2", + "entities": [ + { + "tableName": "application", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `name` TEXT NOT NULL, `relays` TEXT NOT NULL, `url` TEXT NOT NULL, `icon` TEXT NOT NULL, `description` TEXT NOT NULL, `pubKey` TEXT NOT NULL, `isConnected` INTEGER NOT NULL, `secret` TEXT NOT NULL, `useSecret` INTEGER NOT NULL, `signPolicy` INTEGER NOT NULL, `closeApplication` INTEGER NOT NULL, `deleteAfter` INTEGER NOT NULL, `lastUsed` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relays", + "columnName": "relays", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubKey", + "columnName": "pubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isConnected", + "columnName": "isConnected", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "secret", + "columnName": "secret", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "useSecret", + "columnName": "useSecret", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "signPolicy", + "columnName": "signPolicy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "closeApplication", + "columnName": "closeApplication", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleteAfter", + "columnName": "deleteAfter", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUsed", + "columnName": "lastUsed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_key` ON `${TABLE_NAME}` (`key`)" + }, + { + "name": "index_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_name` ON `${TABLE_NAME}` (`name`)" + } + ] + }, + { + "tableName": "applicationPermission", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `pkKey` TEXT NOT NULL, `type` TEXT NOT NULL, `kind` INTEGER, `acceptable` INTEGER NOT NULL, `rememberType` INTEGER NOT NULL, `acceptUntil` INTEGER NOT NULL, `rejectUntil` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`pkKey`) REFERENCES `application`(`key`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER" + }, + { + "fieldPath": "pkKey", + "columnName": "pkKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "INTEGER" + }, + { + "fieldPath": "acceptable", + "columnName": "acceptable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rememberType", + "columnName": "rememberType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "acceptUntil", + "columnName": "acceptUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rejectUntil", + "columnName": "rejectUntil", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "permissions_by_pk_key", + "unique": false, + "columnNames": [ + "pkKey" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `permissions_by_pk_key` ON `${TABLE_NAME}` (`pkKey`)" + }, + { + "name": "permissions_unique", + "unique": true, + "columnNames": [ + "pkKey", + "type", + "kind" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `permissions_unique` ON `${TABLE_NAME}` (`pkKey`, `type`, `kind`)" + } + ], + "foreignKeys": [ + { + "table": "application", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pkKey" + ], + "referencedColumns": [ + "key" + ] + } + ] + }, + { + "tableName": "amber_log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `type` TEXT NOT NULL, `message` TEXT NOT NULL, `time` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "history2", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `pkKey` TEXT NOT NULL, `type` TEXT NOT NULL, `kind` INTEGER, `time` INTEGER NOT NULL, `accepted` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pkKey", + "columnName": "pkKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "INTEGER" + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accepted", + "columnName": "accepted", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "history_by_pk_key2", + "unique": false, + "columnNames": [ + "pkKey" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `history_by_pk_key2` ON `${TABLE_NAME}` (`pkKey`)" + }, + { + "name": "history_by_id2", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `history_by_id2` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "history_by_time", + "unique": false, + "columnNames": [ + "time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `history_by_time` ON `${TABLE_NAME}` (`time`)" + }, + { + "name": "history_by_key_and_time", + "unique": false, + "columnNames": [ + "pkKey", + "time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `history_by_key_and_time` ON `${TABLE_NAME}` (`pkKey`, `time`)" + } + ] + } + ], + "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, '110b460ba682c08e02d7a25bd4b620e2')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt b/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt index 69b3970a..40effd1f 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt @@ -122,6 +122,12 @@ val MIGRATION_12_13 = object : Migration(12, 13) { } } +val MIGRATION_13_14 = object : Migration(13, 14) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE application ADD COLUMN lastUsed INTEGER NOT NULL DEFAULT 0") + } +} + @Database( entities = [ ApplicationEntity::class, @@ -129,7 +135,7 @@ val MIGRATION_12_13 = object : Migration(12, 13) { LogEntity::class, HistoryEntity2::class, ], - version = 13, + version = 14, ) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { @@ -164,6 +170,7 @@ abstract class AppDatabase : RoomDatabase() { .addMigrations(MIGRATION_10_11) .addMigrations(MIGRATION_11_12) .addMigrations(MIGRATION_12_13) + .addMigrations(MIGRATION_13_14) .build() instance.openHelper.writableDatabase.execSQL("VACUUM") diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationDao.kt b/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationDao.kt index 3a64db01..ab6abfe4 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationDao.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationDao.kt @@ -22,22 +22,8 @@ interface ApplicationDao { @Transaction suspend fun getAllNotConnected(): List - @Query( - """ - SELECT - a.*, - MAX(h.time) AS latestTime - FROM application a - LEFT JOIN history2 h ON a.`key` = h.pkKey - WHERE a.pubKey = :pubKey - GROUP BY - a.`key`, a.description, a.icon, a.isConnected, a.name, - a.pubKey, a.secret, a.signPolicy, a.url, a.useSecret, - a.closeApplication, a.deleteAfter, a.relays - ORDER BY latestTime DESC - """, - ) - fun getAllFlow(pubKey: String): Flow> + @Query("SELECT a.* FROM application a WHERE a.pubKey = :pubKey ORDER BY a.lastUsed DESC") + fun getAllFlow(pubKey: String): Flow> @Query("SELECT * FROM application WHERE `key` = :key") @Transaction @@ -153,11 +139,16 @@ interface ApplicationDao { suspend fun addHistory(entity: HistoryEntity2) { try { innerAddHistory(entity) + updateLastUsed(entity.pkKey, entity.time) } catch (e: Exception) { Log.e(Amber.TAG, "Error adding history", e) } } + @Query("UPDATE application SET lastUsed = :time where `key` = :key") + @Transaction + suspend fun updateLastUsed(key: String, time: Long) + @Query("SELECT * FROM history2 where pkKey = :pk ORDER BY time DESC") fun getAllHistory(pk: String): Flow> diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationEntity.kt b/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationEntity.kt index af04718f..eb44cb44 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationEntity.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationEntity.kt @@ -40,6 +40,7 @@ data class ApplicationEntity( var signPolicy: Int, var closeApplication: Boolean, var deleteAfter: Long, + val lastUsed: Long, ) { companion object { fun empty() = ApplicationEntity( @@ -56,6 +57,7 @@ data class ApplicationEntity( signPolicy = 0, closeApplication = true, deleteAfter = 0L, + lastUsed = 0L, ) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt index 361b8e88..4d048e9f 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt @@ -287,6 +287,7 @@ object BunkerRequestUtils { account.signPolicy, shouldCloseApplication ?: bunkerRequest.closeApplication, deleteAfter = deleteAfter, + lastUsed = TimeUtils.now(), ), permissions = mutableListOf(), ) @@ -428,6 +429,7 @@ object BunkerRequestUtils { account.signPolicy, bunkerRequest.closeApplication, deleteAfter = 0L, + lastUsed = TimeUtils.now(), ), permissions = mutableListOf(), ) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt index cdda2589..01ced358 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt @@ -581,19 +581,20 @@ object IntentUtils { val application = savedApplication ?: ApplicationWithPermissions( application = ApplicationEntity( - key, - appName ?: localAppName ?: "", - if (packageName != null) emptyList() else relays, - "", - "", - "", - account.hexKey, - true, - "", - false, - account.signPolicy, - shouldCloseApplication != false, + key = key, + name = appName ?: localAppName ?: "", + relays = if (packageName != null) emptyList() else relays, + url = "", + icon = "", + description = "", + pubKey = account.hexKey, + isConnected = true, + secret = "", + useSecret = false, + signPolicy = account.signPolicy, + closeApplication = shouldCloseApplication != false, deleteAfter = deleteAfter, + lastUsed = TimeUtils.now(), ), permissions = mutableListOf(), ) @@ -746,19 +747,20 @@ object IntentUtils { val application = savedApplication ?: ApplicationWithPermissions( application = ApplicationEntity( - key, - appName, - relays, - "", - "", - "", - account.hexKey, - true, - "", - false, - account.signPolicy, - true, + key = key, + name = appName, + relays = relays, + url = "", + icon = "", + description = "", + pubKey = account.hexKey, + isConnected = true, + secret = "", + useSecret = false, + signPolicy = account.signPolicy, + closeApplication = true, deleteAfter = 0L, + lastUsed = TimeUtils.now(), ), permissions = mutableListOf(), ) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/ApplicationsScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/ApplicationsScreen.kt index fea472c3..b97aa3e9 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/ApplicationsScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/ApplicationsScreen.kt @@ -145,7 +145,7 @@ fun ApplicationsScreen( .fillMaxSize() .padding(vertical = 4.dp) .clickable { - navController.navigate("Permission/${applicationWithHistory.application.key}") + navController.navigate("Permission/${applicationWithHistory.key}") }, verticalAlignment = Alignment.CenterVertically, ) { @@ -154,15 +154,15 @@ fun ApplicationsScreen( ) { Text( modifier = Modifier.padding(top = 16.dp), - text = applicationWithHistory.application.name.ifBlank { applicationWithHistory.application.key.toShortenHex() }, + text = applicationWithHistory.name.ifBlank { applicationWithHistory.key.toShortenHex() }, fontSize = 24.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - if (applicationWithHistory.application.relays.isNotEmpty()) { + if (applicationWithHistory.relays.isNotEmpty()) { Text( modifier = Modifier.padding(top = 4.dp), - text = applicationWithHistory.application.relays.joinToString { it.displayUrl() }, + text = applicationWithHistory.relays.joinToString { it.displayUrl() }, fontSize = 16.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -174,14 +174,14 @@ fun ApplicationsScreen( ) { Text( modifier = Modifier.padding(top = 4.dp, bottom = 16.dp), - text = applicationWithHistory.application.key.toShortenHex(), + text = applicationWithHistory.key.toShortenHex(), fontSize = 16.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, ) Text( modifier = Modifier.padding(top = 4.dp, bottom = 16.dp), - text = if (applicationWithHistory.latestTime == null) stringResource(R.string.never) else TimeUtils.formatLongToCustomDateTime(applicationWithHistory.latestTime * 1000), + text = if (applicationWithHistory.lastUsed == 0L) stringResource(R.string.never) else TimeUtils.formatLongToCustomDateTime(applicationWithHistory.lastUsed * 1000), fontSize = 16.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/NewNsecBunkerScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/NewNsecBunkerScreen.kt index d8feeda1..09047ba7 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/NewNsecBunkerScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/NewNsecBunkerScreen.kt @@ -242,19 +242,20 @@ fun NewNsecBunkerScreen( val application = ApplicationEntity( - secret.value, - name.text, - relays, - "", - "", - "", - account.hexKey, - false, - secret.value, - true, - account.signPolicy, - true, + key = secret.value, + name = name.text, + relays = relays, + url = "", + icon = "", + description = "", + pubKey = account.hexKey, + isConnected = false, + secret = secret.value, + useSecret = true, + signPolicy = account.signPolicy, + closeApplication = true, deleteAfter = deleteAfter, + lastUsed = 0L, ) Amber.instance.getDatabase(account.npub).applicationDao().insertApplication( diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MultiEventHomeScreen.kt index 5eae0a55..6be46848 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MultiEventHomeScreen.kt @@ -242,6 +242,7 @@ fun IntentMultiEventHomeScreen( thisAccount.signPolicy, true, 0L, + lastUsed = TimeUtils.now(), ), permissions = mutableListOf(), ) @@ -435,6 +436,7 @@ fun IntentMultiEventHomeScreen( thisAccount.signPolicy, true, 0L, + lastUsed = TimeUtils.now(), ), permissions = mutableListOf(), ) @@ -614,6 +616,7 @@ fun BunkerMultiEventHomeScreen( thisAccount.signPolicy, request.closeApplication, 0L, + lastUsed = TimeUtils.now(), ), permissions = mutableListOf(), ) @@ -882,6 +885,7 @@ fun BunkerMultiEventHomeScreen( thisAccount.signPolicy, request.closeApplication, 0L, + lastUsed = TimeUtils.now(), ), permissions = mutableListOf(), )