diff --git a/app/schemas/com.greenart7c3.nostrsigner.database.AppDatabase/10.json b/app/schemas/com.greenart7c3.nostrsigner.database.AppDatabase/10.json new file mode 100644 index 00000000..65009adf --- /dev/null +++ b/app/schemas/com.greenart7c3.nostrsigner.database.AppDatabase/10.json @@ -0,0 +1,455 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "bd08ac27efa990e98825cdcc6e8e167d", + "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, 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 + } + ], + "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": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `eventId` TEXT NOT NULL, `time` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_notification_eventId", + "unique": false, + "columnNames": [ + "eventId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notification_eventId` ON `${TABLE_NAME}` (`eventId`)" + }, + { + "name": "index_notification_time", + "unique": false, + "columnNames": [ + "time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notification_time` ON `${TABLE_NAME}` (`time`)" + } + ] + }, + { + "tableName": "history", + "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, FOREIGN KEY(`pkKey`) REFERENCES `application`(`key`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "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_key", + "unique": false, + "columnNames": [ + "pkKey" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `history_by_pk_key` ON `${TABLE_NAME}` (`pkKey`)" + }, + { + "name": "history_by_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `history_by_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "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`)" + } + ] + } + ], + "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, 'bd08ac27efa990e98825cdcc6e8e167d')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.greenart7c3.nostrsigner.database.AppDatabase/9.json b/app/schemas/com.greenart7c3.nostrsigner.database.AppDatabase/9.json index 56d4b999..7a47515e 100644 --- a/app/schemas/com.greenart7c3.nostrsigner.database.AppDatabase/9.json +++ b/app/schemas/com.greenart7c3.nostrsigner.database.AppDatabase/9.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 9, - "identityHash": "08bd4cb1d1d0fd652f0234b33fcd367b", + "identityHash": "bd08ac27efa990e98825cdcc6e8e167d", "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, PRIMARY KEY(`key`))", + "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, PRIMARY KEY(`key`))", "fields": [ { "fieldPath": "key", @@ -79,6 +79,12 @@ "columnName": "closeApplication", "affinity": "INTEGER", "notNull": true + }, + { + "fieldPath": "deleteAfter", + "columnName": "deleteAfter", + "affinity": "INTEGER", + "notNull": true } ], "primaryKey": { @@ -443,7 +449,7 @@ ], "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, '08bd4cb1d1d0fd652f0234b33fcd367b')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bd08ac27efa990e98825cdcc6e8e167d')" ] } } \ No newline at end of file diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt b/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt index 6aa6e8eb..da0b2b41 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt @@ -196,7 +196,7 @@ class Amber : Application(), LifecycleObserver { _instance = this LocalPreferences.allSavedAccounts(this).forEach { - databases[it.npub] = AppDatabase.getDatabase(this, it.npub) + databases[it.npub] = getDatabase(it.npub) applicationIOScope.launch { databases[it.npub]?.applicationDao()?.getAllNotConnected()?.forEach { app -> if (app.application.secret.isNotEmpty() && app.application.secret != app.application.key) { 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 e10afe31..73d34e0a 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt @@ -87,6 +87,12 @@ val MIGRATION_8_9 = } } +val MIGRATION_9_10 = object : Migration(9, 10) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE application ADD COLUMN deleteAfter INTEGER NOT NULL DEFAULT 0") + } +} + @Database( entities = [ ApplicationEntity::class, @@ -96,7 +102,7 @@ val MIGRATION_8_9 = LogEntity::class, HistoryEntity2::class, ], - version = 9, + version = 10, ) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { @@ -127,6 +133,7 @@ abstract class AppDatabase : RoomDatabase() { .addMigrations(MIGRATION_6_7) .addMigrations(MIGRATION_7_8) .addMigrations(MIGRATION_8_9) + .addMigrations(MIGRATION_9_10) .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 206593b4..2c95b265 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationDao.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationDao.kt @@ -46,11 +46,16 @@ interface ApplicationDao { @Query( """ - SELECT a.*, MAX(h.time) as latestTime + SELECT + a.*, + MAX(h.time) AS latestTime FROM application a LEFT JOIN history2 h ON a.`key` = h.pkKey - AND a.pubKey = :pubKey - GROUP BY a.`key`, a.description, a.icon, a.isConnected, a.name, a.pubKey, a.secret, a.signPolicy, a.url + 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 """, ) @@ -233,4 +238,8 @@ interface ApplicationDao { @Delete @Transaction suspend fun deleteLog(logEntity: LogEntity) + + @Query("DELETE FROM application WHERE deleteAfter < :time AND deleteAfter > 0") + @Transaction + suspend fun deleteOldApplications(time: Long): Int } 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 fd79b68e..af04718f 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationEntity.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationEntity.kt @@ -39,6 +39,7 @@ data class ApplicationEntity( val useSecret: Boolean, var signPolicy: Int, var closeApplication: Boolean, + var deleteAfter: Long, ) { companion object { fun empty() = ApplicationEntity( @@ -54,6 +55,7 @@ data class ApplicationEntity( useSecret = false, signPolicy = 0, closeApplication = true, + deleteAfter = 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 0ca53982..2024bf16 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt @@ -232,6 +232,7 @@ object BunkerRequestUtils { shouldCloseApplication: Boolean? = null, rememberType: RememberType, oldKey: String = "", + deleteAfter: Long = 0L, ) { onLoading(true) Amber.instance.applicationIOScope.launch { @@ -262,6 +263,9 @@ object BunkerRequestUtils { } } savedApplication = database.applicationDao().getByKey(key) + if (bunkerRequest.request is BunkerRequestConnect) { + savedApplication?.application?.deleteAfter = deleteAfter + } val relays = savedApplication?.application?.relays?.ifEmpty { defaultRelays } ?: bunkerRequest.relays.ifEmpty { defaultRelays } val secret = if (bunkerRequest.request is BunkerRequestConnect) bunkerRequest.request.secret ?: "" else "" @@ -280,6 +284,7 @@ object BunkerRequestUtils { secret.isNotBlank(), account.signPolicy, shouldCloseApplication ?: bunkerRequest.closeApplication, + deleteAfter = deleteAfter, ), permissions = mutableListOf(), ) @@ -420,6 +425,7 @@ object BunkerRequestUtils { secret.isNotBlank(), account.signPolicy, bunkerRequest.closeApplication, + deleteAfter = 0L, ), permissions = mutableListOf(), ) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/ConnectivityService.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/ConnectivityService.kt index d9d68785..15e77073 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/ConnectivityService.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/ConnectivityService.kt @@ -9,6 +9,8 @@ import android.os.IBinder import android.util.Log import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.BuildConfig +import com.greenart7c3.nostrsigner.LocalPreferences +import com.greenart7c3.nostrsigner.database.LogEntity import java.util.Timer import java.util.TimerTask import kotlinx.coroutines.CoroutineScope @@ -123,6 +125,23 @@ class ConnectivityService : Service() { } if (Amber.instance.settings.killSwitch.value) return + scope.launch { + LocalPreferences.allSavedAccounts(Amber.instance).forEach { accountInfo -> + val now = System.currentTimeMillis() / 1000 + val deleted = Amber.instance.getDatabase(accountInfo.npub).applicationDao().deleteOldApplications(now) + Log.d(Amber.TAG, "Deleted $deleted expired applications") + Amber.instance.getDatabase(accountInfo.npub).applicationDao().insertLog( + LogEntity( + id = 0, + url = "", + type = "deleteApplications", + message = "Deleted $deleted expired applications", + time = System.currentTimeMillis(), + ), + ) + } + } + scope.launch { if (!Amber.instance.client.isActive()) { Amber.instance.client.connect() 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 52335244..9c7c66cd 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt @@ -562,6 +562,7 @@ object IntentUtils { onRemoveIntentData: (List, IntentResultType) -> Unit, shouldCloseApplication: Boolean? = null, rememberType: RememberType, + deleteAfter: Long = 0L, ) { onLoading(true) Amber.instance.applicationIOScope.launch { @@ -592,6 +593,7 @@ object IntentUtils { false, account.signPolicy, shouldCloseApplication != false, + deleteAfter = deleteAfter, ), permissions = mutableListOf(), ) @@ -756,6 +758,7 @@ object IntentUtils { false, account.signPolicy, true, + deleteAfter = 0L, ), permissions = mutableListOf(), ) 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 9717885d..d8feeda1 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/NewNsecBunkerScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/NewNsecBunkerScreen.kt @@ -2,6 +2,7 @@ package com.greenart7c3.nostrsigner.ui import android.content.ClipData import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -22,6 +23,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -51,7 +53,9 @@ import com.greenart7c3.nostrsigner.database.ApplicationEntity import com.greenart7c3.nostrsigner.models.Account import com.greenart7c3.nostrsigner.ui.actions.onAddRelay import com.greenart7c3.nostrsigner.ui.components.AmberButton +import com.greenart7c3.nostrsigner.ui.components.TitleExplainer import java.util.UUID +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -78,6 +82,16 @@ fun NewNsecBunkerScreen( val isLoading = remember { mutableStateOf(false) } val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current + val deleteAfterItems = + persistentListOf( + TitleExplainer(stringResource(DeleteAfterType.NEVER.resourceId)), + TitleExplainer(stringResource(DeleteAfterType.FIVE_MINUTES.resourceId)), + TitleExplainer(stringResource(DeleteAfterType.TEN_MINUTES.resourceId)), + TitleExplainer(stringResource(DeleteAfterType.ONE_HOUR.resourceId)), + TitleExplainer(stringResource(DeleteAfterType.ONE_DAY.resourceId)), + TitleExplainer(stringResource(DeleteAfterType.ONE_WEEK.resourceId)), + ) + var deleteAfterIndex by remember { mutableIntStateOf(DeleteAfterType.NEVER.screenCode) } LaunchedEffect(Unit) { focusRequester.requestFocus() @@ -113,6 +127,17 @@ fun NewNsecBunkerScreen( .padding(vertical = 20.dp), ) + Box { + SettingsRow( + R.string.delete_after, + null, + deleteAfterItems, + deleteAfterIndex, + ) { + deleteAfterIndex = it + } + } + Row( Modifier .fillMaxWidth() @@ -213,6 +238,8 @@ fun NewNsecBunkerScreen( } scope.launch(Dispatchers.IO) { + val deleteAfter = deleteAfterToSeconds(parseDeleteAfterType(deleteAfterIndex)) + val application = ApplicationEntity( secret.value, @@ -227,6 +254,7 @@ fun NewNsecBunkerScreen( true, account.signPolicy, true, + deleteAfter = deleteAfter, ) Amber.instance.getDatabase(account.npub).applicationDao().insertApplication( diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt index db766117..46dc7d0f 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt @@ -456,6 +456,38 @@ enum class RememberType(val screenCode: Int, val resourceId: Int) { ALWAYS(4, R.string.always), } +enum class DeleteAfterType(val screenCode: Int, val resourceId: Int) { + NEVER(0, R.string.never), + FIVE_MINUTES(1, R.string.five_minutes), + TEN_MINUTES(2, R.string.ten_minutes), + ONE_HOUR(3, R.string.one_hour), + ONE_DAY(4, R.string.one_day), + ONE_WEEK(5, R.string.one_week), +} + +fun parseDeleteAfterType(screenCode: Int): DeleteAfterType { + return when (screenCode) { + DeleteAfterType.FIVE_MINUTES.screenCode -> DeleteAfterType.FIVE_MINUTES + DeleteAfterType.TEN_MINUTES.screenCode -> DeleteAfterType.TEN_MINUTES + DeleteAfterType.ONE_HOUR.screenCode -> DeleteAfterType.ONE_HOUR + DeleteAfterType.ONE_DAY.screenCode -> DeleteAfterType.ONE_DAY + DeleteAfterType.ONE_WEEK.screenCode -> DeleteAfterType.ONE_WEEK + else -> DeleteAfterType.NEVER + } +} + +fun deleteAfterToSeconds(deleteAfterType: DeleteAfterType): Long { + val now = System.currentTimeMillis() / 1000 + return when (deleteAfterType) { + DeleteAfterType.FIVE_MINUTES -> now + 300 + DeleteAfterType.TEN_MINUTES -> now + 600 + DeleteAfterType.ONE_HOUR -> now + 3600 + DeleteAfterType.ONE_DAY -> now + 86400 + DeleteAfterType.ONE_WEEK -> now + 604800 + else -> 0 + } +} + enum class BiometricsTimeType(val screenCode: Int, val resourceId: Int) { EVERY_TIME(0, R.string.every_time), ONE_MINUTE(1, R.string.one_minute), diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerConnectRequestScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerConnectRequestScreen.kt index 6bc867b3..74ef0904 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerConnectRequestScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerConnectRequestScreen.kt @@ -2,6 +2,7 @@ package com.greenart7c3.nostrsigner.ui.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -29,8 +30,13 @@ import androidx.compose.ui.unit.sp import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.models.Account import com.greenart7c3.nostrsigner.models.Permission +import com.greenart7c3.nostrsigner.ui.DeleteAfterType import com.greenart7c3.nostrsigner.ui.RememberType +import com.greenart7c3.nostrsigner.ui.SettingsRow +import com.greenart7c3.nostrsigner.ui.deleteAfterToSeconds +import com.greenart7c3.nostrsigner.ui.parseDeleteAfterType import kotlin.collections.forEach +import kotlinx.collections.immutable.persistentListOf @Composable fun BunkerConnectRequestScreen( @@ -39,7 +45,7 @@ fun BunkerConnectRequestScreen( account: Account, appName: String, permissions: List?, - onAccept: (List?, Int, Boolean?, RememberType) -> Unit, + onAccept: (List?, Int, Boolean?, RememberType, Long) -> Unit, onReject: (RememberType) -> Unit, ) { val localPermissions = remember { @@ -70,6 +76,16 @@ fun BunkerConnectRequestScreen( ) var selectedOption by remember { mutableIntStateOf(account.signPolicy) } + val deleteAfterItems = + persistentListOf( + TitleExplainer(stringResource(DeleteAfterType.NEVER.resourceId)), + TitleExplainer(stringResource(DeleteAfterType.FIVE_MINUTES.resourceId)), + TitleExplainer(stringResource(DeleteAfterType.TEN_MINUTES.resourceId)), + TitleExplainer(stringResource(DeleteAfterType.ONE_HOUR.resourceId)), + TitleExplainer(stringResource(DeleteAfterType.ONE_DAY.resourceId)), + TitleExplainer(stringResource(DeleteAfterType.ONE_WEEK.resourceId)), + ) + var deleteAfterIndex by remember { mutableIntStateOf(DeleteAfterType.NEVER.screenCode) } Text( text = stringResource(R.string.handle_application_permissions), @@ -99,6 +115,19 @@ fun BunkerConnectRequestScreen( ) } + Box( + Modifier.padding(4.dp), + ) { + SettingsRow( + R.string.delete_after, + null, + deleteAfterItems, + deleteAfterIndex, + ) { + deleteAfterIndex = it + } + } + ChooseSignPolicy( selectedOption = selectedOption, onSelected = { @@ -122,7 +151,8 @@ fun BunkerConnectRequestScreen( AmberButton( modifier = Modifier.padding(vertical = 20.dp), onClick = { - onAccept(localPermissions, selectedOption, closeApp, rememberType) + val deleteAfter = deleteAfterToSeconds(parseDeleteAfterType(deleteAfterIndex)) + onAccept(localPermissions, selectedOption, closeApp, rememberType, deleteAfter) }, text = stringResource(R.string.grant_permissions), ) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt index c521e894..afdfe7ea 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -26,7 +27,6 @@ import androidx.compose.ui.unit.sp import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.LocalPreferences import com.greenart7c3.nostrsigner.R -import com.greenart7c3.nostrsigner.database.AppDatabase import com.greenart7c3.nostrsigner.database.ApplicationWithPermissions import com.greenart7c3.nostrsigner.models.Account import com.greenart7c3.nostrsigner.models.AmberBunkerRequest @@ -90,6 +90,7 @@ fun BunkerSingleEventHomeScreen( var localSignPolicy by remember { mutableIntStateOf(0) } var localRememberType by remember { mutableStateOf(RememberType.NEVER) } var existingAppKey by remember { mutableStateOf("") } + var localDeleteAfter by remember { mutableLongStateOf(0L) } if (showExistingAppDialog) { AlertDialog( @@ -122,6 +123,7 @@ fun BunkerSingleEventHomeScreen( signPolicy = localSignPolicy, shouldCloseApplication = localCloseApplication, rememberType = localRememberType, + deleteAfter = localDeleteAfter, ) }, text = stringResource(R.string.replace), @@ -144,6 +146,7 @@ fun BunkerSingleEventHomeScreen( signPolicy = localSignPolicy, shouldCloseApplication = localCloseApplication, rememberType = localRememberType, + deleteAfter = localDeleteAfter, ) }, ) @@ -171,7 +174,7 @@ fun BunkerSingleEventHomeScreen( Permission(split[0].trim(), null) } }, - onAccept = { permissions, signPolicy, closeApplication, rememberType -> + onAccept = { permissions, signPolicy, closeApplication, rememberType, deleteAfter -> val result = bunkerRequest.nostrConnectSecret.ifBlank { "ack" } localPermissions = permissions @@ -181,7 +184,7 @@ fun BunkerSingleEventHomeScreen( if (bunkerRequest.name.isNotBlank()) { Amber.instance.applicationIOScope.launch { - val existingApp = AppDatabase.getDatabase(Amber.instance, account.npub).applicationDao().getByName(bunkerRequest.name) + val existingApp = Amber.instance.getDatabase(account.npub).applicationDao().getByName(bunkerRequest.name) if (existingApp == null) { BunkerRequestUtils.sendResult( context = context, @@ -196,6 +199,7 @@ fun BunkerSingleEventHomeScreen( signPolicy = signPolicy, shouldCloseApplication = closeApplication, rememberType = rememberType, + deleteAfter = deleteAfter, ) } else { existingAppKey = existingApp.application.key @@ -216,6 +220,7 @@ fun BunkerSingleEventHomeScreen( signPolicy = signPolicy, shouldCloseApplication = closeApplication, rememberType = rememberType, + deleteAfter = deleteAfter, ) } }, 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 75e0ecfd..5eae0a55 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 @@ -241,6 +241,7 @@ fun IntentMultiEventHomeScreen( false, thisAccount.signPolicy, true, + 0L, ), permissions = mutableListOf(), ) @@ -433,6 +434,7 @@ fun IntentMultiEventHomeScreen( false, thisAccount.signPolicy, true, + 0L, ), permissions = mutableListOf(), ) @@ -611,6 +613,7 @@ fun BunkerMultiEventHomeScreen( secret.isNotBlank(), thisAccount.signPolicy, request.closeApplication, + 0L, ), permissions = mutableListOf(), ) @@ -878,6 +881,7 @@ fun BunkerMultiEventHomeScreen( secret.isNotBlank(), thisAccount.signPolicy, request.closeApplication, + 0L, ), permissions = mutableListOf(), ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fe511259..fe9cb9ca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -554,4 +554,8 @@ Disable kill switch Kill Switch enabled, remote login and profile fetching will not work Edit Relays + One hour + One day + One week + Delete after