mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-29 13:00:17 +02:00
fix(database): db cache
This commit is contained in:
parent
d1c4b4febe
commit
a90f4875a7
@ -261,69 +261,71 @@ class ScopeContent(
|
|||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
// e2ee section
|
// e2ee section
|
||||||
|
|
||||||
SectionTitle(translation["e2ee_title"])
|
if (context.config.root.experimental.e2eEncryption.globalState == true) {
|
||||||
var hasSecretKey by remember { mutableStateOf(context.e2eeImplementation.friendKeyExists(friend.userId))}
|
SectionTitle(translation["e2ee_title"])
|
||||||
var importDialog by remember { mutableStateOf(false) }
|
var hasSecretKey by remember { mutableStateOf(context.e2eeImplementation.friendKeyExists(friend.userId))}
|
||||||
|
var importDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
if (importDialog) {
|
if (importDialog) {
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = { importDialog = false }
|
onDismissRequest = { importDialog = false }
|
||||||
) {
|
) {
|
||||||
dialogs.RawInputDialog(onDismiss = { importDialog = false }, onConfirm = { newKey ->
|
dialogs.RawInputDialog(onDismiss = { importDialog = false }, onConfirm = { newKey ->
|
||||||
importDialog = false
|
importDialog = false
|
||||||
runCatching {
|
runCatching {
|
||||||
val key = Base64.decode(newKey)
|
val key = Base64.decode(newKey)
|
||||||
if (key.size != 32) {
|
if (key.size != 32) {
|
||||||
context.longToast("Invalid key size (must be 32 bytes)")
|
context.longToast("Invalid key size (must be 32 bytes)")
|
||||||
return@runCatching
|
return@runCatching
|
||||||
|
}
|
||||||
|
|
||||||
|
context.e2eeImplementation.storeSharedSecretKey(friend.userId, key)
|
||||||
|
context.longToast("Successfully imported key")
|
||||||
|
hasSecretKey = true
|
||||||
|
}.onFailure {
|
||||||
|
context.longToast("Failed to import key: ${it.message}")
|
||||||
|
context.log.error("Failed to import key", it)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
context.e2eeImplementation.storeSharedSecretKey(friend.userId, key)
|
}
|
||||||
context.longToast("Successfully imported key")
|
|
||||||
hasSecretKey = true
|
|
||||||
}.onFailure {
|
|
||||||
context.longToast("Failed to import key: ${it.message}")
|
|
||||||
context.log.error("Failed to import key", it)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ContentCard {
|
ContentCard {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
if (hasSecretKey) {
|
if (hasSecretKey) {
|
||||||
OutlinedButton(onClick = {
|
OutlinedButton(onClick = {
|
||||||
val secretKey = Base64.encode(context.e2eeImplementation.getSharedSecretKey(friend.userId) ?: return@OutlinedButton)
|
val secretKey = Base64.encode(context.e2eeImplementation.getSharedSecretKey(friend.userId) ?: return@OutlinedButton)
|
||||||
//TODO: fingerprint auth
|
//TODO: fingerprint auth
|
||||||
context.activity!!.startActivity(Intent.createChooser(Intent().apply {
|
context.activity!!.startActivity(Intent.createChooser(Intent().apply {
|
||||||
action = Intent.ACTION_SEND
|
action = Intent.ACTION_SEND
|
||||||
putExtra(Intent.EXTRA_TEXT, secretKey)
|
putExtra(Intent.EXTRA_TEXT, secretKey)
|
||||||
type = "text/plain"
|
type = "text/plain"
|
||||||
}, "").apply {
|
}, "").apply {
|
||||||
putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(
|
putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
putExtra(Intent.EXTRA_TEXT, secretKey)
|
putExtra(Intent.EXTRA_TEXT, secretKey)
|
||||||
putExtra(Intent.EXTRA_SUBJECT, secretKey)
|
putExtra(Intent.EXTRA_SUBJECT, secretKey)
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
Text(
|
||||||
|
text = "Export Base64",
|
||||||
|
maxLines = 1
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
OutlinedButton(onClick = { importDialog = true }) {
|
||||||
Text(
|
Text(
|
||||||
text = "Export Base64",
|
text = "Import Base64",
|
||||||
maxLines = 1
|
maxLines = 1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OutlinedButton(onClick = { importDialog = true }) {
|
|
||||||
Text(
|
|
||||||
text = "Import Base64",
|
|
||||||
maxLines = 1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -363,7 +363,7 @@ class ExportMemories : AbstractAction() {
|
|||||||
val database = runCatching {
|
val database = runCatching {
|
||||||
SQLiteDatabase.openDatabase(
|
SQLiteDatabase.openDatabase(
|
||||||
context.androidContext.getDatabasePath("memories.db"),
|
context.androidContext.getDatabasePath("memories.db"),
|
||||||
OpenParams.Builder().build(),
|
OpenParams.Builder().setOpenFlags(SQLiteDatabase.OPEN_READONLY).build()
|
||||||
)
|
)
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
|
@ -19,17 +19,45 @@ import me.rhunk.snapenhance.core.ModContext
|
|||||||
import me.rhunk.snapenhance.core.manager.Manager
|
import me.rhunk.snapenhance.core.manager.Manager
|
||||||
|
|
||||||
|
|
||||||
|
enum class DatabaseType(
|
||||||
|
val fileName: String
|
||||||
|
) {
|
||||||
|
MAIN("main.db"),
|
||||||
|
ARROYO("arroyo.db")
|
||||||
|
}
|
||||||
|
|
||||||
class DatabaseAccess(
|
class DatabaseAccess(
|
||||||
private val context: ModContext
|
private val context: ModContext
|
||||||
) : Manager {
|
) : Manager {
|
||||||
companion object {
|
private val openedDatabases = mutableMapOf<DatabaseType, SQLiteDatabase>()
|
||||||
val DATABASES = mapOf(
|
|
||||||
"main" to "main.db",
|
private fun useDatabase(database: DatabaseType, writeMode: Boolean = false): SQLiteDatabase? {
|
||||||
"arroyo" to "arroyo.db"
|
if (openedDatabases.containsKey(database) && openedDatabases[database]?.isOpen == true) {
|
||||||
)
|
return openedDatabases[database]
|
||||||
|
}
|
||||||
|
|
||||||
|
val dbPath = context.androidContext.getDatabasePath(database.fileName)
|
||||||
|
if (!dbPath.exists()) return null
|
||||||
|
return runCatching {
|
||||||
|
SQLiteDatabase.openDatabase(
|
||||||
|
dbPath,
|
||||||
|
OpenParams.Builder()
|
||||||
|
.setOpenFlags(
|
||||||
|
if (writeMode) SQLiteDatabase.OPEN_READWRITE or SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING
|
||||||
|
else SQLiteDatabase.OPEN_READONLY
|
||||||
|
)
|
||||||
|
.setErrorHandler {
|
||||||
|
context.androidContext.deleteDatabase(dbPath.absolutePath)
|
||||||
|
context.softRestartApp()
|
||||||
|
}.build()
|
||||||
|
)
|
||||||
|
}.onFailure {
|
||||||
|
context.log.error("Failed to open database ${database.fileName}!", it)
|
||||||
|
}.getOrNull()?.also {
|
||||||
|
openedDatabases[database] = it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private val mainDb by lazy { openLocalDatabase("main") }
|
|
||||||
private val arroyoDb by lazy { openLocalDatabase("arroyo") }
|
|
||||||
|
|
||||||
private inline fun <T> SQLiteDatabase.performOperation(crossinline query: SQLiteDatabase.() -> T?): T? {
|
private inline fun <T> SQLiteDatabase.performOperation(crossinline query: SQLiteDatabase.() -> T?): T? {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
@ -56,7 +84,7 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val dmOtherParticipantCache by lazy {
|
private val dmOtherParticipantCache by lazy {
|
||||||
(arroyoDb?.performOperation {
|
(useDatabase(DatabaseType.ARROYO)?.performOperation {
|
||||||
safeRawQuery(
|
safeRawQuery(
|
||||||
"SELECT client_conversation_id, conversation_type, user_id FROM user_conversation WHERE user_id != ?",
|
"SELECT client_conversation_id, conversation_type, user_id FROM user_conversation WHERE user_id != ?",
|
||||||
arrayOf(myUserId)
|
arrayOf(myUserId)
|
||||||
@ -79,49 +107,27 @@ class DatabaseAccess(
|
|||||||
} ?: emptyMap()).toMutableMap()
|
} ?: emptyMap()).toMutableMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openLocalDatabase(databaseName: String, writeMode: Boolean = false): SQLiteDatabase? {
|
fun hasMain(): Boolean = useDatabase(DatabaseType.MAIN)?.isOpen == true
|
||||||
val dbPath = context.androidContext.getDatabasePath(DATABASES[databaseName]!!)
|
fun hasArroyo(): Boolean = useDatabase(DatabaseType.ARROYO)?.isOpen == true
|
||||||
if (!dbPath.exists()) return null
|
|
||||||
return runCatching {
|
|
||||||
SQLiteDatabase.openDatabase(
|
|
||||||
dbPath,
|
|
||||||
OpenParams.Builder()
|
|
||||||
.setOpenFlags(
|
|
||||||
if (writeMode) SQLiteDatabase.OPEN_READWRITE or SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING
|
|
||||||
else SQLiteDatabase.OPEN_READONLY
|
|
||||||
)
|
|
||||||
.setErrorHandler {
|
|
||||||
context.androidContext.deleteDatabase(dbPath.absolutePath)
|
|
||||||
context.softRestartApp()
|
|
||||||
}.build()
|
|
||||||
)
|
|
||||||
}.onFailure {
|
|
||||||
context.log.error("Failed to open database $databaseName!", it)
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasMain(): Boolean = mainDb?.isOpen == true
|
|
||||||
fun hasArroyo(): Boolean = arroyoDb?.isOpen == true
|
|
||||||
|
|
||||||
override fun init() {
|
override fun init() {
|
||||||
// perform integrity check on databases
|
// perform integrity check on databases
|
||||||
DATABASES.forEach { (name, fileName) ->
|
DatabaseType.entries.forEach { type ->
|
||||||
openLocalDatabase(name, writeMode = true)?.apply {
|
useDatabase(type, writeMode = true)?.apply {
|
||||||
rawQuery("PRAGMA integrity_check", null).use { query ->
|
rawQuery("PRAGMA integrity_check", null).use { query ->
|
||||||
if (!query.moveToFirst() || query.getString(0).lowercase() != "ok") {
|
if (!query.moveToFirst() || query.getString(0).lowercase() != "ok") {
|
||||||
context.log.error("Failed to perform integrity check on $fileName")
|
context.log.error("Failed to perform integrity check on ${type.fileName}")
|
||||||
context.androidContext.deleteDatabase(fileName)
|
context.androidContext.deleteDatabase(type.fileName)
|
||||||
return@apply
|
return@apply
|
||||||
}
|
}
|
||||||
context.log.verbose("database $fileName integrity check passed")
|
context.log.verbose("database ${type.fileName} integrity check passed")
|
||||||
}
|
}
|
||||||
}?.close()
|
}?.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun finalize() {
|
fun finalize() {
|
||||||
mainDb?.close()
|
openedDatabases.values.forEach { it.close() }
|
||||||
arroyoDb?.close()
|
|
||||||
context.log.verbose("Database closed")
|
context.log.verbose("Database closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,7 +149,7 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getFeedEntryByUserId(userId: String): FriendFeedEntry? {
|
fun getFeedEntryByUserId(userId: String): FriendFeedEntry? {
|
||||||
return mainDb?.performOperation {
|
return useDatabase(DatabaseType.MAIN)?.performOperation {
|
||||||
readDatabaseObject(
|
readDatabaseObject(
|
||||||
FriendFeedEntry(),
|
FriendFeedEntry(),
|
||||||
"FriendsFeedView",
|
"FriendsFeedView",
|
||||||
@ -155,7 +161,7 @@ class DatabaseAccess(
|
|||||||
|
|
||||||
val myUserId by lazy {
|
val myUserId by lazy {
|
||||||
context.androidContext.getSharedPreferences("user_session_shared_pref", 0).getString("key_user_id", null) ?:
|
context.androidContext.getSharedPreferences("user_session_shared_pref", 0).getString("key_user_id", null) ?:
|
||||||
arroyoDb?.performOperation {
|
useDatabase(DatabaseType.ARROYO)?.performOperation {
|
||||||
safeRawQuery(buildString {
|
safeRawQuery(buildString {
|
||||||
append("SELECT value FROM required_values WHERE key = 'USERID'")
|
append("SELECT value FROM required_values WHERE key = 'USERID'")
|
||||||
}, null)?.use { query ->
|
}, null)?.use { query ->
|
||||||
@ -168,7 +174,7 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? {
|
fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? {
|
||||||
return mainDb?.performOperation {
|
return useDatabase(DatabaseType.MAIN)?.performOperation {
|
||||||
readDatabaseObject(
|
readDatabaseObject(
|
||||||
FriendFeedEntry(),
|
FriendFeedEntry(),
|
||||||
"FriendsFeedView",
|
"FriendsFeedView",
|
||||||
@ -179,7 +185,7 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getFriendInfo(userId: String): FriendInfo? {
|
fun getFriendInfo(userId: String): FriendInfo? {
|
||||||
return mainDb?.performOperation {
|
return useDatabase(DatabaseType.MAIN)?.performOperation {
|
||||||
readDatabaseObject(
|
readDatabaseObject(
|
||||||
FriendInfo(),
|
FriendInfo(),
|
||||||
"FriendWithUsername",
|
"FriendWithUsername",
|
||||||
@ -190,7 +196,7 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getAllFriends(): List<FriendInfo> {
|
fun getAllFriends(): List<FriendInfo> {
|
||||||
return mainDb?.performOperation {
|
return useDatabase(DatabaseType.MAIN)?.performOperation {
|
||||||
safeRawQuery(
|
safeRawQuery(
|
||||||
"SELECT * FROM FriendWithUsername",
|
"SELECT * FROM FriendWithUsername",
|
||||||
null
|
null
|
||||||
@ -209,7 +215,7 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getFeedEntries(limit: Int): List<FriendFeedEntry> {
|
fun getFeedEntries(limit: Int): List<FriendFeedEntry> {
|
||||||
return mainDb?.performOperation {
|
return useDatabase(DatabaseType.MAIN)?.performOperation {
|
||||||
safeRawQuery(
|
safeRawQuery(
|
||||||
"SELECT * FROM FriendsFeedView ORDER BY _id LIMIT ?",
|
"SELECT * FROM FriendsFeedView ORDER BY _id LIMIT ?",
|
||||||
arrayOf(limit.toString())
|
arrayOf(limit.toString())
|
||||||
@ -228,7 +234,7 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getConversationMessageFromId(clientMessageId: Long): ConversationMessage? {
|
fun getConversationMessageFromId(clientMessageId: Long): ConversationMessage? {
|
||||||
return arroyoDb?.performOperation {
|
return useDatabase(DatabaseType.ARROYO)?.performOperation {
|
||||||
readDatabaseObject(
|
readDatabaseObject(
|
||||||
ConversationMessage(),
|
ConversationMessage(),
|
||||||
"conversation_message",
|
"conversation_message",
|
||||||
@ -239,7 +245,7 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getConversationType(conversationId: String): Int? {
|
fun getConversationType(conversationId: String): Int? {
|
||||||
return arroyoDb?.performOperation {
|
return useDatabase(DatabaseType.ARROYO)?.performOperation {
|
||||||
safeRawQuery(
|
safeRawQuery(
|
||||||
"SELECT conversation_type FROM user_conversation WHERE client_conversation_id = ?",
|
"SELECT conversation_type FROM user_conversation WHERE client_conversation_id = ?",
|
||||||
arrayOf(conversationId)
|
arrayOf(conversationId)
|
||||||
@ -253,7 +259,7 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getConversationLinkFromUserId(userId: String): UserConversationLink? {
|
fun getConversationLinkFromUserId(userId: String): UserConversationLink? {
|
||||||
return arroyoDb?.performOperation {
|
return useDatabase(DatabaseType.ARROYO)?.performOperation {
|
||||||
readDatabaseObject(
|
readDatabaseObject(
|
||||||
UserConversationLink(),
|
UserConversationLink(),
|
||||||
"user_conversation",
|
"user_conversation",
|
||||||
@ -265,7 +271,7 @@ class DatabaseAccess(
|
|||||||
|
|
||||||
fun getDMOtherParticipant(conversationId: String): String? {
|
fun getDMOtherParticipant(conversationId: String): String? {
|
||||||
if (dmOtherParticipantCache.containsKey(conversationId)) return dmOtherParticipantCache[conversationId]
|
if (dmOtherParticipantCache.containsKey(conversationId)) return dmOtherParticipantCache[conversationId]
|
||||||
return arroyoDb?.performOperation {
|
return useDatabase(DatabaseType.ARROYO)?.performOperation {
|
||||||
safeRawQuery(
|
safeRawQuery(
|
||||||
"SELECT user_id FROM user_conversation WHERE client_conversation_id = ? AND conversation_type = 0",
|
"SELECT user_id FROM user_conversation WHERE client_conversation_id = ? AND conversation_type = 0",
|
||||||
arrayOf(conversationId)
|
arrayOf(conversationId)
|
||||||
@ -284,14 +290,14 @@ class DatabaseAccess(
|
|||||||
|
|
||||||
|
|
||||||
fun getStoryEntryFromId(storyId: String): StoryEntry? {
|
fun getStoryEntryFromId(storyId: String): StoryEntry? {
|
||||||
return mainDb?.performOperation {
|
return useDatabase(DatabaseType.MAIN)?.performOperation {
|
||||||
readDatabaseObject(StoryEntry(), "Story", "storyId = ?", arrayOf(storyId))
|
readDatabaseObject(StoryEntry(), "Story", "storyId = ?", arrayOf(storyId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConversationParticipants(conversationId: String): List<String>? {
|
fun getConversationParticipants(conversationId: String): List<String>? {
|
||||||
if (dmOtherParticipantCache[conversationId] != null) return dmOtherParticipantCache[conversationId]?.let { listOf(myUserId, it) }
|
if (dmOtherParticipantCache[conversationId] != null) return dmOtherParticipantCache[conversationId]?.let { listOf(myUserId, it) }
|
||||||
return arroyoDb?.performOperation {
|
return useDatabase(DatabaseType.ARROYO)?.performOperation {
|
||||||
safeRawQuery(
|
safeRawQuery(
|
||||||
"SELECT user_id, conversation_type FROM user_conversation WHERE client_conversation_id = ?",
|
"SELECT user_id, conversation_type FROM user_conversation WHERE client_conversation_id = ?",
|
||||||
arrayOf(conversationId)
|
arrayOf(conversationId)
|
||||||
@ -321,7 +327,7 @@ class DatabaseAccess(
|
|||||||
conversationId: String,
|
conversationId: String,
|
||||||
limit: Int
|
limit: Int
|
||||||
): List<ConversationMessage>? {
|
): List<ConversationMessage>? {
|
||||||
return arroyoDb?.performOperation {
|
return useDatabase(DatabaseType.ARROYO)?.performOperation {
|
||||||
safeRawQuery(
|
safeRawQuery(
|
||||||
"SELECT * FROM conversation_message WHERE client_conversation_id = ? ORDER BY creation_timestamp DESC LIMIT ?",
|
"SELECT * FROM conversation_message WHERE client_conversation_id = ? ORDER BY creation_timestamp DESC LIMIT ?",
|
||||||
arrayOf(conversationId, limit.toString())
|
arrayOf(conversationId, limit.toString())
|
||||||
@ -341,7 +347,7 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getAddSource(userId: String): String? {
|
fun getAddSource(userId: String): String? {
|
||||||
return mainDb?.performOperation {
|
return useDatabase(DatabaseType.MAIN)?.performOperation {
|
||||||
rawQuery(
|
rawQuery(
|
||||||
"SELECT addSource FROM FriendWhoAddedMe WHERE userId = ?",
|
"SELECT addSource FROM FriendWhoAddedMe WHERE userId = ?",
|
||||||
arrayOf(userId)
|
arrayOf(userId)
|
||||||
@ -355,7 +361,7 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun markFriendStoriesAsSeen(userId: String) {
|
fun markFriendStoriesAsSeen(userId: String) {
|
||||||
openLocalDatabase("main", writeMode = true)?.apply {
|
useDatabase(DatabaseType.MAIN, writeMode = true)?.apply {
|
||||||
performOperation {
|
performOperation {
|
||||||
execSQL("UPDATE StorySnap SET viewed = 1 WHERE userId = ?", arrayOf(userId))
|
execSQL("UPDATE StorySnap SET viewed = 1 WHERE userId = ?", arrayOf(userId))
|
||||||
}
|
}
|
||||||
@ -364,7 +370,7 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getAccessTokens(userId: String): Map<String, String>? {
|
fun getAccessTokens(userId: String): Map<String, String>? {
|
||||||
return mainDb?.performOperation {
|
return useDatabase(DatabaseType.MAIN)?.performOperation {
|
||||||
rawQuery(
|
rawQuery(
|
||||||
"SELECT accessTokensPb FROM SnapToken WHERE userId = ?",
|
"SELECT accessTokensPb FROM SnapToken WHERE userId = ?",
|
||||||
arrayOf(userId)
|
arrayOf(userId)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user