diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt index 0f83c32c..4fae657c 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt @@ -55,6 +55,7 @@ class Routes( const val CONFIG_EXPORT_SUMMARY_ROUTE = "config_export_summary/?exportSensitiveData={exportSensitiveData}" const val FRIEND_TRACKER_CONFIG_EXPORT_ROUTE = "friend_tracker_config_export/?rule_id={rule_id}" const val FRIEND_TRACKER_CONFIG_IMPORT_ROUTE = "friend_tracker_config_import" + const val VIEW_LOGGER_HISTORY_ROUTE = "view_logger_history/{uri}" } lateinit var navController: NavController @@ -77,6 +78,7 @@ class Routes( val settings = route(RouteInfo("home_settings"), HomeSettings()).parent(home) val homeLogs = route(RouteInfo("home_logs"), HomeLogs()).parent(home) val loggerHistory = route(RouteInfo("logger_history"), LoggerHistoryRoot()).parent(home) + val viewLoggerHistory = route(RouteInfo(VIEW_LOGGER_HISTORY_ROUTE), LoggerHistoryRoot()).parent(home) val friendTracker = route(RouteInfo("friend_tracker", icon = Icons.Default.PersonSearch), FriendTrackerManagerRoot()).parent(home) val editRule = route(RouteInfo("edit_rule/?rule_id={rule_id}", hasOwnTopBar = true), EditRule()) val friendTrackerConfigExport = route(RouteInfo(FRIEND_TRACKER_CONFIG_EXPORT_ROUTE), me.rhunk.snapenhance.ui.manager.pages.tracker.FriendTrackerConfigExportScreen()) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/LoggerHistoryRoot.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/LoggerHistoryRoot.kt index 8483e894..79a0713b 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/LoggerHistoryRoot.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/LoggerHistoryRoot.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.net.toUri import androidx.navigation.NavBackStackEntry import com.google.gson.JsonParser import kotlinx.coroutines.Dispatchers @@ -46,6 +47,7 @@ import me.rhunk.snapenhance.core.features.impl.downloader.decoder.MessageDecoder import me.rhunk.snapenhance.download.DownloadProcessor import me.rhunk.snapenhance.storage.findFriend import me.rhunk.snapenhance.ui.manager.Routes +import java.net.URLDecoder import java.text.DateFormat import java.util.concurrent.ConcurrentHashMap import kotlin.math.absoluteValue @@ -216,9 +218,14 @@ class LoggerHistoryRoot : Routes.Route() { @OptIn(ExperimentalMaterial3Api::class) - override val content: @Composable (NavBackStackEntry) -> Unit = { + override val content: @Composable (NavBackStackEntry) -> Unit = { navBackStackEntry -> LaunchedEffect(Unit) { - loggerWrapper = LoggerWrapper(context.androidContext) + val uri = navBackStackEntry.arguments?.getString("uri")?.let { + runCatching { + URLDecoder.decode(it, "UTF-8").toUri() + }.getOrNull() + } + loggerWrapper = LoggerWrapper(context.androidContext, uri) } val conversationInfoCache = remember { ConcurrentHashMap() } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt index 92852217..5a36081a 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt @@ -46,6 +46,9 @@ import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper import me.rhunk.snapenhance.ui.util.AlertDialogs import me.rhunk.snapenhance.ui.util.openFile import me.rhunk.snapenhance.ui.util.saveFile +import java.io.File +import java.io.FileOutputStream +import java.net.URLEncoder import java.util.concurrent.TimeUnit class HomeSettings : Routes.Route() { @@ -417,6 +420,26 @@ class HomeSettings : Routes.Route() { }) { Text(text = translation["export_button"]) } + Button(onClick = { + runCatching { + activityLauncherHelper.openFile("application/octet-stream") { uri -> + val tempFile = File(context.androidContext.cacheDir, "view_message_logger.db") + context.androidContext.contentResolver.openInputStream(uri.toUri())?.use { inputStream -> + FileOutputStream(tempFile).use { outputStream -> + inputStream.copyTo(outputStream) + } + } + routes.viewLoggerHistory.navigate { + put("uri", URLEncoder.encode(tempFile.toUri().toString(), "UTF-8")) + } + } + }.onFailure { + context.log.error("Failed to open file", it) + context.longToast("Failed to open file! ${it.localizedMessage}") + } + }) { + Text(text = translation["view_button"]) + } Button(onClick = { runCatching { context.messageLogger.purgeAll() diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 4dee92a4..6264233a 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -74,6 +74,7 @@ "success_toast": "Done!", "message_logger_summary": "{messageCount} messages\n{storyCount} stories", "export_button": "Export", + "view_button": "View", "clear_button": "Clear", "backup_button": "Backup", "restore_button": "Restore", diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LoggerWrapper.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LoggerWrapper.kt index cd8764ef..d9b82455 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LoggerWrapper.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LoggerWrapper.kt @@ -3,6 +3,7 @@ package me.rhunk.snapenhance.common.bridge.wrapper import android.content.ContentValues import android.content.Context import android.database.sqlite.SQLiteDatabase +import android.net.Uri import com.google.gson.GsonBuilder import com.google.gson.JsonObject import kotlinx.coroutines.* @@ -70,9 +71,13 @@ data class TrackerLog( } class LoggerWrapper( - val databaseFile: File + val databaseFile: File, + private val readOnly: Boolean = false ): LoggerInterface.Stub() { - constructor(context: Context): this(File(context.getDatabasePath(InternalFileHandleType.MESSAGE_LOGGER.fileName).absolutePath)) + constructor(context: Context, uri: Uri? = null): this( + uri?.path?.let { File(it) } ?: File(context.getDatabasePath(InternalFileHandleType.MESSAGE_LOGGER.fileName).absolutePath), + uri != null + ) private var _database: SQLiteDatabase? = null @OptIn(ExperimentalCoroutinesApi::class) @@ -82,49 +87,52 @@ class LoggerWrapper( private val database get() = synchronized(this) { _database?.takeIf { it.isOpen } ?: run { _database?.close() - val openedDatabase = SQLiteDatabase.openDatabase(databaseFile.absolutePath, null, SQLiteDatabase.CREATE_IF_NECESSARY or SQLiteDatabase.OPEN_READWRITE) - SQLiteDatabaseHelper.createTablesFromSchema(openedDatabase, mapOf( - "messages" to listOf( - "id INTEGER PRIMARY KEY", - "message_id BIGINT", - "conversation_id VARCHAR", - "user_id CHAR(36)", - "username VARCHAR", - "send_timestamp BIGINT", - "added_timestamp BIGINT", - "group_title VARCHAR", - "message_data BLOB" - ), - "chat_edits" to listOf( - "id INTEGER PRIMARY KEY", - "edit_number INTEGER", - "added_timestamp BIGINT", - "conversation_id VARCHAR", - "message_id BIGINT", - "message_text BLOB" - ), - "stories" to listOf( - "id INTEGER PRIMARY KEY", - "added_timestamp BIGINT", - "user_id VARCHAR", - "posted_timestamp BIGINT", - "created_timestamp BIGINT", - "url VARCHAR", - "encryption_key BLOB", - "encryption_iv BLOB" - ), - "tracker_events" to listOf( - "id INTEGER PRIMARY KEY", - "timestamp BIGINT", - "conversation_id CHAR(36)", - "conversation_title VARCHAR", - "is_group BOOLEAN", - "username VARCHAR", - "user_id VARCHAR", - "event_type VARCHAR", - "data VARCHAR" - ) - )) + val dbFlags = if (readOnly) SQLiteDatabase.OPEN_READONLY else SQLiteDatabase.CREATE_IF_NECESSARY or SQLiteDatabase.OPEN_READWRITE + val openedDatabase = SQLiteDatabase.openDatabase(databaseFile.absolutePath, null, dbFlags) + if (!readOnly) { + SQLiteDatabaseHelper.createTablesFromSchema(openedDatabase, mapOf( + "messages" to listOf( + "id INTEGER PRIMARY KEY", + "message_id BIGINT", + "conversation_id VARCHAR", + "user_id CHAR(36)", + "username VARCHAR", + "send_timestamp BIGINT", + "added_timestamp BIGINT", + "group_title VARCHAR", + "message_data BLOB" + ), + "chat_edits" to listOf( + "id INTEGER PRIMARY KEY", + "edit_number INTEGER", + "added_timestamp BIGINT", + "conversation_id VARCHAR", + "message_id BIGINT", + "message_text BLOB" + ), + "stories" to listOf( + "id INTEGER PRIMARY KEY", + "added_timestamp BIGINT", + "user_id VARCHAR", + "posted_timestamp BIGINT", + "created_timestamp BIGINT", + "url VARCHAR", + "encryption_key BLOB", + "encryption_iv BLOB" + ), + "tracker_events" to listOf( + "id INTEGER PRIMARY KEY", + "timestamp BIGINT", + "conversation_id CHAR(36)", + "conversation_title VARCHAR", + "is_group BOOLEAN", + "username VARCHAR", + "user_id VARCHAR", + "event_type VARCHAR", + "data VARCHAR" + ) + )) + } _database = openedDatabase openedDatabase } diff --git a/native/build.gradle.kts b/native/build.gradle.kts index 8ed45c4c..edc870ee 100644 --- a/native/build.gradle.kts +++ b/native/build.gradle.kts @@ -161,4 +161,4 @@ val syncTasks = cargoTargets.mapIndexed { index, target -> tasks.named("preBuild").configure { syncTasks.forEach { dependsOn(it) } -} +} \ No newline at end of file