mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-04 07:25:25 +01:00
Give a media/no-media choice in labs plaintext export.
This commit is contained in:
@@ -47,6 +47,7 @@ object PlaintextExportRepository {
|
||||
threadId: Long,
|
||||
directoryUri: Uri,
|
||||
chatName: String,
|
||||
includeMedia: Boolean,
|
||||
progressListener: ProgressListener,
|
||||
cancellationSignal: CancellationSignal
|
||||
): Boolean {
|
||||
@@ -70,9 +71,13 @@ object PlaintextExportRepository {
|
||||
return false
|
||||
}
|
||||
|
||||
val mediaDir = chatDir.createDirectory("media") ?: run {
|
||||
Log.w(TAG, "Could not create media directory")
|
||||
return false
|
||||
val mediaDir = if (includeMedia) {
|
||||
chatDir.createDirectory("media") ?: run {
|
||||
Log.w(TAG, "Could not create media directory")
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val chatFile = chatDir.createFile("text/plain", "chat.txt") ?: run {
|
||||
@@ -117,11 +122,15 @@ object PlaintextExportRepository {
|
||||
for (message in batch) {
|
||||
if (cancellationSignal.isCancelled()) return false
|
||||
|
||||
writer.writeMessage(context, message, extraData, dateFormat, attachmentDateFormat, pendingAttachments)
|
||||
writer.writeMessage(context, message, extraData, dateFormat, attachmentDateFormat, pendingAttachments, includeMedia)
|
||||
writer.newLine()
|
||||
|
||||
messagesProcessed++
|
||||
progressListener.onProgress(messagesProcessed, stats.messageCount, 0, stats.attachmentCount)
|
||||
if (includeMedia) {
|
||||
progressListener.onProgress(messagesProcessed, stats.messageCount, 0, stats.attachmentCount)
|
||||
} else {
|
||||
progressListener.onProgress(messagesProcessed, stats.messageCount, 0, 0)
|
||||
}
|
||||
}
|
||||
eventTimer.emit("messages")
|
||||
}
|
||||
@@ -136,32 +145,34 @@ object PlaintextExportRepository {
|
||||
|
||||
// Attachments — use createFile directly (like LocalArchiver's FilesFileSystem) to avoid
|
||||
// the extra content resolver queries that newFile/findFile perform.
|
||||
val totalAttachments = pendingAttachments.size
|
||||
var attachmentsProcessed = 0
|
||||
for (pending in pendingAttachments) {
|
||||
if (cancellationSignal.isCancelled()) return false
|
||||
if (includeMedia && mediaDir != null) {
|
||||
val totalAttachments = pendingAttachments.size
|
||||
var attachmentsProcessed = 0
|
||||
for (pending in pendingAttachments) {
|
||||
if (cancellationSignal.isCancelled()) return false
|
||||
|
||||
try {
|
||||
val outputStream = mediaDir.createFile("application/octet-stream", pending.exportedName)?.let { it.outputStream(context) }
|
||||
if (outputStream == null) {
|
||||
Log.w(TAG, "Could not create attachment file: ${pending.exportedName}")
|
||||
attachmentsProcessed++
|
||||
progressListener.onProgress(stats.messageCount, stats.messageCount, attachmentsProcessed, totalAttachments)
|
||||
continue
|
||||
}
|
||||
|
||||
outputStream.use { out ->
|
||||
SignalDatabase.attachments.getAttachmentStream(pending.attachment.attachmentId, 0).use { input ->
|
||||
input.copyTo(out)
|
||||
try {
|
||||
val outputStream = mediaDir.createFile("application/octet-stream", pending.exportedName)?.let { it.outputStream(context) }
|
||||
if (outputStream == null) {
|
||||
Log.w(TAG, "Could not create attachment file: ${pending.exportedName}")
|
||||
attachmentsProcessed++
|
||||
progressListener.onProgress(stats.messageCount, stats.messageCount, attachmentsProcessed, totalAttachments)
|
||||
continue
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error exporting attachment: ${pending.exportedName}", e)
|
||||
}
|
||||
|
||||
attachmentsProcessed++
|
||||
progressListener.onProgress(stats.messageCount, stats.messageCount, attachmentsProcessed, totalAttachments)
|
||||
eventTimer.emit("media")
|
||||
outputStream.use { out ->
|
||||
SignalDatabase.attachments.getAttachmentStream(pending.attachment.attachmentId, 0).use { input ->
|
||||
input.copyTo(out)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error exporting attachment: ${pending.exportedName}", e)
|
||||
}
|
||||
|
||||
attachmentsProcessed++
|
||||
progressListener.onProgress(stats.messageCount, stats.messageCount, attachmentsProcessed, totalAttachments)
|
||||
eventTimer.emit("media")
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "[PlaintextExport] ${eventTimer.stop().summary}")
|
||||
@@ -222,7 +233,8 @@ object PlaintextExportRepository {
|
||||
extraData: ExtraMessageData,
|
||||
dateFormat: SimpleDateFormat,
|
||||
attachmentDateFormat: SimpleDateFormat,
|
||||
pendingAttachments: MutableList<PendingAttachment>
|
||||
pendingAttachments: MutableList<PendingAttachment>,
|
||||
includeMedia: Boolean
|
||||
) {
|
||||
val timestamp = dateFormat.format(Date(message.dateSent))
|
||||
|
||||
@@ -262,7 +274,7 @@ object PlaintextExportRepository {
|
||||
}
|
||||
|
||||
if (stickerAttachment != null) {
|
||||
this.writeSticker(stickerAttachment, prefix, hasQuote, attachmentDateFormat, pendingAttachments)
|
||||
this.writeSticker(stickerAttachment, prefix, hasQuote, attachmentDateFormat, pendingAttachments, includeMedia)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -282,7 +294,7 @@ object PlaintextExportRepository {
|
||||
}
|
||||
|
||||
val wrotePrefix = !body.isNullOrEmpty() || hasQuote
|
||||
this.writeAttachments(mainAttachments, prefix, wrotePrefix, attachmentDateFormat, pendingAttachments)
|
||||
this.writeAttachments(mainAttachments, prefix, wrotePrefix, attachmentDateFormat, pendingAttachments, includeMedia)
|
||||
}
|
||||
|
||||
private fun BufferedWriter.writeUpdateMessage(context: Context, message: MmsMessageRecord, timestamp: String) {
|
||||
@@ -323,15 +335,20 @@ object PlaintextExportRepository {
|
||||
prefix: String,
|
||||
hasQuote: Boolean,
|
||||
attachmentDateFormat: SimpleDateFormat,
|
||||
pendingAttachments: MutableList<PendingAttachment>
|
||||
pendingAttachments: MutableList<PendingAttachment>,
|
||||
includeMedia: Boolean
|
||||
) {
|
||||
val emoji = stickerAttachment.stickerLocator?.emoji ?: ""
|
||||
val exportedName = buildAttachmentFileName(stickerAttachment, attachmentDateFormat)
|
||||
pendingAttachments.add(PendingAttachment(stickerAttachment, exportedName))
|
||||
if (!hasQuote) {
|
||||
this.write(prefix)
|
||||
}
|
||||
this.write("(Sticker) $emoji [See: media/$exportedName]")
|
||||
if (includeMedia) {
|
||||
val exportedName = buildAttachmentFileName(stickerAttachment, attachmentDateFormat)
|
||||
pendingAttachments.add(PendingAttachment(stickerAttachment, exportedName))
|
||||
this.write("(Sticker) $emoji [See: media/$exportedName]")
|
||||
} else {
|
||||
this.write("(Sticker) $emoji")
|
||||
}
|
||||
this.newLine()
|
||||
}
|
||||
|
||||
@@ -340,23 +357,33 @@ object PlaintextExportRepository {
|
||||
prefix: String,
|
||||
wrotePrefix: Boolean,
|
||||
attachmentDateFormat: SimpleDateFormat,
|
||||
pendingAttachments: MutableList<PendingAttachment>
|
||||
pendingAttachments: MutableList<PendingAttachment>,
|
||||
includeMedia: Boolean
|
||||
) {
|
||||
for ((index, attachment) in attachments.withIndex()) {
|
||||
val exportedName = buildAttachmentFileName(attachment, attachmentDateFormat)
|
||||
pendingAttachments.add(PendingAttachment(attachment, exportedName))
|
||||
|
||||
val label = getAttachmentLabel(attachment)
|
||||
|
||||
if (!wrotePrefix && index == 0) {
|
||||
this.write(prefix)
|
||||
}
|
||||
|
||||
val caption = attachment.caption
|
||||
if (caption != null) {
|
||||
this.write("[$label: media/$exportedName] $caption")
|
||||
if (includeMedia) {
|
||||
val exportedName = buildAttachmentFileName(attachment, attachmentDateFormat)
|
||||
pendingAttachments.add(PendingAttachment(attachment, exportedName))
|
||||
|
||||
val caption = attachment.caption
|
||||
if (caption != null) {
|
||||
this.write("[$label: media/$exportedName] $caption")
|
||||
} else {
|
||||
this.write("[$label: media/$exportedName]")
|
||||
}
|
||||
} else {
|
||||
this.write("[$label: media/$exportedName]")
|
||||
val caption = attachment.caption
|
||||
if (caption != null) {
|
||||
this.write("[$label] $caption")
|
||||
} else {
|
||||
this.write("[$label]")
|
||||
}
|
||||
}
|
||||
this.newLine()
|
||||
}
|
||||
|
||||
@@ -552,6 +552,7 @@ class ConversationFragment :
|
||||
private lateinit var giphyMp4ProjectionRecycler: GiphyMp4ProjectionRecycler
|
||||
private lateinit var addToContactsLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var plaintextExportDirectoryLauncher: ActivityResultLauncher<Uri?>
|
||||
private var exportWithMedia = false
|
||||
private lateinit var conversationActivityResultContracts: ConversationActivityResultContracts
|
||||
private lateinit var scrollToPositionDelegate: ScrollToPositionDelegate
|
||||
private lateinit var adapter: ConversationAdapterV2
|
||||
@@ -1604,7 +1605,7 @@ class ConversationFragment :
|
||||
if (uri != null) {
|
||||
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
viewModel.startPlaintextExport(requireContext().applicationContext, uri)
|
||||
viewModel.startPlaintextExport(requireContext().applicationContext, uri, exportWithMedia)
|
||||
}
|
||||
}
|
||||
conversationActivityResultContracts = ConversationActivityResultContracts(this, ActivityResultCallbacks())
|
||||
@@ -4292,7 +4293,19 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
override fun handleExportChat() {
|
||||
plaintextExportDirectoryLauncher.launch(null)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.ChatExportDialogs__export_chat_history_title)
|
||||
.setMessage(R.string.ChatExportDialogs__export_confirm_body)
|
||||
.setPositiveButton(R.string.ChatExportDialogs__export_with_media) { _, _ ->
|
||||
exportWithMedia = true
|
||||
plaintextExportDirectoryLauncher.launch(null)
|
||||
}
|
||||
.setNeutralButton(R.string.ChatExportDialogs__export_without_media) { _, _ ->
|
||||
exportWithMedia = false
|
||||
plaintextExportDirectoryLauncher.launch(null)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -761,7 +761,7 @@ class ConversationViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun startPlaintextExport(context: Context, directoryUri: Uri) {
|
||||
fun startPlaintextExport(context: Context, directoryUri: Uri, withMedia: Boolean) {
|
||||
val recipient = recipientSnapshot ?: return
|
||||
val chatName = if (recipient.isSelf) context.getString(R.string.note_to_self) else recipient.getDisplayName(context)
|
||||
|
||||
@@ -774,12 +774,17 @@ class ConversationViewModel(
|
||||
threadId = threadId,
|
||||
directoryUri = directoryUri,
|
||||
chatName = chatName,
|
||||
includeMedia = withMedia,
|
||||
progressListener = { messagesProcessed, messageCount, attachmentsProcessed, attachmentCount ->
|
||||
val messagePercent = if (messageCount > 0) (messagesProcessed * 25) / messageCount else 25
|
||||
val attachmentPercent = if (attachmentCount > 0) (attachmentsProcessed * 75) / attachmentCount else 75
|
||||
val percent = messagePercent + attachmentPercent
|
||||
val percent = if (withMedia) {
|
||||
val messagePercent = if (messageCount > 0) (messagesProcessed * 25) / messageCount else 25
|
||||
val attachmentPercent = if (attachmentCount > 0) (attachmentsProcessed * 75) / attachmentCount else 75
|
||||
messagePercent + attachmentPercent
|
||||
} else {
|
||||
if (messageCount > 0) (messagesProcessed * 100) / messageCount else 100
|
||||
}
|
||||
|
||||
val status = if (attachmentsProcessed > 0 || messagesProcessed >= messageCount) {
|
||||
val status = if (withMedia && (attachmentsProcessed > 0 || messagesProcessed >= messageCount)) {
|
||||
"Exporting media ($attachmentsProcessed/$attachmentCount)..."
|
||||
} else {
|
||||
"Exporting messages ($messagesProcessed/$messageCount)..."
|
||||
|
||||
Reference in New Issue
Block a user