feat(app/settings): message logger export & clear
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -86,6 +87,22 @@ class SettingsSection : Section() {
|
||||
context.androidContext.startActivity(intent)
|
||||
}
|
||||
|
||||
@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(
|
||||
@@ -99,16 +116,106 @@ class SettingsSection : Section() {
|
||||
launchActionIntent(enumAction)
|
||||
}
|
||||
}
|
||||
|
||||
RowTitle(title = "Clear Files")
|
||||
BridgeFileType.entries.forEach { fileType ->
|
||||
RowAction(title = fileType.displayName, requireConfirmation = true) {
|
||||
runCatching {
|
||||
fileType.resolve(context.androidContext).delete()
|
||||
context.longToast("Deleted ${fileType.displayName}!")
|
||||
}.onFailure {
|
||||
context.longToast("Failed to delete ${fileType.displayName}!")
|
||||
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 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 {
|
||||
selectedFileType.resolve(context.androidContext).delete()
|
||||
}.onFailure {
|
||||
context.log.error("Failed to clear file", it)
|
||||
context.longToast("Failed to clear file! ${it.localizedMessage}")
|
||||
}.onSuccess {
|
||||
context.shortToast("Done!")
|
||||
}
|
||||
}) {
|
||||
Text(text = "Clear")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user