srctree

Akito parent a8c6b8d8 9dfd2f86
Implement support for exporting the chat history

inlinesplit
WORKSPACE added: 126, removed: 10, total 116
@@ -177,6 +177,10 @@ maven_install(
"androidx.room:room-runtime:2.2.6",
"androidx.room:room-testing:2.2.6",
"androidx.test.ext:junit:1.1.2",
"com.fasterxml.jackson.core:jackson-annotations:2.13.4",
"com.fasterxml.jackson.core:jackson-core:2.13.4",
"com.fasterxml.jackson.core:jackson-databind:2.13.4",
"com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4",
"com.google.android.material:material:1.4.0",
"com.google.code.gson:gson:2.8.6",
"com.google.guava:guava:19.0",
 
atox/src/main/kotlin/ui/chat/ChatFragment.kt added: 126, removed: 10, total 116
@@ -10,6 +10,7 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.net.Uri
import android.os.Build
import android.os.Bundle
@@ -23,6 +24,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import androidx.core.os.ConfigurationCompat
import androidx.core.os.bundleOf
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
@@ -50,7 +52,8 @@ import ltd.evilcorp.domain.tox.PublicKey
import java.io.File
import java.net.URLConnection
import java.text.DateFormat
import java.util.Locale
import java.text.SimpleDateFormat
import java.util.*
 
const val CONTACT_PUBLIC_KEY = "publicKey"
const val FOCUS_ON_MESSAGE_BOX = "focusOnMessageBox"
@@ -71,6 +74,12 @@ class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::infl
private var selectedFt: Int = Int.MIN_VALUE
private var fts: List<FileTransfer> = listOf()
 
private val exportBackupLauncher =
registerForActivityResult(ActivityResultContracts.CreateDocument("application/json")) { dest ->
if (dest == null) return@registerForActivityResult
viewModel.backupHistory(contactPubKey, dest)
}
 
private val exportFtLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument()) { dest ->
if (dest == null) return@registerForActivityResult
viewModel.exportFt(selectedFt, dest)
@@ -145,6 +154,18 @@ class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::infl
toolbar.inflateMenu(R.menu.chat_options_menu)
toolbar.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.backup_history -> {
exportBackupLauncher.launch(
"backup-atox-${"messages" /* TODO @Akito: Put in Helper object. */}_${contactPubKey}_${
SimpleDateFormat(
"""yyyy-MM-dd'T'HH-mm-ss""",
ConfigurationCompat
.getLocales(Resources.getSystem().configuration).get(0),
).format(Date())
}.json",
)
true
}
R.id.clear_history -> {
AlertDialog.Builder(requireContext())
.setTitle(R.string.clear_history)
 
atox/src/main/kotlin/ui/chat/ChatViewModel.kt added: 126, removed: 10, total 116
@@ -14,7 +14,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
@@ -33,6 +32,7 @@ import ltd.evilcorp.domain.feature.CallManager
import ltd.evilcorp.domain.feature.CallState
import ltd.evilcorp.domain.feature.ChatManager
import ltd.evilcorp.domain.feature.ContactManager
import ltd.evilcorp.domain.feature.ExportManager
import ltd.evilcorp.domain.feature.FileTransferManager
import ltd.evilcorp.domain.tox.PublicKey
import java.io.File
@@ -50,6 +50,7 @@ enum class CallAvailability {
class ChatViewModel @Inject constructor(
private val callManager: CallManager,
private val chatManager: ChatManager,
private val exportManager: ExportManager,
private val contactManager: ContactManager,
private val fileTransferManager: FileTransferManager,
private val notificationHelper: NotificationHelper,
@@ -156,6 +157,33 @@ class ChatViewModel @Inject constructor(
}
}
 
fun backupHistory(publicKey: String, locationSave: Uri) = scope.launch {
val backupContent = exportManager.generateExportMessagesJString(publicKey)
launch(Dispatchers.IO) {
try {
resolver.openOutputStream(locationSave).use { os ->
backupContent.byteInputStream().copyTo(os!!)
}
withContext(Dispatchers.Main) {
Toast.makeText(
context,
R.string.backup_history_success,
Toast.LENGTH_LONG,
).show()
}
} catch (e: Exception) {
Log.e(TAG, e.toString())
withContext(Dispatchers.Main) {
Toast.makeText(
context,
"${R.string.backup_history_failure} : " + e.message,
Toast.LENGTH_LONG,
).show()
}
}
}
}
 
fun setDraft(draft: String) = contactManager.setDraft(publicKey, draft)
fun clearDraft() = setDraft("")
 
 
atox/src/main/res/menu/chat_options_menu.xml added: 126, removed: 10, total 116
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/call" android:title="@string/call"/>
<item android:id="@+id/backup_history" android:title="@string/backup_history"/>
<item android:id="@+id/clear_history" android:title="@string/clear_history"/>
</menu>
 
atox/src/main/res/values/strings.xml added: 126, removed: 10, total 116
@@ -187,4 +187,7 @@
<string name="share">Share</string>
<string name="mark_as_read">Mark as read</string>
<string name="file_not_found">File not found</string>
<string name="backup_history">Backup history</string>
<string name="backup_history_success">Message history exported</string>
<string name="backup_history_failure">Message history export failed</string>
</resources>
No newline at end of file
 
domain/BUILD.bazel added: 126, removed: 10, total 116
@@ -30,6 +30,10 @@ kt_android_library(
":tox4j",
"//core",
"@maven//:androidx_core_core_ktx",
"@maven//:com_fasterxml_jackson_core_jackson_annotations",
"@maven//:com_fasterxml_jackson_core_jackson_core",
"@maven//:com_fasterxml_jackson_core_jackson_databind",
"@maven//:com_fasterxml_jackson_module_jackson_module_kotlin",
],
)
 
 
domain/build.gradle.kts added: 126, removed: 10, total 116
@@ -78,6 +78,12 @@ dependencies {
api(libs.tox4j.api)
implementation(libs.tox4j.c)
 
// JSON
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("com.fasterxml.jackson.core:jackson-databind:2.13.4")
implementation("com.fasterxml.jackson.core:jackson-core:2.13.4")
implementation("com.fasterxml.jackson.core:jackson-annotations:2.13.4")
 
testImplementation(libs.test.junit.core)
androidTestImplementation(libs.test.runner)
androidTestImplementation(libs.test.junit.ext)
 
filename was Deleted added: 126, removed: 10, total 116
@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2022 Akito <the@akito.ooo>
//
// SPDX-License-Identifier: GPL-3.0-only
 
package ltd.evilcorp.domain.feature
 
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import ltd.evilcorp.core.repository.MessageRepository
import ltd.evilcorp.core.vo.Message
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject
 
class ExportManager @Inject constructor(
private val messageRepository: MessageRepository,
) {
 
private fun List<Message>.generateExportMessages(): ExportMessages {
return ExportMessages(
version = 1, // TODO @Akito: Increment version programmatically on major changes.
timestamp = SimpleDateFormat(
"""yyyy-MM-dd'T'HH-mm-ss""",
Locale.getDefault(),
).format(Date()),
entries = this,
)
}
 
private fun generateExportMessagesJString(messages: List<Message>): String =
jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(messages.generateExportMessages())
 
private fun getMessages(publicKey: String): List<Message> = runBlocking {
messageRepository.get(publicKey).first()
}
 
fun generateExportMessagesJString(publicKey: String): String =
generateExportMessagesJString(
getMessages(publicKey),
)
}
 
data class ExportMessages(
val version: Int,
val timestamp: String,
val entries: List<Message>,
)