commit 15c56b705f99c27905d5719285ae7955858ac2ab
parent 780d5b98588453fe51e8b77203210a79283cff89
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Wed, 22 Nov 2023 20:41:03 +0100

feat(app/settings): message logger export & clear

Diffstat:
Mapp/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt | 4++++
Mapp/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt | 4+---
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt | 2+-
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/SettingsSection.kt | 139++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MessageLoggerWrapper.kt | 35+++++++++++++++++++++++++++--------
5 files changed, 156 insertions(+), 28 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt @@ -21,8 +21,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import me.rhunk.snapenhance.bridge.BridgeService import me.rhunk.snapenhance.common.BuildConfig +import me.rhunk.snapenhance.common.bridge.types.BridgeFileType import me.rhunk.snapenhance.common.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.common.bridge.wrapper.MappingsWrapper +import me.rhunk.snapenhance.common.bridge.wrapper.MessageLoggerWrapper import me.rhunk.snapenhance.common.config.ModConfig import me.rhunk.snapenhance.e2ee.E2EEImplementation import me.rhunk.snapenhance.messaging.ModDatabase @@ -67,6 +69,7 @@ class RemoteSideContext( val scriptManager = RemoteScriptManager(this) val settingsOverlay = SettingsOverlay(this) val e2eeImplementation = E2EEImplementation(this) + val messageLogger by lazy { MessageLoggerWrapper(androidContext.getDatabasePath(BridgeFileType.MESSAGE_LOGGER_DATABASE.fileName)) } //used to load bitmoji selfies and download previews val imageLoader by lazy { @@ -104,6 +107,7 @@ class RemoteSideContext( modDatabase.init() streaksReminder.init() scriptManager.init() + messageLogger.init() }.onFailure { log.error("Failed to load RemoteSideContext", it) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -20,7 +20,6 @@ import me.rhunk.snapenhance.download.DownloadProcessor import kotlin.system.measureTimeMillis class BridgeService : Service() { - private lateinit var messageLoggerWrapper: MessageLoggerWrapper private lateinit var remoteSideContext: RemoteSideContext lateinit var syncCallback: SyncCallback var messagingBridge: MessagingBridge? = null @@ -38,7 +37,6 @@ class BridgeService : Service() { remoteSideContext.apply { bridgeService = this@BridgeService } - messageLoggerWrapper = MessageLoggerWrapper(getDatabasePath(BridgeFileType.MESSAGE_LOGGER_DATABASE.fileName)).also { it.init() } return BridgeBinder() } @@ -180,7 +178,7 @@ class BridgeService : Service() { override fun getScriptingInterface() = remoteSideContext.scriptManager override fun getE2eeInterface() = remoteSideContext.e2eeImplementation - override fun getMessageLogger() = messageLoggerWrapper + override fun getMessageLogger() = remoteSideContext.messageLogger override fun registerMessagingBridge(bridge: MessagingBridge) { messagingBridge = bridge } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt @@ -271,7 +271,7 @@ class HomeSection : Section() { homeSubSection.LogsSection() } composable(SETTINGS_SECTION_ROUTE) { - SettingsSection().also { it.context = context }.Content() + SettingsSection(activityLauncherHelper).also { it.context = context }.Content() } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/SettingsSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/SettingsSection.kt @@ -6,27 +6,28 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.OpenInNew -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog +import androidx.core.net.toUri +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import me.rhunk.snapenhance.common.Constants import me.rhunk.snapenhance.common.action.EnumAction import me.rhunk.snapenhance.common.bridge.types.BridgeFileType import me.rhunk.snapenhance.ui.manager.Section +import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper import me.rhunk.snapenhance.ui.util.AlertDialogs +import me.rhunk.snapenhance.ui.util.saveFile -class SettingsSection : Section() { +class SettingsSection( + private val activityLauncherHelper: ActivityLauncherHelper +) : Section() { private val dialogs by lazy { AlertDialogs(context.translation) } @Composable @@ -59,7 +60,7 @@ class SettingsSection : Section() { } } - Row( + ShiftedRow( modifier = Modifier .fillMaxWidth() .height(65.dp) @@ -87,6 +88,22 @@ class SettingsSection : Section() { } @Composable + private fun ShiftedRow( + modifier: Modifier = Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, + verticalAlignment: Alignment.Vertical = Alignment.Top, + content: @Composable RowScope.() -> Unit + ) { + Row( + modifier = modifier.padding(start = 26.dp), + horizontalArrangement = horizontalArrangement, + verticalAlignment = verticalAlignment + ) { content(this) } + } + + + @OptIn(ExperimentalMaterial3Api::class) + @Composable override fun Content() { Column( modifier = Modifier @@ -99,16 +116,106 @@ class SettingsSection : Section() { launchActionIntent(enumAction) } } + RowTitle(title = "Message Logger") + ShiftedRow { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + var storedMessagesCount by remember { mutableIntStateOf(0) } + LaunchedEffect(Unit) { + withContext(Dispatchers.IO) { + storedMessagesCount = context.messageLogger.getStoredMessageCount() + } + } + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(5.dp) + ) { + Text(text = "$storedMessagesCount messages", modifier = Modifier.weight(1f)) + Button(onClick = { + runCatching { + activityLauncherHelper.saveFile("message_logger.db", "application/octet-stream") { uri -> + context.androidContext.contentResolver.openOutputStream(uri.toUri())?.use { outputStream -> + context.messageLogger.databaseFile.inputStream().use { inputStream -> + inputStream.copyTo(outputStream) + } + } + } + }.onFailure { + context.log.error("Failed to export database", it) + context.longToast("Failed to export database! ${it.localizedMessage}") + } + }) { + Text(text = "Export") + } + Button(onClick = { + runCatching { + context.messageLogger.clearMessages() + storedMessagesCount = 0 + }.onFailure { + context.log.error("Failed to clear messages", it) + context.longToast("Failed to clear messages! ${it.localizedMessage}") + }.onSuccess { + context.shortToast("Done!") + } + }) { + Text(text = "Clear") + } + } + } + } - RowTitle(title = "Clear Files") - BridgeFileType.entries.forEach { fileType -> - RowAction(title = fileType.displayName, requireConfirmation = true) { + RowTitle(title = "Clear App Files") + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + var selectedFileType by remember { mutableStateOf(BridgeFileType.entries.first()) } + Box( + modifier = Modifier + .weight(1f) + .padding(start = 26.dp) + ) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + modifier = Modifier.fillMaxWidth(0.8f) + ) { + TextField( + value = selectedFileType.displayName, + onValueChange = {}, + readOnly = true, + modifier = Modifier.menuAnchor() + ) + + ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + BridgeFileType.entries.forEach { fileType -> + DropdownMenuItem(onClick = { + expanded = false + selectedFileType = fileType + }, text = { + Text(text = fileType.displayName) + }) + } + } + } + } + Button(onClick = { runCatching { - fileType.resolve(context.androidContext).delete() - context.longToast("Deleted ${fileType.displayName}!") + selectedFileType.resolve(context.androidContext).delete() }.onFailure { - context.longToast("Failed to delete ${fileType.displayName}!") + context.log.error("Failed to clear file", it) + context.longToast("Failed to clear file! ${it.localizedMessage}") + }.onSuccess { + context.shortToast("Done!") } + }) { + Text(text = "Clear") } } } diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MessageLoggerWrapper.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MessageLoggerWrapper.kt @@ -2,15 +2,18 @@ package me.rhunk.snapenhance.common.bridge.wrapper import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import kotlinx.coroutines.* import me.rhunk.snapenhance.bridge.MessageLoggerInterface import me.rhunk.snapenhance.common.util.SQLiteDatabaseHelper import java.io.File import java.util.UUID class MessageLoggerWrapper( - private val databaseFile: File + val databaseFile: File ): MessageLoggerInterface.Stub() { private var _database: SQLiteDatabase? = null + @OptIn(ExperimentalCoroutinesApi::class) + private val coroutineScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1)) private val database get() = synchronized(this) { _database?.takeIf { it.isOpen } ?: run { @@ -74,19 +77,35 @@ class MessageLoggerWrapper( if (state) { return false } - database.insert("messages", null, ContentValues().apply { - put("conversation_id", conversationId) - put("message_id", messageId) - put("message_data", serializedMessage) - }) + runBlocking { + withContext(coroutineScope.coroutineContext) { + database.insert("messages", null, ContentValues().apply { + put("conversation_id", conversationId) + put("message_id", messageId) + put("message_data", serializedMessage) + }) + } + } return true } fun clearMessages() { - database.execSQL("DELETE FROM messages") + coroutineScope.launch { + database.execSQL("DELETE FROM messages") + } + } + + fun getStoredMessageCount(): Int { + val cursor = database.rawQuery("SELECT COUNT(*) FROM messages", null) + cursor.moveToFirst() + val count = cursor.getInt(0) + cursor.close() + return count } override fun deleteMessage(conversationId: String, messageId: Long) { - database.execSQL("DELETE FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString())) + coroutineScope.launch { + database.execSQL("DELETE FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString())) + } } } \ No newline at end of file