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.
This commit is contained in:
greenart7c3
2025-10-22 15:47:54 -03:00
parent e11704f146
commit 77500e7b49
9 changed files with 418 additions and 59 deletions
@@ -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')"
]
}
}
@@ -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")
@@ -22,22 +22,8 @@ interface ApplicationDao {
@Transaction
suspend fun getAllNotConnected(): List<ApplicationWithPermissions>
@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<List<ApplicationWithLatestHistory>>
@Query("SELECT a.* FROM application a WHERE a.pubKey = :pubKey ORDER BY a.lastUsed DESC")
fun getAllFlow(pubKey: String): Flow<List<ApplicationEntity>>
@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<List<HistoryEntity2>>
@@ -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,
)
}
@@ -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(),
)
@@ -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(),
)
@@ -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,
@@ -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(
@@ -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(),
)