commit 7dd4241be0c2b1e0d4e108a9148134aeaa9c6270
parent c420d22c04567bdbd70ff5e9b259326c70367d85
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Sat, 16 Dec 2023 16:52:00 +0100
perf(core): chat exporter
- feat compose ui
Diffstat:
4 files changed, 330 insertions(+), 165 deletions(-)
diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json
@@ -910,12 +910,16 @@
},
"chat_export": {
- "select_export_format": "Select the Export Format",
- "select_media_type": "Select Media Types to export",
- "select_amount_of_messages": "Select the amount of messages to export (leave empty for all)",
- "select_conversation": "Select a Conversation to export",
+ "exporter_dialog": {
+ "select_conversations_title": "Select Conversations",
+ "text_field_selection": "{amount} selected",
+ "text_field_selection_all": "All",
+ "export_file_format_title": "Export File Format",
+ "message_type_filter_title": "Filter Messages by Type",
+ "amount_of_messages_title": "Amount of Messages (leave it blank for all)",
+ "download_medias_title": "Download Medias"
+ },
"dialog_negative_button": "Cancel",
- "dialog_neutral_button": "Export All",
"dialog_positive_button": "Export",
"exported_to": "Exported to {path}",
"exporting_chats": "Exporting Chats...",
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportChatMessages.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportChatMessages.kt
@@ -3,29 +3,46 @@ package me.rhunk.snapenhance.core.action.impl
import android.app.AlertDialog
import android.content.DialogInterface
import android.os.Environment
-import android.text.InputType
-import android.widget.EditText
+import android.view.WindowManager
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
import kotlinx.coroutines.*
import me.rhunk.snapenhance.common.data.ContentType
import me.rhunk.snapenhance.common.database.impl.FriendFeedEntry
+import me.rhunk.snapenhance.common.ui.createComposeView
import me.rhunk.snapenhance.core.action.AbstractAction
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
import me.rhunk.snapenhance.core.logger.CoreLogger
import me.rhunk.snapenhance.core.messaging.ConversationExporter
import me.rhunk.snapenhance.core.messaging.ExportFormat
+import me.rhunk.snapenhance.core.messaging.ExportParams
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.core.wrapper.impl.Message
import java.io.File
import kotlin.math.absoluteValue
class ExportChatMessages : AbstractAction() {
+ private val translation by lazy { context.translation.getCategory("chat_export") }
private val dialogLogs = mutableListOf<String>()
private var currentActionDialog: AlertDialog? = null
- private var exportType: ExportFormat? = null
- private var mediaToDownload: List<ContentType>? = null
- private var amountOfMessages: Int? = null
-
private fun logDialog(message: String) {
context.runOnUiThread {
if (dialogLogs.size > 10) dialogLogs.removeAt(0)
@@ -41,101 +58,221 @@ class ExportChatMessages : AbstractAction() {
}
}
- private suspend fun askExportType() = suspendCancellableCoroutine { cont ->
- context.runOnUiThread {
- ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
- .setTitle(context.translation["chat_export.select_export_format"])
- .setItems(ExportFormat.entries.map { it.name }.toTypedArray()) { _, which ->
- cont.resumeWith(Result.success(ExportFormat.entries[which]))
- }
- .setOnCancelListener {
- cont.resumeWith(Result.success(null))
- }
- .show()
+ @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
+ @Composable
+ private fun ExporterDialog(
+ getDialog: () -> AlertDialog? = { null }
+ ) {
+ val exporterTranslation = remember {
+ translation.getCategory("exporter_dialog")
}
- }
- private suspend fun askAmountOfMessages() = suspendCancellableCoroutine { cont ->
- context.coroutineScope.launch(Dispatchers.Main) {
- val input = EditText(context.mainActivity)
- input.inputType = InputType.TYPE_CLASS_NUMBER
- input.setSingleLine()
- input.maxLines = 1
+ var feedEntries by remember { mutableStateOf(emptyList<FriendFeedEntry>()) }
+ var exportType by remember { mutableStateOf(ExportFormat.HTML) }
+ val selectedFeedEntries = remember { mutableStateListOf<FriendFeedEntry>() }
+ val messageTypeFilter = remember { mutableStateListOf<ContentType>() }
+ var amountOfMessages by remember { mutableIntStateOf(-1) }
+ var downloadMedias by remember { mutableStateOf(false) }
- ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
- .setTitle(context.translation["chat_export.select_amount_of_messages"])
- .setView(input)
- .setPositiveButton(context.translation["button.ok"]) { _, _ ->
- cont.resumeWith(Result.success(input.text.takeIf { it.isNotEmpty() }?.toString()?.toIntOrNull()?.absoluteValue))
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(remember { ScrollState(0) })
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Text(exporterTranslation["select_conversations_title"])
+ run {
+ var expanded by remember { mutableStateOf(false) }
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ ) {
+ TextField(
+ value = selectedFeedEntries.let {
+ exporterTranslation.format("text_field_selection", "amount" to it.size.toString())
+ },
+ onValueChange = {},
+ readOnly = true,
+ singleLine = true,
+ modifier = Modifier.menuAnchor()
+ )
+
+ ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
+ LazyColumn(
+ modifier = Modifier.size(LocalConfiguration.current.screenWidthDp.dp, 300.dp)
+ ) {
+ items(feedEntries) { feedEntry ->
+ DropdownMenuItem(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {
+ if (selectedFeedEntries.contains(feedEntry)) selectedFeedEntries -= feedEntry
+ else selectedFeedEntries += feedEntry
+ },
+ text = {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Checkbox(checked = selectedFeedEntries.contains(feedEntry), onCheckedChange = null)
+ Text(
+ text = feedEntry.feedDisplayName ?: feedEntry.friendDisplayName ?: "unknown",
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1
+ )
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+
+ Text(exporterTranslation["export_file_format_title"])
+ run {
+ var expanded by remember { mutableStateOf(false) }
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ ) {
+ TextField(
+ value = exportType.extension,
+ onValueChange = {},
+ readOnly = true,
+ modifier = Modifier.menuAnchor()
+ )
+
+ ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
+ ExportFormat.entries.forEach { exportFormat ->
+ DropdownMenuItem(onClick = {
+ exportType = exportFormat
+ expanded = false
+ }, text = {
+ Text(text = exportFormat.name)
+ })
+ }
+ }
}
- .setOnCancelListener {
- cont.resumeWith(Result.success(null))
+ }
+ Text(exporterTranslation["message_type_filter_title"])
+ run {
+ var expanded by remember { mutableStateOf(false) }
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ ) {
+ TextField(
+ value = messageTypeFilter.takeIf { it.isNotEmpty() }?.let {
+ exporterTranslation.format("text_field_selection", "amount" to it.size.toString())
+ } ?: exporterTranslation["text_field_selection_all"],
+ onValueChange = {},
+ readOnly = true,
+ modifier = Modifier.menuAnchor()
+ )
+
+ ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
+ arrayOf(
+ ContentType.CHAT,
+ ContentType.SNAP,
+ ContentType.EXTERNAL_MEDIA,
+ ContentType.NOTE,
+ ContentType.STICKER
+ ).forEach { contentType ->
+ DropdownMenuItem(onClick = {
+ if (messageTypeFilter.contains(contentType)) messageTypeFilter -= contentType
+ else messageTypeFilter += contentType
+ }, text = {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Checkbox(checked = messageTypeFilter.contains(contentType), onCheckedChange = null)
+ Text(text = contentType.name)
+ }
+ })
+ }
+ }
}
- .show()
- }
- }
+ }
- private suspend fun askMediaToDownload() = suspendCancellableCoroutine { cont ->
- context.runOnUiThread {
- val mediasToDownload = mutableListOf<ContentType>()
- val contentTypes = arrayOf(
- ContentType.CHAT,
- ContentType.SNAP,
- ContentType.EXTERNAL_MEDIA,
- ContentType.NOTE,
- ContentType.STICKER
+ Text(exporterTranslation["amount_of_messages_title"])
+ val focusManager = LocalFocusManager.current
+ val keyboard = LocalSoftwareKeyboardController.current
+ TextField(
+ value = amountOfMessages.takeIf { it != -1 }?.toString() ?: "",
+ onValueChange = { amountOfMessages = it.toIntOrNull()?.absoluteValue ?: -1 },
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done, keyboardType = KeyboardType.Number),
+ keyboardActions = KeyboardActions(
+ onDone = {
+ focusManager.clearFocus()
+ keyboard?.hide()
+ })
)
- ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
- .setTitle(context.translation["chat_export.select_media_type"])
- .setMultiChoiceItems(contentTypes.map { it.name }.toTypedArray(), BooleanArray(contentTypes.size) { false }) { _, which, isChecked ->
- val media = contentTypes[which]
- if (isChecked) {
- mediasToDownload.add(media)
- } else if (mediasToDownload.contains(media)) {
- mediasToDownload.remove(media)
- }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Checkbox(checked = downloadMedias, onCheckedChange = { downloadMedias = it })
+ Text(exporterTranslation["download_medias_title"])
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ ) {
+ Button(
+ onClick = { getDialog()?.dismiss() }
+ ) {
+ Text(text = translation["dialog_negative_button"])
}
- .setOnCancelListener {
- cont.resumeWith(Result.success(null))
+ Button(
+ enabled = selectedFeedEntries.isNotEmpty(),
+ onClick = {
+ exportChatForConversations(selectedFeedEntries, ExportParams(
+ exportFormat = exportType,
+ messageTypeFilter = messageTypeFilter.takeIf { it.isNotEmpty() },
+ amountOfMessages = amountOfMessages.takeIf { it != -1 },
+ downloadMedias = downloadMedias
+ ))
+ }
+ ) {
+ Text(text = translation["dialog_positive_button"])
}
- .setPositiveButton(context.translation["button.ok"]) { _, _ ->
- cont.resumeWith(Result.success(mediasToDownload))
+ }
+
+ LaunchedEffect(Unit) {
+ withContext(Dispatchers.IO) {
+ feedEntries = context.database.getFeedEntries(500)
}
- .show()
+ }
}
}
override fun run() {
context.coroutineScope.launch(Dispatchers.Main) {
- exportType = askExportType() ?: return@launch
- mediaToDownload = if (exportType == ExportFormat.HTML) askMediaToDownload() else null
- amountOfMessages = askAmountOfMessages()
-
- val friendFeedEntries = context.database.getFeedEntries(500)
- val selectedConversations = mutableListOf<FriendFeedEntry>()
-
+ lateinit var exporterDialog: AlertDialog
ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
- .setTitle(context.translation["chat_export.select_conversation"])
- .setMultiChoiceItems(
- friendFeedEntries.map { it.feedDisplayName ?: it.friendDisplayUsername!!.split("|").firstOrNull() }.toTypedArray(),
- BooleanArray(friendFeedEntries.size) { false }
- ) { _, which, isChecked ->
- if (isChecked) {
- selectedConversations.add(friendFeedEntries[which])
- } else if (selectedConversations.contains(friendFeedEntries[which])) {
- selectedConversations.remove(friendFeedEntries[which])
+ .setTitle(translation["select_conversation"])
+ .setView(createComposeView(context.mainActivity!!) {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.surface
+ ) {
+ ExporterDialog { exporterDialog }
}
+ })
+ .create().apply {
+ exporterDialog = this
+ setCanceledOnTouchOutside(false)
+ show()
+ window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
+ window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
}
- .setNegativeButton(context.translation["chat_export.dialog_negative_button"]) { dialog, _ ->
- dialog.dismiss()
- }
- .setNeutralButton(context.translation["chat_export.dialog_neutral_button"]) { _, _ ->
- exportChatForConversations(friendFeedEntries)
- }
- .setPositiveButton(context.translation["chat_export.dialog_positive_button"]) { _, _ ->
- exportChatForConversations(selectedConversations)
- }
- .show()
}
}
@@ -158,26 +295,72 @@ class ExportChatMessages : AbstractAction() {
emptyList()
}
- private suspend fun exportFullConversation(friendFeedEntry: FriendFeedEntry) {
+ private fun exportChatForConversations(
+ conversations: List<FriendFeedEntry>,
+ exportParams: ExportParams,
+ ) {
+ dialogLogs.clear()
+ val jobs = mutableListOf<Job>()
+
+ currentActionDialog = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
+ .setTitle(translation["exporting_chats"])
+ .setCancelable(false)
+ .setMessage("")
+ .create()
+
+ val conversationSize = translation.format("processing_chats", "amount" to conversations.size.toString())
+
+ logDialog(conversationSize)
+
+ context.coroutineScope.launch {
+ conversations.forEach { conversation ->
+ launch {
+ runCatching {
+ exportFullConversation(conversation, exportParams)
+ }.onFailure {
+ logDialog(translation.format("export_fail", "conversation" to conversation.key.toString()))
+ logDialog(it.stackTraceToString())
+ CoreLogger.xposedLog(it)
+ }
+ }.also { jobs.add(it) }
+ }
+ jobs.joinAll()
+ logDialog(translation["finished"])
+ }.also {
+ currentActionDialog?.setButton(DialogInterface.BUTTON_POSITIVE, translation["dialog_negative_button"]) { dialog, _ ->
+ it.cancel()
+ jobs.forEach { it.cancel() }
+ dialog.dismiss()
+ }
+ }
+
+ currentActionDialog!!.also {
+ it.setCanceledOnTouchOutside(false)
+ }.show()
+ }
+
+ private suspend fun exportFullConversation(
+ feedEntry: FriendFeedEntry,
+ exportParams: ExportParams,
+ ) {
//first fetch the first message
- val conversationId = friendFeedEntry.key!!
- val conversationName = friendFeedEntry.feedDisplayName ?: friendFeedEntry.friendDisplayName!!.split("|").lastOrNull() ?: "unknown"
- val conversationParticipants = context.database.getConversationParticipants(friendFeedEntry.key!!)
+ val conversationId = feedEntry.key!!
+ val conversationName = feedEntry.feedDisplayName ?: feedEntry.friendDisplayName!!.split("|").lastOrNull() ?: "unknown"
+ val conversationParticipants = context.database.getConversationParticipants(feedEntry.key!!)
?.mapNotNull {
context.database.getFriendInfo(it)
}?.associateBy { it.userId!! } ?: emptyMap()
val publicFolder = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "SnapEnhance").also { if (!it.exists()) it.mkdirs() }
- val outputFile = publicFolder.resolve("conversation_${conversationName}_${System.currentTimeMillis()}.${exportType!!.extension}")
+ val outputFile = publicFolder.resolve("conversation_${conversationName}_${System.currentTimeMillis()}.${exportParams.exportFormat.extension}")
- logDialog(context.translation.format("chat_export.exporting_message", "conversation" to conversationName))
+ logDialog(translation.format("exporting_message", "conversation" to conversationName))
val conversationExporter = ConversationExporter(
context = context,
- friendFeedEntry = friendFeedEntry,
+ friendFeedEntry = feedEntry,
conversationParticipants = conversationParticipants,
- exportFormat = exportType!!,
- messageTypeFilter = mediaToDownload,
+ exportParams = exportParams,
cacheFolder = publicFolder.resolve("cache").also { if (!it.exists()) it.mkdirs() },
outputFile = outputFile,
).apply { init(); printLog = {
@@ -187,17 +370,28 @@ class ExportChatMessages : AbstractAction() {
var foundMessageCount = 0
var lastMessageId = fetchMessagesPaginated(conversationId, Long.MAX_VALUE, amount = 1).firstOrNull()?.messageDescriptor?.messageId ?: run {
- logDialog(context.translation["chat_export.no_messages_found"])
+ logDialog(translation["no_messages_found"])
return
}
while (true) {
- val fetchedMessages = fetchMessagesPaginated(conversationId, lastMessageId, amount = 500)
+ val fetchedMessages = fetchMessagesPaginated(conversationId, lastMessageId, amount = 500).toMutableList()
if (fetchedMessages.isEmpty()) break
+
+ fetchedMessages.firstOrNull()?.let {
+ lastMessageId = it.messageDescriptor!!.messageId!!
+ }
+
+ exportParams.messageTypeFilter?.let { filter ->
+ fetchedMessages.removeIf { message ->
+ !filter.contains(message.messageContent?.contentType ?: return@removeIf false)
+ }
+ }
+
foundMessageCount += fetchedMessages.size
- if (amountOfMessages != null && foundMessageCount >= amountOfMessages!!) {
- fetchedMessages.subList(0, amountOfMessages!! - foundMessageCount).reversed().forEach { message ->
+ if (exportParams.amountOfMessages != null && foundMessageCount >= exportParams.amountOfMessages) {
+ fetchedMessages.reversed().subList(0, exportParams.amountOfMessages - (foundMessageCount - fetchedMessages.size)).forEach { message ->
conversationExporter.readMessage(message)
}
break
@@ -207,57 +401,15 @@ class ExportChatMessages : AbstractAction() {
conversationExporter.readMessage(message)
}
- fetchedMessages.firstOrNull()?.let {
- lastMessageId = it.messageDescriptor!!.messageId!!
- }
setStatus("Exporting (found ${foundMessageCount})")
}
- if (exportType == ExportFormat.HTML) conversationExporter.awaitDownload()
+ if (exportParams.exportFormat == ExportFormat.HTML) conversationExporter.awaitDownload()
conversationExporter.close()
- logDialog(context.translation["chat_export.writing_output"])
+ logDialog(translation["writing_output"])
dialogLogs.clear()
- logDialog("\n" + context.translation.format("chat_export.exported_to",
+ logDialog("\n" + translation.format("exported_to",
"path" to outputFile.absolutePath.toString()
) + "\n")
}
-
- private fun exportChatForConversations(conversations: List<FriendFeedEntry>) {
- dialogLogs.clear()
- val jobs = mutableListOf<Job>()
-
- currentActionDialog = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
- .setTitle(context.translation["chat_export.exporting_chats"])
- .setCancelable(false)
- .setMessage("")
- .create()
-
- val conversationSize = context.translation.format("chat_export.processing_chats", "amount" to conversations.size.toString())
-
- logDialog(conversationSize)
-
- context.coroutineScope.launch {
- conversations.forEach { conversation ->
- launch {
- runCatching {
- exportFullConversation(conversation)
- }.onFailure {
- logDialog(context.translation.format("chat_export.export_fail", "conversation" to conversation.key.toString()))
- logDialog(it.stackTraceToString())
- CoreLogger.xposedLog(it)
- }
- }.also { jobs.add(it) }
- }
- jobs.joinAll()
- logDialog(context.translation["chat_export.finished"])
- }.also {
- currentActionDialog?.setButton(DialogInterface.BUTTON_POSITIVE, context.translation["chat_export.dialog_negative_button"]) { dialog, _ ->
- it.cancel()
- jobs.forEach { it.cancel() }
- dialog.dismiss()
- }
- }
-
- currentActionDialog!!.show()
- }
}
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/ConversationExporter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/ConversationExporter.kt
@@ -34,8 +34,7 @@ class ConversationExporter(
private val context: ModContext,
private val friendFeedEntry: FriendFeedEntry,
private val conversationParticipants: Map<String, FriendInfo>,
- private val exportFormat: ExportFormat,
- private val messageTypeFilter: List<ContentType>? = null,
+ private val exportParams: ExportParams,
private val cacheFolder: File,
private val outputFile: File
) {
@@ -44,13 +43,13 @@ class ConversationExporter(
private val downloadThreadExecutor = Executors.newFixedThreadPool(4)
private val writeThreadExecutor = Executors.newSingleThreadExecutor()
- private val conversationJsonDataFile by lazy { cacheFolder.resolve("messages.json") }
+ private val conversationJsonDataFile by lazy { cacheFolder.resolve("messages_${friendFeedEntry.key}.json") }
private val jsonDataWriter by lazy { JsonWriter(conversationJsonDataFile.writer()) }
private val outputFileStream by lazy { outputFile.outputStream() }
private val participants = mutableMapOf<String, Int>()
fun init() {
- when (exportFormat) {
+ when (exportParams.exportFormat) {
ExportFormat.TEXT -> {
outputFileStream.write("Conversation id: ${friendFeedEntry.key}\n".toByteArray())
outputFileStream.write("Conversation name: ${friendFeedEntry.feedDisplayName}\n".toByteArray())
@@ -86,7 +85,7 @@ class ConversationExporter(
jsonDataWriter.name("messages").beginArray()
- if (exportFormat != ExportFormat.HTML) return
+ if (exportParams.exportFormat != ExportFormat.HTML) return
outputFileStream.write("""
<!DOCTYPE html>
<html>
@@ -158,7 +157,7 @@ class ConversationExporter(
}
fun readMessage(message: Message) {
- if (exportFormat == ExportFormat.TEXT) {
+ if (exportParams.exportFormat == ExportFormat.TEXT) {
val (displayName, senderUsername) = conversationParticipants[message.senderId.toString()]?.let {
it.displayName to it.mutableUsername
} ?: ("" to message.senderId.toString())
@@ -169,15 +168,10 @@ class ConversationExporter(
}
val contentType = message.messageContent?.contentType ?: return
- if (messageTypeFilter != null) {
- if (!messageTypeFilter.contains(contentType)) return
-
- if (contentType == ContentType.NOTE || contentType == ContentType.SNAP || contentType == ContentType.EXTERNAL_MEDIA) {
- downloadMedia(message)
- }
+ if (exportParams.downloadMedias && (contentType == ContentType.NOTE || contentType == ContentType.SNAP || contentType == ContentType.EXTERNAL_MEDIA)) {
+ downloadMedia(message)
}
-
jsonDataWriter.apply {
beginObject()
name("orderKey").value(message.orderKey)
@@ -236,20 +230,20 @@ class ConversationExporter(
}
fun close() {
- if (exportFormat != ExportFormat.TEXT) {
+ if (exportParams.exportFormat != ExportFormat.TEXT) {
jsonDataWriter.endArray()
jsonDataWriter.endObject()
jsonDataWriter.flush()
jsonDataWriter.close()
}
- if (exportFormat == ExportFormat.JSON) {
+ if (exportParams.exportFormat == ExportFormat.JSON) {
conversationJsonDataFile.inputStream().use {
it.copyTo(outputFileStream)
}
}
- if (exportFormat == ExportFormat.HTML) {
+ if (exportParams.exportFormat == ExportFormat.HTML) {
//write the json file
outputFileStream.write("<script type=\"application/json\" class=\"exported_content\">".toByteArray())
@@ -259,9 +253,14 @@ class ConversationExporter(
android.util.Base64.DEFAULT or android.util.Base64.NO_WRAP,
true
) as OutputStream).let { outputStream ->
- val deflateOutputStream = DeflaterOutputStream(outputStream, Deflater(Deflater.BEST_COMPRESSION, true), true)
- conversationJsonDataFile.inputStream().use {
- it.copyTo(deflateOutputStream)
+ val deflateOutputStream = DeflaterOutputStream(outputStream, Deflater(Deflater.BEST_SPEED, true), true)
+ conversationJsonDataFile.inputStream().use { fileInputStream ->
+ val buffer = ByteArray(4096)
+ var length: Int
+ while (fileInputStream.read(buffer).also { length = it } > 0) {
+ deflateOutputStream.write(buffer, 0, length)
+ deflateOutputStream.flush()
+ }
}
deflateOutputStream.finish()
outputStream.flush()
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/ExportParams.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/ExportParams.kt
@@ -0,0 +1,10 @@
+package me.rhunk.snapenhance.core.messaging
+
+import me.rhunk.snapenhance.common.data.ContentType
+
+class ExportParams(
+ val exportFormat: ExportFormat = ExportFormat.HTML,
+ val messageTypeFilter: List<ContentType>? = null,
+ val amountOfMessages: Int? = null,
+ val downloadMedias: Boolean = false,
+)